ASP.NET

Navigation for ASP.NET Web Forms フレームワークの概要

Graham Mendick

コード サンプルのダウンロード

Navigation for ASP.NET Web Forms フレームワークは、navigation.codeplex.com (英語) でホストされているオープン ソース プロジェクトです。このフレームワークにより、単体テスト カバレッジを備えた Web フォーム コードを記述でき、ASP.NET MVC アプリケーションを魅力あるものにしている DRY (Don't repeat yourself) の原則に準拠できます。

分離コードが単体テストによって到達できないほど巨大になるのを嫌うことから、Web フォームよりも MVC を選択する開発者もいましたが、この新しいフレームワークをデータ バインドと連携して使用することは、Web フォームを見直す大きなきっかけになります。

ObjectDataSource コントロールを使ったデータ バインドは Visual Studio 2005 の頃から使用されており、これによって分離コードを整理し、データ取得コードの単体テストが可能になりますが、これを妨げる問題もありました。たとえば、ビジネス検証の失敗を UI に返す唯一の方法が例外の生成であることなどです。

次回リリースされる Visual Studio の Web フォーム開発ではデータ バインドに対して多くの作業が行われており、問題を解決するために、MVC のモデル バインドの概念を導入しています。たとえば、モデル状態を導入することで、ビジネス検証失敗時の通知の問題に対処しています。ただし、データ バインドの面では、ナビゲーションとデータの受け渡しという問題が残りますが、これは Navigation for ASP.NET Web Forms フレームワーク (ここからは単に「Navigation フレームワーク」と呼びます) を使用して難なく抽出できます。

ナビゲーションの問題は、ナビゲーション ロジックがコントローラー メソッドの戻り値の型にカプセル化される MVC とは異なり、ナビゲーション ロジックが抽象化されないことです。このため、データ バインドのメソッド内部でリダイレクト呼び出しを行うことになり、単体テストを行うことができません。データの受け渡しの問題は、ObjectDataSource のパラメーターの型によって、値をどこから取得するかが決まることです。たとえば、QueryStringParameter は値を常にクエリ文字列から取得します。このため、同じデータ ソースを別のナビゲーション コンテキストで使用することはできません。たとえば、ポストバックとポストバック以外のナビゲーションで同じデータ ソースを使用するには、分離コードにかなりのロジックを記述しなければなりません。

Navigation フレームワークは、ナビゲーションとデータの受け渡しに包括的なアプローチを採用することにより、これらの問題を解決します。ハイパーリンク、ポストバック、AJAX の履歴、単体テストなど、実行するナビゲーションの種類にかかわらず、受け渡されるデータは常に同じ方法で保持されます。今後の記事では、このフレームワークを基にして、データ取得とナビゲーションの単体テスト完了済みのロジックを使って分離コードを記述しなくても済むようにする方法と、検索エンジンの最適化 (SEO) に適し、JavaScript 対応と非対応のシナリオでコードの重複をなくした、単一ページのアプリケーションを作成する方法を紹介する予定です。今回は、Navigation フレームワークについて説明し、サンプル Web アプリケーションを作成して、基本的かつ重要な概念を示します。

サンプル アプリケーション

作成するサンプル Web アプリケーションはオンライン アンケートです。アンケートには 2 つの質問があり、完了すると「ありがとうございました」と表示します。質問ごとに Question1.aspx、Question2.aspx と、ASPX ページが分かれています。「ありがとうございます」メッセージも Thanks.aspx というまた別のページです。

最初の質問は「あなたが現在使用している ASP.NET テクノロジーはどちらですか」というもので、回答は「Web フォーム」または「MVC」のいずれかから選択します。そこで、Question1.aspx に質問とハードコーディングしたオプション ボタンの回答を追加します。

<h1>Question 1</h1>
<h2>Which ASP.NET technology are you currently using?</h2>
<asp:RadioButtonList ID="Answer" runat="server">
  <asp:ListItem Text="Web Forms" Selected="True" />
  <asp:ListItem Text="MVC" />
</asp:RadioButtonList>

2 つ目の質問は、「Navigation for ASP.NET Web Forms フレームワークを使用していますか」というもので、回答は「はい」または「いいえ」のいずれかから選びます。こちらも同様の方法でマーク アップを作成します。

手始めに

Navigation フレームワークを使用するようにこのアンケート Web プロジェクトをセットアップする最も単純な方法は、NuGet パッケージ マネージャーを使って Navigation フレームワークをインストールすることです。パッケージ マネージャー コンソールで "Install-Package Navigation" コマンドを実行すると、必要な参照と構成が追加されます。Visual Studio 2010 を使用していない場合は、navigation.codeplex.com/documentation (英語) にある手動セットアップ手順を参照します。

ナビゲーションの構成

Navigation フレームワークは 1 つの状態機械 (ステート マシン) と考えることができ、それぞれの状態が 1 つのページを表し、ある状態から別の状態へ遷移がページ間のナビゲーションになります。このあらかじめ定義された状態と遷移のセットが、NuGet のインストールによって作成される StateInfo.config ファイルに構成されます。この基礎となる構成がないままアンケート アプリケーションを実行すると、例外がスローされます。

状態は事実上単なるページなので、このアンケート アプリケーションでは 3 つのページにそれぞれ対応する 3 つの状態が必要です。

<state key="Question1" page="~/Question1.aspx">
</state>
<state key="Question2" page="~/Question2.aspx">
</state>
<state key="Thanks" page="~/Thanks.aspx">
</state>

ここからは、それぞれの状態を表すページではなく、それぞれのキーの名前、Question1、Question2、Thanks を使って状態を示します。

遷移は状態間で可能なナビゲーションを表すため、アンケート アプリケーションには 2 つの遷移が必要です。1 つは Question1 から Question2 へのナビゲーション、もう 1 つは Question2 から Thanks へのナビゲーションです。1 つの遷移は、遷移元の状態の子要素で、その "to" 属性で遷移先の状態を示します。

<state key="Question1" page="~/Question1.aspx">
  <transition key="Next" to="Question2"/>
</state>
<state key="Question2" page="~/Question2.aspx">
  <transition key="Next" to="Thanks"/>
</state>
<state key="Thanks" page="~/Thanks.aspx">
</state>

ダイアログが構成の最後の要素で、状態の論理グループを表します。Question1、Question2、Thanks は事実上 1 つのナビゲーション パスなので、アンケート アプリケーションにはダイアログが 1 つしか必要ありません。ダイアログの "initial" 属性は最初の状態、つまり Question1 を指す必要があります。

<dialog key="Survey" initial="Question1" path="~/Question1.aspx">
  <state key="Question1" page="~/Question1.aspx">
    <transition key="Next" to="Question2"/>
  </state>
  <state key="Question2" page="~/Question2.aspx">
    <transition key="Next" to="Thanks"/>
  </state>
  <state key="Thanks" page="~/Thanks.aspx">
  </state>
</dialog>

ダイアログ、状態、遷移にはそれぞれ "key" 属性があります。ここではページの名前を使って状態キーに名前を付けましたが、これは必須ではありません。ただし、すべてのキーはその親の下で一意になる必要があります。たとえば、同じキーを使って兄弟関係の状態を表すことはできません。

Question1.aspx を開始ページに指定すると、アンケート アプリケーションは Question1 の状態から正常に開始されるようになります。しかし、Question2 に進む方法がないため、このアンケートは Question1 の状態のまま遷移しません。

ナビゲーション

Web フォームのナビゲーションの種類はわかりやすく 2 つに大別できます。ポストバックを行わない方法では、ハイパーリンク、リダイレクト、転送のいずれかの形式で、ある ASPX ページから別の ASPX ページに制御が渡されます。ポストバックを行う方法では、ポストバック、部分ページ要求、AJAX の履歴のいずれかの形式で、同じページ内で引き続き制御されます。ポストバックについては今後の記事で扱い、シングル ページ インターフェイス パターンについて説明する予定です。今回は、ポストバックを行わないナビゲーションに注目します。

ページ間を移動するには、URL を構築する必要があります。Visual Studio 2008 より前は、ハードコーディングした ASPX ページ名から手動で URL を構築するのが唯一の方法でした。そのためページ間に密結合が生じ、アプリケーションが脆弱になり、メンテナンスを困難なものにしていました。ルーティングの導入により、ページ名に代えて構成可能なルート名を使うことによって、この問題は軽減されましたが、ルーティングを Web 環境外部で使用すると例外がスローされることと、ルーティングではモックを作成できないことから、単体テストが困難です。

Navigation フレームワークでは、ルーティングが提供する疎結合を保持することにより、単体テストを容易にしています。ルート名を使用するのと同様に、ASPX ページ名をハード コーディングするのではなく、コードから参照するのは、前のセクションで構成されるダイアログと遷移の "key" 属性で、遷移先の状態は "initial" 属性と "to" 属性によって決定されます。

アンケートに戻り、遷移キー "Next" を使って Question1 から Question2 に移動します。Question1.aspx に [Next] ボタンを追加し、関連付けるクリック ハンドラーに次のコードを追加します。

protected void Next_Click(object sender, EventArgs e)
{
  StateController.Navigate("Next");
}

Navigate メソッドに渡すキーが Question1 状態の構成済みの子遷移と照合され、"to" 属性で特定される状態、つまり Question2 が表示されます。同じボタンとハンドラーを Question2.aspx にも追加します。アンケートを実行して [Next] をクリックすると、3 つの状態を遷移できます。

2 つ目の質問は Web フォーム独特の質問なので、1 つ目の質問で回答として「MVC」を選んだ場合には意味がないことがわかります。このシナリオに対応し、Question1 から Question2 を飛ばして直接 Thanks に移動するには、コードに変更が必要です。

現在の構成でリストされている遷移は Question2 へのものだけで、Question1 から Thanks へは遷移できません。そこで構成を変更し、第 2 の遷移を Question1 状態に追加します。

<state key="Question1" page="~/Question1.aspx">
  <transition key="Next" to="Question2"/>
  <transition key="Next_MVC" to="Thanks"/>
</state>

この新しい遷移を追加したら、[Next] ボタンのクリック ハンドラーを調整して、回答によって別の遷移キーを渡すようにするのは容易です。

if (Answer.SelectedValue != "MVC")
{
  StateController.Navigate("Next");
}
else
{
  StateController.Navigate("Next_MVC");
}

アンケートではユーザーが回答を変更できるようにしておくのが親切です。現状では、前の質問に戻る方法がありません (ブラウザーの [戻る] ボタンを除く)。ページを戻る遷移を行うには、Thanks に Question1 と Question2 を指す 2 つの遷移を追加し、Question2 に Quesion1 を指す遷移を追加する必要があると考えたかもしれません。もちろんそれでも機能しますが、ページを戻る遷移は Navigation フレームワークに含まれているため、その必要はありません。

階層リンク ナビゲーションは、ユーザーが現在のページに到達するまでに表示したページにアクセスできる一連のリンクです。Web フォームにはサイト マップ機能に組み込まれた階層リンク ナビゲーションがあります。しかし、サイト マップは固定ナビゲーション構造により表されるため、特定のページの階層リンクはたどったルートにかかわらず常に同じです。これではこのアンケートで、Thanks に到達するまでに Question2 を飛ばした場合に対応できません。Navigation フレームワークでは、遷移が行われアクセスした状態を追跡することにより、実際にアクセスしたルートの階層リンクの軌跡を構築します。

これを示すために、ハイパーリンクを Question2.aspx に追加し、ページを戻る遷移を使った NavigateUrl プロパティをプログラムで分離コードに設定します。状態をいくつ戻るかを示す distance パラメーターを渡す必要があります。値 1 は直前の状態に戻ることを意味します。

protected void Page_Load(object sender, EventArgs e)
{
  Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
}

アプリを実行し、質問 1 に「Web フォーム」と答えると、質問 1 に戻るハイパーリンクが Question2.aspx に設定されます。

Thanks.aspx にも同じことを行いますが、こちらはそれぞれの質問に戻るため 2 つのハイパーリンクが必要となり、またユーザーは質問 2 に回答していない場合もあるため (質問 1 に「MVC」と回答した場合)、少し複雑です。ハイパーリンクの設定方法を決める前に、それまでの状態の数をチェックします (図 1)。

図 1 ページを戻る動的ナビゲーション

protected void Page_Load(object sender, EventArgs e)
{
  if (StateController.CanNavigateBack(2))
  {
    Question1.NavigateUrl = StateController.GetNavigationBackLink(2);
    Question2.NavigateUrl = StateController.GetNavigationBackLink(1);
  }
  else
  {
    Question1.NavigateUrl = StateController.GetNavigationBackLink(1);
    Question2.Visible = false;
  }
}

これでこのアンケートは、質問に回答して、前の回答を変更できるようになりました。しかし、アンケートではこれらの回答を活用できるようにする必要があります。回答を Question1 から Question2 へ、さらに Thanks へ渡し、そこでサマリーを表示する方法を示します。

データの受け渡し

Web フォームでデータの受け渡しを行う方法はいろいろあります。これはナビゲーションを行う方法がいろいろあるのと同じです。ハイパーリンク、リダイレクト、転送によってあるページから別のページに制御を渡す、ポストバックを行わないナビゲーションでは、クエリ文字列やルート データなどを使用します。ポストバック、部分ページ要求、AJAX の履歴によって同じページ内に制御がとどまる、ポストバック型のナビゲーションでは、通常、コントロール値、ビュー ステート、イベント引数などを使用します。

Visual Studio 2005 より前は、受け渡されるデータの処理の負担が分離コードにかかっており、値の抽出と型変換のロジックでコードが膨れ上がっていました。この負荷は、データ ソース コントロールと選択パラメーター (Visual Studio の次期バージョンでは「値プロバイダー」) の導入によって、かなり軽減されています。しかし、このような選択パラメーターは特定のデータ ソースに結び付けられ、ナビゲーションのコンテキストに応じて動的にソースを切り替えることができません。たとえば、ポストバックを行うかどうかによって、コントロールから値を取得したり、クエリ文字列から値を取得するように切り替えることはできません。これらの制限事項を回避しようとすると、また分離コードが膨れ上がることになり、テストできない巨大な分離コードという最初の問題に戻ります。

Navigation フレームワークでは、どのナビゲーションを使う場合でも、状態データという 1つのデータ ソースを用意することにより、この問題を回避します。最初にページを読み込むときに、ナビゲーション時に受け渡されたデータを状態データに設定します。これはクエリ文字列やルート データと同様の方法です。ただし、注意すべき相違点は、状態データが読み取り専用ではないことです。そのため連続してポストバック ナビゲーションが行われると、現在のページを反映して状態データが更新される場合があります。このセクションの最後でナビゲーションについて再度触れる際に、これがメリットとなることがわかります。

アンケートを変更して、質問 1 への回答が Thanks 状態に受け渡され、ユーザーに表示されるようにします。キーと値のペアのコレクションを使ってナビゲーションを行うときに、データを受け渡します。このデータを NavigationData と呼びます。Question1.aspx の Next クリック ハンドラーを変更して、質問 1 の回答を次の状態に受け渡すようにします。

NavigationData data = new NavigationData();
data["technology"] = Answer.SelectedValue;
if (Answer.SelectedValue != "MVC")
{
  StateController.Navigate("Next", data);
}
else
{
  StateController.Navigate("Next_MVC", data);
}

ナビゲーション時に受け渡すこの NavigationData は、状態データの初期化に使用します。このデータは次の状態で StateContext オブジェクトの Data プロパティから取得できます。Thanks.aspx に Label を追加し、Text プロパティを設定して受け渡された回答を表示します。

Summary.Text = (string) StateContext.Data["technology"];

アンケートを実行すると、このサマリー情報は質問 1 の回答が「MVC」の場合のみ表示され、「Web フォーム」の場合は表示されないことがわかります。これは NavigationData を使用できるのは次の状態のみで、その後のナビゲーションによって到達する状態では使用できないためです。そのため「Web フォーム」という回答は Question2 の状態データには存在しますが、Thanks に到達する時点では利用できません。これを解決する 1 つの方法は、Question2.aspx を変更して、質問 1 の回答を中継することです。つまり、状態データから回答を取得し、ナビゲーションが行われると、それを Thanks に受け渡します。

NavigationData data = new NavigationData();
data["technology"] = StateContext.Data["technology"];
StateController.Navigate("Next", data);

この方法は Question1 と Question2 を結び付け、Question2 の状態は Question1 が受け渡すデータを認識しておく必要があるため、あまり望ましくありません。たとえば、質問 1 と質問 2 の間に新しい質問を挿入する場合には、Question2.aspx に変更を加える必要があります。将来性に対応した実装としては、Question2 の状態データのすべてを含む新しい NavigationData を作成します。これは NavigationData コンストラクターに true を渡すことによって行います。

NavigationData data = new NavigationData(true);
StateController.Navigate("Next", data);

状態データと、クエリ文字列やルート データとの重要なもう 1 つの相違点は、状態データを使う場合、受け渡すデータは文字列に限らないことです。Question1 で行ったように文字列を使って回答を受け渡すのでなく、Question2 では「はい」に対応する true 値を使って Thanks にブール値を受け渡します。

NavigationData data = new NavigationData(true);
data["navigation"] = Answer.SelectedValue == "Yes" ? true : false;
StateController.Navigate("Next", data);

Thanks 状態データから取得する際は、データ型が保持されることがわかります。

Summary.Text = (string) StateContext.Data["technology"];
if (StateContext.Data["navigation"] != null)
{
  Summary.Text += ", " + (bool) StateContext.Data["navigation"];
}

これでアンケートは完成しましたが、1 つだけ問題があります。ページを戻るナビゲーション ハイパーリンクを使うと、質問への回答が保持されないことです。たとえば、Thanks から Question1 に戻るときにはコンテキストが失われ、それまでの回答にかかわらず、既定の「Web フォーム」のオプション ボタンが常に選択状態になります。

前のセクションでは、静的サイト マップの階層リンクに対する、戻るナビゲーションのメリットを説明しました。サイト マップが生成する階層リンクのもう 1 つの制限事項は、階層リンクがデータを伴わないことです。つまり、階層リンクをたどる場合にはコンテキスト情報が失われます。たとえば、Thanks から Question1 に戻るときに、以前に選択されていた「MVC」という回答を受け渡すことができません。Navigation フレームワークでは、遷移が行われてアクセスされた状態に関連付けられる状態データを追跡することにより、状況に依存する階層リンクの軌跡を構築します。ページを戻るナビゲーションの間にこの状態データが復元され、ページを以前とまったく同様に再作成することができます。

状況依存の戻るナビゲーションを使って、状態に再度アクセスする際も回答が保持されるように、アンケートを変更します。最初の段階では、遷移を行う前に、Next クリック ハンドラーで回答を状態データに設定します。

StateContext.Data["answer"] = Answer.SelectedValue;

これで、Question1 や Question2 に再度アクセスするときに、状態データには以前に選択されていた回答を含みます。これにより、この回答を Page_Load メソッドで取得し、関連するオプション ボタンをあらかじめ選択しておくことを簡単に行えます。

protected void Page_Load(object sender, EventArgs e)
{
  if (!Page.IsPostBack)
  {
    if (StateContext.Data["answer"] != null)
    {
      Answer.SelectedValue = 
        (string)StateContext.Data["answer"];
    }
  }
}

このようにして完成したアンケートは、ユーザーがブラウザーの戻るボタンを押した際 (または複数のブラウザーを開いた際) に Web アプリケーションで通常発生するエラーに対して脆弱ではありません。そのような問題は、通常、ページ特有のデータがサーバー側のセッションに永続化されることにより発生します。セッション オブジェクトは 1 つしか存在しませんが、単一ページの現在バージョンが複数存在する場合があります。たとえば、戻るボタンを使って、ブラウザーのキャッシュからページの最新でないバージョンを取得することにより、クライアントとサーバーの同期を失う場合があります。Navigation フレームワークはサーバー側キャッシュを持たないため、そのような問題が発生することはありません、代わりに、状態、状態データ、階層リンクの軌跡はすべて URL に保持されています。ただし、これはユーザーが URL を編集することによって、それらを変更可能であることを意味します。

MVC に対するメリット

Navigation フレームワークを使うと、Web フォーム コードを作成することができ、これは MVC に対する大きなメリットであることを以前に説明しました。その断言の割には、このアンケート サンプル アプリケーションは少々期待外れだと思うかもしれません。分離コードが面倒になる点が MVC よりも優れているようには見えないかもしれないためです。しかし落胆するのはまだ早く、これは主要概念の入り口にすぎません。今後の記事では、単体テストの実施と DRY 原則に注意を払って、アーキテクチャの整合性に注目します。

次回は、空の分離コードと完全な単体テストのコード カバレッジを備えた、データ バインドされたサンプルを作成します。このカバレッジにはナビゲーション コードをも含みます。これは MVC アプリケーションではテストが非常に困難なことで有名です。

さらにその次の回では、SEO 対応のシングル ページ アプリケーションを作成します。JavaScript を有効にしている場合は ASP.NET AJAX を使って機能を拡張し、無効にしている場合は適切に機能を低下させます。どちらのシナリオでも同じデータ バインドのメソッドを使います。繰り返しになりますが、これは MVC アプリケーションで行うと非常に複雑になります。

さらに高度な機能に興味があり、次回を待ちきれない場合は、navigation.codeplex.com (英語) から完全な機能のドキュメントとサンプル コードをダウンロードできます。

Graham Mendick は Web フォームの熱心な愛好者で、Web フォームが ASP.NET MVC と同様に優れたアーキテクチャであることを示してきています。彼は Navigation for ASP.NET Web Forms フレームワークを作成しました。これをデータ バインドと連携して使用することにより、Web フォームに新たな息吹をもたらすと信じています。

この記事のレビューに協力してくれた技術スタッフの Damian Edwards に心より感謝いたします。