Cutting Edge
ASP.NET の部分レンダリングを使用して AJAX をプログラミングする
Dino Esposito

目次
AJAX の中心は XMLHttpRequest オブジェクトです。AJAX を使用した場合のユーザー エクスペリエンスの能力は、複数のプラットフォーム上の多様なブラウザでこのオブジェクトを使用できるかどうかによって決まります。コンポーネントのベンダが初めて AJAX アプリケーションを発表した 2004 年以降、いろいろなことがありましたが、根本的には、XMLHttpRequest オブジェクトを使用した帯域外呼び出しが実行できなければ AJAX は実現不可能です。
AJAX アプリケーションの複雑さが増すにつれ、開発者は、プレーンなスクリプト駆動の帯域外呼び出しでは新世代の Web アプリケーションをコスト効果の高い方法で構築するには不十分だということを認識するようになりました。そこで、より高機能なツール セットに対する需要が高まっています。これらのツールでは、従来の ASP.NET と同じ開発パラダイムを使用しながら、ページやアプリケーションに AJAX 機能を追加することが想定されています。つまり、AJAX サイトを構築するには、開発者には XMLHttpRequest オブジェクトのプレーン呼び出しでデータを取得し、ドキュメント オブジェクト モデル (DOM) を操作する JavaScript 関数を加工する以上の機能が必要です。
今月のコラムでは、ASP.NET の部分レンダリング エンジンを利用した AJAX の実用的な手法を紹介します。これから説明するように、AJAX にはアプリケーションのパフォーマンスと開発者の生産性とのトレードオフがあります。現実には、AJAX サイトの全体を部分レンダリングまたは XMLHttpRequest オブジェクトの手動スクリプトで構築することはありません。多くの場合、カスタム コントロールを使用して適切に合成された手法を高度に混合する必要があります。
メニューを表示するページ
AJAX の最も重要な役割は、ページ全体の再読み込みの回数を少なくすることにあります。部分レンダリングと手動によるスクリプト駆動呼び出しは、どちらを使用しても、サーバー側データを取得でき、ページ全体の更新が不要になります。多くのコンテンツの種類では、バックエンド サービスと DOM の操作で十分です。しかし、ナビゲーションをサポートする必要がある場合はどうでしょうか。
従来のハイパーリンクは AJAX の魔法を解き、別の URL に要求を送るようブラウザに指示します。その結果、HTML の新しいチャンクがダウンロードされるまで、現在のページは停止します。新しいデータが到着すると、ページは破棄され、ブラウザのクライアントが完全に再描画されます。
ユーザーをサイトの別の領域に移動させるマスタ ページ上に配置された最上位リンクは、実際には従来のハイパーリンクとして実装されている場合があります。この場合には、ユーザーの要望に応じてページ全体の再読み込みも許容されます。
メニューを作成する都度、ユーザー クリックの処理方法を選択してください。各メニュー項目に URL を割り当てることも、同じページにポストバックするだけにすることもできます。AJAX の観点からは、現在のページを破棄して新しいページを読み込むか、サーバーから新しいコンテンツを非同期に読み込むかを選択します。
次のコード例は、ASP.NET の Menu コントロールの一部を示しています。
<asp:Menu runat="server" ID="Menu1">
<asp:MenuItem Text="Products" Value="Products">
<asp:MenuItem Text="By price" NavigateUrl="..." />
<asp:MenuItem Text="All" Value="Products-All" />
</asp:MenuItem>
...
</asp:Menu>
Products メニュー項目の最初の MenuItem 子要素は、NavigateUrl プロパティを示しています。このプロパティは、メニュー項目をクリックしたときに移動する URL を取得または設定します。2 番目の MenuItem 要素は Value プロパティを示しています。Value プロパティはメニュー項目に関する追加データの保存に使用され、MenuItem のポストバック イベントに渡されます。同じレベルのメニュー項目の Value プロパティには、一意の値を指定する必要があります。明示的なナビゲーション URL が指定されていない場合、メニュー項目をクリックすると、従来のポストバックが行われます。サーバー上では、MenuItemClick イベントを処理し、Value プロパティのコンテンツを処理するだけです。
当然ながら、Value プロパティを使用する方法を選択した場合は、AJAX の手法を使用してページ全体の再読み込みを不要にすることができます。このタイプのページの全体的なモデルは、シングル ページ インターフェイスです。
シングル ページ インターフェイスでは、再読み込みの回数が減り、ちらつきがなくなるため、ユーザー インターフェイスの負荷が軽減されます。ただし、シングル ページ インターフェイスの場合、アプリケーションで使用する URL があまり細分化されていないので、検索エンジンからのサポートが縮小されます。また、シングル ページ インターフェイスでは、開発チームが作成するページ数が少なくなり、各ページの密度が濃くなります。このような方法では、チーム内の並行作業も減少します。
ナビゲーション URL
メニューでユーザーを新しいページに移動させる場合、ソリューションの実装に Menu コントロールは不要です。理論的には、ハイパーリンクのリストがあれば十分です。使用可能な DHTML および AJAX メニュー フレームワークは多数ありますが、提供するアニメーション、グラフィックス、定義済みスキンによって異なります。機能的には、これらのメニューはハイパーリンクのプレーンなコレクションであり、ハイパーリンクはブラウザによって AJAX 以外の方法でネイティブに処理されます。
一般に、合計ページ数が数百ページに及ぶ大規模な Web サイトの設計では、少数のエントリ ページを中心にして、そこからユーザーを別のサブサイトに遷移させるようにする傾向があります。このような設計では、ホーム ページに必要な機能は各サブサイトへのリンクのみです。この場合、ナビゲーションは必要ですが、AJAX は必要ありません。ところが、現在のページにコンテンツを表示する場合はどうでしょうか。アプリケーションのパフォーマンスとチームの生産性を高めるには、このコンテンツをどのように整理したらよいでしょうか。
非同期ポストバック
先月は Windows® Communication Foundation (WCF) サービスを使用して生データまたは HTML をクライアントに返す方法を紹介しました。どちらのソリューションにも長所と短所があります。生データを送信すると、帯域幅が最適化されますが、JavaScript を使用して実装されたクライアントに DOM 操作ロジックが必要になります。サーバーで生成した HTML を送信すると、移動するデータ量が増えますが、サーバー上のレンダリング ロジックの多くを維持できます。
どちらの方法も、アプリケーションが明確に 2 層モデル (Web ブラウザ層とサービス層) に従って設計される、いわゆる "純粋な" AJAX ソリューションのカテゴリに属します。ユーザー インターフェイスに関する状態およびロジックはクライアントによって維持および制御されます。ビュー状態やポストバックは不要です。
サーバーで生成された HTML は、読み取り専用のマークアップを返す場合に特に便利です。データの静的グリッドや、選択した顧客または請求書に関する情報を示すパネルを返す必要がある場合に適しています。イベントを生成し、ハンドラを必要とするコントロールが多い対話型のパネルを表示する場合には、あまり適しません。純粋な AJAX 方式では、表示されたマークアップ (クライアント上またはサーバー上のどちらで生成された場合でも) に JavaScript 関数の呼び出しを含める必要があります。
先月の記事では、HTML メッセージ パターンとブラウザ側テンプレート パターンを実装するサービスを使用しました。ところが、お気付きかもしれませんが、そのときの例は実際には対話型ではありませんでした。どちらの方法も、それぞれ適している場合と適さない場合があります。紹介したサンプル サービスでは株価を返しましたが、最終的なグリッドはページング可能ではありませんでした。ページングをサポートするには、たとえば、JavaScript 関数を参照するハイパーリンクを挿入し、参照した JavaScript がダウンロードされたことを確認するなどの処理が必要でした。
メニューと部分レンダリング
図 1 は、ユーザーが一連のアプリケーション機能にナビゲートするための最上位メニューが表示されたサンプル ページを示しています。このメニューは、図 2 のように、ASP.NET Menu コントロールを使用して作成されています。ご覧のとおり、どのメニュー項目でも NavigateUrl 属性を指定していません。この属性を指定すると、各メニュー項目で物理的な URL が参照され、まったく違うページに遷移する可能性があります。Value 属性を指定すると、ユーザーがいずれかのメニュー項目を選択するたびにポストバックが発生し、ページに MenuItemClick イベントが生成されます。
void Menu1_MenuItemClick(object sender, MenuEventArgs e)
{
LoadContent(e.Item.Value);
}
図 1 最上位メニューが表示された ASP.NET AJAX ページ (クリックすると拡大画像が表示されます)

図 2 サンプル ページのメニュー
<asp:Menu ID="Menu1" runat="server" Orientation="Horizontal"
onmenuitemclick="Menu1_MenuItemClick">
<Items>
<asp:MenuItem Text="Customers">
<asp:MenuItem Text="All"
Value="Customers-All" />
<asp:MenuItem Text="By Country"
Value="Customers-by-country" />
</asp:MenuItem>
<asp:MenuItem Text="Orders">
<asp:MenuItem Text="1997"
Value="Orders-1997" />
<asp:MenuItem Text="By Customer"
Value="Orders-by-customer" />
</asp:MenuItem>
<asp:MenuItem Text="Products">
<asp:MenuItem Text="All"
Value="Products-All" />
</asp:MenuItem>
</Items>
</asp:Menu>
MenuEventArgs クラスは、クリックされたメニュー項目を表す Item プロパティを指定しています。MenuItem オブジェクトの Value プロパティは、クリックされた項目の ID をイベント ハンドラに通知します。ローカル関数 LoadContent を使用して、必要なコンテンツをページに動的に読み込むことができます。では、ポストバックについてはどうでしょう。
ASP.NET Menu コントロールは部分レンダリングを完全にサポートしています。ポストバックを処理して新しいコンテンツを円滑に読み込むために必要なのは、次のように、ScriptManager コントロールを設定して UpdatePanel コントロールを挿入することだけです。
<asp:UpdatePanel runat="server" ID="UpdatePanel1"
UpdateMode="Conditional">
<ContentTemplate>
...
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Menu1"
EventName="MenuItemClick" />
</Triggers>
</asp:UpdatePanel>
更新可能なコンテンツが Menu の MenuItemClick にバインドされます。UpdateMode プロパティを使用して、パネルにコントロール固有の更新も設定します。これにより、更新可能な領域は、ユーザーがメニュー項目をクリックしたときにのみ更新されます。
多くの Web アプリケーションでは、メニューを使用して、ユーザーがサイトまたはページの別の機能領域を参照するようにします。機能ごとに異なる ASPX ページを実装すると、いくつかの理由で便利です。まず、コードがわかりやすく、保守およびテストが楽になります。さらに重要なこととして、チームの開発者ごとに個別の機能の実装を割り当てることができます。そのため、いくつかの開発作業を平行して行うことにより生産性を高めることが可能になります。
シングル ページ インターフェイスは、イベントが発生するたびにユーザー インターフェイスを再編成するアプリケーションにメイン ページを設けることを提案する一般的な AJAX パターンです。シングル ページ インターフェイス モデルはポストバックを最小限に抑えるという点で優れていますが、チームの開発作業を平行して行う場合には注意が必要です。シングル ページ インターフェイス モデルは、プラグインの概念に基づくページ アーキテクチャであると考えてください。つまり、動的に読み込まれたコンポーネントをすばやく簡単にプラグインできるように、コントラクトに基づくインターフェイスを持つプレースホルダをページに定義します。
プレースホルダを使用する
ASP.NET でプラグイン指向のページ アーキテクチャを実現する最も簡単な方法は、PlaceHolder コントロールと ASCX ユーザー コントロールを使用してオンデマンド コントロールを定義することです。更新可能な領域の定義方法を次に示します。
<asp:UpdatePanel runat="server" ID="UpdatePanel1"
UpdateMode="Conditional">
<ContentTemplate>
<asp:PlaceHolder runat="server" ID="PlaceHolder1"
EnableViewState="false" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Menu1"
EventName="MenuItemClick" />
</Triggers>
</asp:UpdatePanel>
各メニュー項目を ASCX ユーザー コントロールに関連付けて、メニュー項目がクリックされるたびにプレースホルダに読み込みます。図 3 は、MenuItemClick イベント ハンドラから呼び出す上記の LoadContent 関数のソース コードを示しています。

図 3 コンテンツを動的にページに読み込む
public partial class _Default : System.Web.UI.Page
{
public string TrackedUserControl
{
get { return ViewState["TrackedUserControl"] as string; }
set { ViewState["TrackedUserControl"] = value; }
}
protected void Menu1_MenuItemClick(object sender, MenuEventArgs e)
{
LoadContent(e.Item.Value);
}
private void LoadContent(string menuItemName)
{
string ucUrl = menuItemName + ".ascx";
UserControl uc = null;
try
{
uc = this.LoadControl(ucUrl) as UserControl;
}
catch(Exception ex)
{
}
if (uc != null)
{
Placeholder1.Controls.Add(uc);
TrackedUserControl = ucUrl;
}
}
...
}
Page クラスの LoadControl メソッドは、ASP.NET ユーザー コントロールの URL を受け取り、その URL を UserControl から派生したクラスのインスタンスとしてメモリに読み込みます。コントロールが正常に読み込まれると、メソッドはそのコントロールを PlaceHolder の Controls コレクションに追加します。ASP.NET PlaceHolder コントロールはマークアップを出力しないので、このコントロールを使用してもユーザー インターフェイスのマークアップに混乱が生じる心配はありません。
ASCX コントロールは ASP.NET の一種なので、動的な対話型コンテンツを表示する AJAX サイトのコンテキストで ASCX ユーザー コントロールを使用しても、コンテンツの複数ブロックを同時に開発するチャンスを減らすことなく、サイトの他の部分と独立して開発することが可能です。
動的に読み込まれたコントロール
ASP.NET では、ポストバック間の動作があるので、動的に読み込まれたコントロールには多少の注意が必要です。たとえば、顧客に関するページング可能なグリッドを表示するユーザー コントロールを作成し、作成したユーザー コントロールをプレースホルダに読み込むとしましょう (図 4 を参照)。最初は問題なく動作しますが、下部のハイパーリンクの 1 つをクリックして新しいページに移動すると、ページは部分的に更新され、ユーザー コントロールのコンテンツ全体が表示されなくなります。なぜでしょうか。
図 4 ページに動的に読み込まれた対話型コントロール (クリックすると拡大画像が表示されます)
ASP.NET では、各ページ要求は、それ以前または以後の他の要求とは独立して処理されます。そのため、ページ クラスの新しいインスタンスが作成され、受信した各要求にサービスを提供します。この新規ページには、ASPX ソース コードで静的に参照されているすべてのサーバー コントロールの新しいインスタンスが含まれます。
では、動的に追加されたコントロールはどうでしょうか。ページ内のコードで永続化メカニズムを使用して動的コントロールを追跡しない限り、ページには動的に追加されたコントロールは認識されません。ページのビュー状態は、この種の情報の優れた記憶媒体になります。図 3 では、ページ クラスに TrackUserControl プロパティを追加して、現在表示されているユーザー コントロールを記憶しています。
Page_Load イベントで、TrackUserControl プロパティのコンテンツを使用して、ページで前回使用されていたのとまったく同一のコントロール コレクションを復元するために手動で再読み込みする必要があるユーザー コントロールを特定します。
void Page_Load(object sender, EventArgs e)
{
if (!IsPostingFromMenu() && IsPostBack)
ReloadContent();
}
この場合、ポストバックの発生原因としては、表示されたユーザー コントロール内からユーザーがアクションをトリガしたか、ユーザーが別のメニュー項目をクリックしたかことが考えられます。Page_Load の if ステートメントは、ユーザーがメニューからポストしているかどうかを判断します。メニューからポストしている場合は、新しいコンテンツを読み込む必要があるため、何も起こりません。メニュー以外からポストしている場合は、ポストバック イベントを処理するために、追跡されたユーザー コントロールが再読み込みされます。
void ReloadContent()
{
UserControl uc = null;
try
{
uc = this.LoadControl(TrackedUserControl) as UserControl;
}
catch
{
}
if (uc != null)
Placeholder1.Controls.Add(uc);
}
動的に追加されたユーザー コントロールのビュー状態はどうなるでしょうか。また、HTML 要素からそのユーザー コントロールにポストされた可能性があるデータはどうなるでしょうか。ユーザーにページを提供する場合、静的に追加されたコントロールと動的に追加されたコントロールには区別がありません。入力フィールドのテキストなど、すべてのユーザー入力は、フォームの送信時に自動的に HTTP 要求にパックされます。そのため、動的に読み込まれたコントロールで使用されるすべての情報がサーバー上で使用可能になります。ただし、ページの Init イベントの発生時には、このようなコントロールはまだ使用できません。なぜなら、ページのコントロール ツリーには静的に参照されたコントロールのみが設定されるためです。
動的に参照されたコントロールをツリーに追加するかどうかはページの作成者が決定します。ただし、ページの作成者は、追加されたコントロールを永続化された記憶媒体から読み取ることが必要な場合があります。この情報がセッションまたはキャッシュに保存されている場合、Init イベント ハンドラから安全にアクセスできます。この情報がビュー状態バッグに保存されている場合、Load イベントを待機する必要があります。実際に ASP.NET ページのライフサイクルでは、ビュー状態はアンパックされ、Init イベントと Load イベントの間で処理されます。
この段階で、ポストされたすべてのデータは検査され、既存のコントロールにマップされます。では、動的に追加されたコントロール用のデータはどうなるでしょうか。基本的には、ポストされたデータは Page__Load イベントの前か後のいずれかで処理されます。イベント前の処理で一致するコントロールが見つからなかったデータはキャッシュされ、Load イベントの後で再処理されます。
Load イベントで、開発者は、前回動的に追加されたコントロールはどれかを確認し、ページ コントロール ツリーに再読み込みする必要があります。好都合なことに、Controls コレクションの Add メソッドは、ビュー状態で見つかったそのコントロールのデータを使用してコントロールの状態を自動更新します。
どのコントロールがポスト バックされたか
一部のコンテンツを外部リソースから読み込むページでは、ポストバックが発生した原因を知る必要があります。図 4 に示すサンプル コードの場合、ページがポストバックした原因は、ユーザーがメニューから特定の要素を選択したか、ユーザー インターフェイス上の別のコンポーネントを操作中にポストバックをトリガしたことである可能性があります。
ASP.NET では、どのコントロールでポストバックが発生したかを通知するプロパティは存在しません。ただし、ポストしたコントロールの ID を確認することは難しくありません。ユーザーが送信ボタンをクリックしてポストバックすると、コントロールの ID が HTTP 要求に表示されます。HTTP 要求の本体にボタン コントロールが存在しない場合、ユーザーはリンク ボタンまたは自動ポストバック コントロールを操作してポストバックします。
いずれの場合も、ポストバック ソースの ID は __EVENTTARGET 非表示フィードにあります。ASP.NET AJAX では、ScriptManager コントロールがカスタム プロパティ (AsyncPostBackSourceElementID プロパティ) を公開しているので、部分レンダリングを使用すると処理が簡単になります。次のコードは図 4 で作成したコードの抜粋です。このコードでは、ユーザーがメニュー項目をクリックしたか、別のコントロールを使用してサーバーにポストバックしたかどうかを確認する方法を示しています。
bool IsPostingFromMenu()
{
ScriptManager sm = ScriptManager.GetCurrent(this);
string ctlID = sm.AsyncPostBackSourceElementID;
Control c = this.FindControl(ctlID);
if (c == null)
return false;
return (c.ID == "Menu1");
}
ユーザーがメニューを何度もクリックすると、特定の ASCX ユーザー コントロールが再び読み込まれる可能性があります。ユーザー コントロールを読み込むということは、コントロールのマークアップをダウンロードし、サーバーのライフサイクルに従って処理されることを意味します。ただし、ユーザー コントロールを連続して再読み込みするとパフォーマンスに悪い影響を与える場合があります。出力キャッシュなどの ASP.NET 機能を使用すると、ページの全体的なパフォーマンスが向上する可能性があります。
ユーザー コントロールの出力をキャッシュする
一部のコンテンツをサイトに提供するユーザー コントロールの作成は、1 回以上のデータベース呼び出しと、時には数百万 CPU サイクルが必要になる場合があるため、負荷が大きくなる可能性があります。はたして、1 秒間に何度も同じユーザー コントロールを再生成する理由があるでしょうか。継続的な変更が行われないコンテンツの場合は特に疑問です。
より優れた方法は、ユーザー コントロールを 1 回作成したら、その出力をキャッシュし、期間を最大にすることです。ユーザー コントロールのキャッシュされたスナップショットが古くなると、最初の受信要求が標準的な方法で再度処理され、コントロールのコードが実行されて、生成されたマークアップが再度キャッシュされます。
ASP.NET のページ出力キャッシュ機能では、ページおよびユーザー コントロールの応答をキャッシュすることにより、コードを実行しなくても、キャッシュされた出力を返すだけで後続の要求を満たすことができます。ユーザー コントロールをキャッシュ可能にするには、次のコードに示すように、ASCX のソースに @OutputCache 属性を宣言します。このコード例では、コントロールの出力を 1 分間キャッシュします。
<% @OutputCache Duration="60" VaryByParam="None" %>
出力キャッシュは優れた機能ですが、特効薬とは言えず、アプリケーションがより多くのページおよびユーザー コントロールをさらに迅速に提供するための 1 つの方法にすぎません。必ずしもアプリケーションの効率やスケーラビリティが向上するとは限りません。ただし、出力キャッシュを使用すると、ページやリソースがダウンストリームにキャッシュされるので、サーバーの負荷が確実に軽減されます。
また、出力キャッシュは匿名コンテンツに対して有効な唯一のオプションです。キャッシュされたコンテンツに対する要求は直接 IIS によって提供され、呼び出し側が認証される可能性がある ASP.NET パイプラインに送られることはありません。
最後に、ポストバックするページおよびユーザー コントロールには追加作業が必要です。具体的には、1 つ以上のパラメータに応じて異なる出力の複数のコピーを保存するよう、ASP.NET に指示する必要があります。それには、@OutputCache ディレクティブの VaryByParam 属性を使用します。
実践上の問題
貧者の AJAX という認識が高い部分レンダリングは、"純粋な" AJAX アプリケーションの作成に十分なエネルギーを注げない開発者のために用意されています。今のところ、AJAX は ASP.NET を使用している開発者にとって最適なオプションの 1 つですが、最終的には、AJAX には多くのトレードオフがあり、有効な AJAX はさまざまな方法やパターンの組み合わせによって得られるものであることを覚えておいてください。