MSDN マガジン > Home > 発行物 > 2008 > October >  ASP.NET AJAX 4.0: データ駆動型 Web アプリケーションに対する新しい AJ...
ASP.NET AJAX 4.0
データ駆動型 Web アプリケーションに対する新しい AJAX サポート
Bertrand Le Roy

コードのダウンロード : MSDN Code Gallery (188 KB)
オンラインでのコードの参照

この記事は、ASP.NET のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • サーバー側のデータ操作
  • UpdatePanel とクライアント
  • ポストバックとペイロードの低減
  • クライアント側のテンプレート レンダリング
この記事では、次のテクノロジを使用しています。
ASP.NET AJAX 4.0
AJAX は、さまざまな点で魅力的な Web プラットフォームです。AJAX により、従来はサーバーで実行されていた多くのタスクがブラウザで実行されるようになることで、サーバーへのラウンド トリップ回数が少なくなり、消費帯域幅が減少し、Web UI の速度や応答性が向上します。これらの利点は作業負荷の多くをクライアントに移した結果として得られるものですが、それでもやはり、サーバー アプリケーションの力と柔軟性を自在に活用することを求める多くの開発者は、ブラウザを実行環境としては選択しません。
これまでは、ソリューションとして UpdatePanel コントロールが使用されてきました。それにより開発者は、すべてのサーバー ツールを維持しながら AJAX アプリケーションを開発できました。しかし UpdatePanel は、従来のポストバック モデルから多くの重みを引きずっており、UpdatePanel 要求は依然として完全なポストバックです。実際、UpdatePanel を使用すると、(ViewState を含む) フォーム全体がサーバーにポストされ、ページのライフサイクルのほぼ全体がそこで実行され、レンダリングはサーバーで行われます。このような方法では明らかに、AJAX に移行する主な理由の 1 つが相殺されてしまいます。ここで唯一の真のメリットは、通常の HTTP POST 要求の代わりに XmlHttpRequest が使用され、ページの更新部分と ViewState だけがクライアントに返されることです。したがって、応答のサイズはずっと小さくなりますが、要求はそうではありません。
純粋な AJAX アプローチは、ほとんどの状況で UpdatePanel アプローチよりも優れています。純粋な AJAX のソリューションでは、レンダリングがクライアント上で行われ、サーバーはデータだけを返します。これは同等な HTML よりも通常はかなり小さなデータとなります。また、このアプローチでは、ネットワーク要求の数を大幅に減らすことができます。クライアント上にデータを保持することで、アプリケーションの UI ロジックの多くをブラウザで実行できます。
ただし、純粋な AJAX アプローチの主な問題は、データを HTML に変換するツールがブラウザに存在しないことです。そのためにすぐに利用できるのは、2 つの大まかな機能である、要素のコンテンツ全体を指定した HTML 文字列で置き換える innerHTML と、それよりやや低速な機能として、タグおよび属性に対して動作するドキュメント オブジェクト モデル (DOM) API (抽象レベルに関して HtmlTextWriter と同様なもの) だけです。
この記事では、同じ 1 つのページを、古典的なポストバック、UpdatePanel、および純粋な AJAX の 3 つの方法でそれぞれ記述したものを示し、サーバーで使用される手法が状況によってはクライアントで実行した方が優れていることを説明します。最初の 2 つの例は現在、一般に提供されている ASP.NET 3.5 SP1 を使用して構築できますが、3 つ目の例は ASP.NET 4.0 の新しいクライアント機能のいくつかを使用します。

ポストバック ベースのマスタ/詳細ページ
ここで構築するページは、製品のリストを表示し、ユーザーが製品を選択するとその製品の詳細説明をリストの右側のパネルに表示します。ここで使用する AdventureWorks サンプル データベースは、go.microsoft.com/fwlink/?LinkId=124953 からダウンロードできます。データ層はこの記事の中心的な話題ではないので、LINQ to SQL を使用して基本的なデータ層だけを作成します。
最初に、アプリケーションの App_Data フォルダに AdventureWorks.mdf ファイルを追加します。次に、単純に新しい "LINQ to SQL classes" .dbml ファイルを追加し、サーバー エクスプローラから Product、ProductPhoto、ProductProductPhoto の各テーブルと vProductModelCatalogDescription ビューをデザイン画面にドロップします。結果のデータ層を図 1 に示します。
図 1 データ層のアーキテクチャ (クリックすると拡大画像が表示されます)
ページは 2 つのビュー ウィンドウで構成され、1 つは製品のリスト、もう 1 つは製品の詳細を示します。図 2 に、レンダリングされたページを示します。ページに対する最初の要求では、次のコードを使用して製品リストがデータにバインドされます。
private void BindProductList() {
    ProductList.DataSource = from p in AdventureWorksContext.Products
                             where p.ProductSubcategoryID == 1 
                             //Mountain bikes
                             orderby p.Name
                             select p;
    ProductList.DataBind();
}
図 2 自転車のリストと詳細 (クリックすると拡大画像が表示されます)
このリスト自体とそのテンプレートを図 3 に示します。データベースに対してクエリを実行するコードは、ダウンロード可能なプロジェクト内に含まれており、その内容はかなり単純です。データベースに対して Mountain Bike カテゴリの製品を取得するクエリを実行し、その結果に対して ListView コントロールをバインドします。もちろん、データ ソース コントロールを使って同じ処理を行うこともできますが、コードによるアプローチの方が柔軟で予測可能です。デザイン ビューを重視する開発者であれば、違った結果になるかもしれません。
データセットから HTML マークアップを作成する実際の作業はすべて ListView コントロールで処理されるため、非常に便利で手間がかかりません。必要なのは、その HTML のテンプレートを LayoutTemplate および ItemTemplate プロパティに指定することだけです (図 3 を参照)。このテンプレートは、製品のリストを記号付きリスト (UL および LI タグ) 内のリンクとしてレンダリングします。
<asp:ListView ID="ProductList" runat="server"
    DataKeyNames="ProductId"
    OnSelectedIndexChanging="ProductList_SelectedIndexChanging"
    OnSelectedIndexChanged="ProductList_SelectedIndexChanged">
    <LayoutTemplate>
        <ul ID="itemPlaceholderContainer" runat="server">
            <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
        </ul>
    </LayoutTemplate>
    <ItemTemplate>
        <li><asp:LinkButton runat="server" ID="Select" CommandName="Select" 
                                         Text='<%# Eval("Name") %>' /></li>
    </ItemTemplate>
</asp:ListView>
これらのリンク自体は通常のリンクでなく LinkButton コントロールであることに注意してください。これは、別のページに移動する代わりにページにポストバックすることを意味します。これらは実質的に、ボタンの機能を持つリンクです。もちろん、これらの LinkButton コントロールを適切なスタイルの通常のボタン コントロールで置き換えれば、JavaScript なしでページのアクセシビリティを簡単に改善できます。
ここで使用している LinkButton の主な特徴は、各ボタンにイベント ハンドラをアタッチする代わりに、CommandName プロパティを "Select" に設定していることです。その結果、ボタンがクリックされると、それを理解するコントロールによって処理されるまで、コマンドがコントロール ツリーを上にたどっていきます。これは非常に強力な機能であり、任意の UI 要素がその親コントロールにコマンドを送信する際に、親コントロールから期待されるコマンドと引数以外、多くの情報を知っている必要はありません。その結果、ListView などの強力なデータ コントロールを使用して、開発者がマークアップを完全に制御できます。後でこのページの純粋な AJAX バージョンを構築するときに、これが同様なブラウザ概念にどのように変換されるのかを示します。
この段階では、何もコードを記述する必要なしに、選択をサポートする製品リストが得られています。DataSource コントロールと ControlParameters を使用して、リスト内の選択されたデータ キーを詳細ビュー内の選択されたデータ キーに結び付ければ、引き続きコードなしで進むこともできますが、ここではそれをコードで行うことにします。次は、リストの SelectedIndexChanged イベントを処理し、関連する製品 ID を使用して BindProductDetails メソッドを呼び出します。
protected void ProductList_SelectedIndexChanged(object sender, 
                                                 EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    BindProductDetails(productId);
}
BindProductDetails は、データベースに対して製品の情報および写真のクエリを実行した後、それらを詳細ビュー内の対応するコントロールにバインドします。
写真は、データベースからクエリでイメージ バイトを取得してそれを応答のバイナリ ストリームにコピーする、単純なハンドラによって処理されます (図 4 を参照)。このハンドラは、ページの 3 つのバージョンのそれぞれで使用されます。これで、命令型と宣言型の混在したサーバー コードで全体が記述された、製品のデータ駆動型マスタ/明細ビューができ上がりましたが、まだいくつか改良できる点があります。
public void ProcessRequest (HttpContext context) {
    int id;
    if (int.TryParse(context.Request.QueryString["id"], out id)) {
        context.Response.ContentType = "image/gif";
        AdventureWorksDataContext dc = new AdventureWorksDataContext();
        var bytes = dc.ProductPhotos
            .Where(p => p.ProductPhotoID == id)
            .Single().LargePhoto;
        context.Response.OutputStream.Write(bytes.ToArray(), 0, bytes.Length);
    }
    else {
        throw new HttpException(404, "Image not found");
    }                
}
これは、状態の大きさに関してステートフルであるという点で、非常に典型的な Web フォーム ページです。ブラウザにレンダリングされたソースをざっとチェックすると、異なる各ビューがその内部状態をすべて覚えており、ポストバックごとにそれを保持していることから、ViewState が約 4 KB であることがわかります。
同時に、ページはその現在の状態に関して手掛かりを提供しません。ページ上で何を行っても、ブラウザのナビゲーション バーに表示される URL は "1_WebForm.aspx" のままです。ユーザーがページをブックマークすると、常に、製品詳細のないページが最初に表示されます。
この単純な例では、製品リスト内のリンクを LinkButton コントロールから通常のリンクに変更し、選択ポストバックを詳細ページへの単純なナビゲーションで置き換えることにより、上記の問題は修正できます (ViewState をオフにして、ラウンドトリップごとに 2 回、約 4 KB ずつ節約することも可能です)。最近の ASP.NET Model View Controller (MVC) ライブラリ (msdn.microsoft.com/magazine/cc337884 を参照) についてご存知の方は、これが MVC アプローチを活用できる典型的なケースの 1 つであることにお気付きではないでしょうか。
また、ナビゲーション ベースのアプローチを用いても、サイトの検索機能を大きく向上させることができます (これだけで、まったく別の記事のテーマとなる価値があります)。しかし、一般的なデータ駆動型アプリケーションは、この単純な例よりもずっと複雑であり、単純なナビゲーションでは UI フローの構築に適切な方法とは言えません。したがって、この単純なアプリケーションについても、ポストバックと Web フォームの概念がどのように AJAX に変換され、それによってどのように改善されるのかを詳しく示していきます。
ポストバックもリンク ナビゲーションも、思った以上にユーザー エクスペリエンスに悪影響を与える可能性があります。ポストバックおよびナビゲーション中には、UI が無効になり、サーバーが新しいコンテンツで応答するまでの間、他のユーザー操作は不可能になります。新しいコンテンツはドキュメント全体を置き換えてレンダリングされる必要があり、スクロール位置などの細かい状態が失われる場合があります。
もう 1 つの問題は、ユーザーが [戻る] ボタンや履歴の動作について非常に具体的な期待を抱いている点です。ポストバック モデルでは、残念ながら、何が履歴に記録されるか、またはユーザーが [戻る]、[進む]、[更新] を押したときに何が起こるかについては、ほとんどまたはまったく制御できません。理想的には、状態の変化に何が含まれ、何が履歴に残るのかは開発者が管理したいところですが、ポストバック アプリケーションでは、ユーザーとのほとんどすべてのやり取りでブラウザの履歴にエントリが作成されます。

UpdatePanel バージョン
このページを改良する簡単な方法の 1 つは、UpdatePanel を使用することです。UpdatePanel を使用すると、ユーザーの動作によりポストバックが生じるような場面で変更される部分を、ページ内で区切ることができます。今見ている非常に単純な例では、ページ内で更新する領域は詳細ビューです。UpdatePanel の部分更新を有効にするには、ページのフォーム タグのすぐ後に次に示すような ScriptManager コントロールを追加する必要があります。
<asp:ScriptManager ID="ScriptManager1" runat="server"/>
また、詳細ビューの周囲に、UpdatePanel 自体を追加する必要があります。
<asp:UpdatePanel ID="UpdatePanel1" runat="server" RenderMode="Inline">
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="ProductList" 
            EventName="SelectedIndexChanged" />
    </Triggers>
    <ContentTemplate>
        <div class="float" id="productDetails">
            <fieldset>
            ...
            </fieldset>
        </div>
    </ContentTemplate>
</asp:UpdatePanel>
この UpdatePanel には、製品リストの SelectedIndexChanged イベントを監視するトリガがあることに注意してください。これは、部分更新をトリガする可能性のあるコントロールがすべて UpdatePanel の内部にある場合には必要ありませんが、ここでは、製品リストは常に UpdatePanel の外部に保持する必要があります。SelectedIndexChanged イベントの発生時にリストのレンダリングを更新する必要はないからです。このトリガがないと、部分更新の代わりに通常のポストバックが発生します。また、UpdatePanel を使用するときには、部分更新をトリガする可能性のあるすべてのポストバック コントロールに常に ID を付けることを忘れないでください。そうしないと、ページが特にはっきりした理由もなく通常のポストバックを使用する場合があります。
AJAX の外観を持つ古典的なポストバック Web フォームの変換についての説明は、これで終わりです。しかし、実際にはまだ必要な機能がすべて揃ったようには見えません。1 つの問題は、[戻る] ボタンに対する小さなサポートを失ったことです。ユーザーが何台かの自転車をブラウズした後で [戻る] ボタンを押すと、このサイトに来る前に見ていたサイトに戻ってしまいます。それに気付いたユーザーが [進む] ボタンを押すと、今度はアプリケーションの既定の状態 (ここでは、何も選択されていない状態の製品リスト) に戻ります。
さいわい、ASP.NET 3.5 SP1 では、ページに [戻る] ボタンのサポートを再度付け加えるための簡単な方法が用意されています。ScriptManager には非常に便利な EnableHistory プロパティ、AddHistoryPoint メソッド、および Navigate イベントがあり、それらを組み合わせることでアプリケーション開発者は、通常のポストバックで可能な範囲よりもずっと細かくブラウザの履歴を制御できます。この機能は、UpdatePanel の使用によって失われたものを取り戻すだけでなく、それをはるかに強力な形で再現します。
通常のポストバックとの大きな違いは、アプリケーションの状態の変化を構成する内容を正確に把握し、重要性が低いと判断したユーザーとのやり取りをフィルタリングできることです。また、"ブックマーク機能" も得られ、ブラウザの履歴ドロップダウンにわかりやすく意味のあるエントリを表示できるようになります。
ページに履歴管理を追加するには、ブックマークを使用する際にユーザーがどのような情報の保持を期待しているかを判断する必要があります。ここで、状態に含める必要のある関連情報は、1 つしかありません。現在選択されている製品の ID です。
その状態を変更しようとするイベントがある場合は、それを遮断する必要があります。実際、処理する必要のある唯一のイベントは、前にトリガとして使用したものと同じです。それは、製品リストの SelectedItemChanged イベントです。このイベントは既に詳細ビューの再バインドのために処理しているため、このイベントが発生するたびに新しい履歴ポイントを作成するようなコードを追加します。
protected void ProductList_SelectedIndexChanged(object sender, 
                                                       EventArgs e) {
    var productId = (int)ProductList.SelectedDataKey.Value;
    var product = BindProductDetails(productId);
    if (ScriptManager1.IsInAsyncPostBack 
                                   && !ScriptManager1.IsNavigating) {
         ScriptManager1.AddHistoryPoint("product", 
           productId.ToString(), "AdventureWorks - " + product.Name);
    }
}
このイベントが、ユーザーが [戻る] ボタンで前のアプリケーション状態に戻った結果として発生したのではないことを確認するために、コードでは、要求が非同期ポストバックの一部であり、ナビゲーション操作の一部ではないことを確認します。このチェックを行わない場合、新しい履歴ポイントが作成され、ブラウザに存在する先の履歴を上書きしてしまいます。
このチェックが完了すると、AddHistoryPoint メソッドを安全に呼び出せるようになります。このメソッドには、状態の中で注目する唯一の情報である製品 ID を、"product" というパラメータ名で渡します。この名前は、後で示すとおり、変更された URL 内で使用されます。この値自体は、文字列に変換される必要があります。履歴状態は、クエリ文字列の 1 つの形式であると考えてください。メソッドに与える最後の情報は、ドキュメント タイトルです。履歴のナビゲーション ドロップダウンには、アプリケーションのナビゲーションに役立つ意味のある情報が表示されるため、これはユーザー エクスペリエンスを向上させる良い機会となります (図 5 を参照)。
図 5 履歴ドロップダウン (クリックすると拡大画像が表示されます)
この状態は、ブラウザ URL のハッシュによって保持されます (もともとはドキュメント内ナビゲーションのためにデザインされた、# 記号に続く部分)。これを記憶媒体として使用する理由は、ページおよびその JavaScript 状態と DOM 状態から実際に離れることなく履歴エントリを追加できる唯一の手段だからです (ブラウザでは URL が変更された場合以外は別の履歴項目を追加できません)。これには URL に状態を格納するという制約があり、使用できるスペースが限られます。一部のブラウザでは、1 KB を超える URL は拒否される場合があります。それより大きな URL が必要になる場合は、状態として関連する情報を適切に選択していない可能性があるので、情報をリファクタリングする必要があります。
ここで、製品の完全な名前ではなく、比較的短いデータである製品の ID を使用している点に注意してください。製品名はわかりやすいですが、通常は ID よりも長くなります。また、実際に URL のスペースが不十分である場合、そのデザインに対しては AJAX と履歴よりも通常の Web フォームと ViewState の方が適切である可能性があります。
問題の後半は、履歴をナビゲートしたときに、保存した状態を復元する必要があることです。これは、ScriptManager で Navigate イベントを処理することにより行います (図 6 を参照)。コードでは最初に、状態のない場合を処理します。これは、GET 要求にまでさかのぼったときに、ページの既定の状態に戻るようにするためです。状態が実際には新しいポストバックの実行によって復元されることを知らないと、これは多少普通ではないように見えるかもしれません。この場合、ポストバックの "前の" 状態、つまりフレームワークによって自動的に復元される状態は、ブラウザの履歴では時系列的に "後の" 状態であるため、その復元された状態を消去して、既定の状態で置き換える必要があります。
protected void ScriptManager_Navigate(object sender, HistoryEventArgs e) {
    var productIdString = e.State["product"];
    if (productIdString == null) {
        ProductList.SelectedIndex = -1;
        ProductDetails.DataSource = null;
        ProductDetails.DataBind();
        ProductModelDetails.DataSource = null;
        ProductModelDetails.DataBind();
        ProductPhotoList.DataSource = null;
        ProductPhotoList.DataBind();
        Page.Title = "AdventureWorks";
    }
    else {
        var productId = int.Parse(productIdString);
        var product = BindProductDetails(productId);
        ProductList.SelectedIndex = (
            from p in AdventureWorksContext.Products
            where p.ProductSubcategoryID == 1 // Mountain bikes
            orderby p.Name
            select p).ToList().IndexOf(product);
        BindProductList();
        Page.Title = "AdventureWorks - " + product.Name;
    }
}
次に、状態それ自体を、ユーザーから提供されたデータと考えて検証する必要があり、これは状態を整数として解析することで行っています。そして最後に、リストおよび詳細の状態をリセットするのに加えて、状態の復元時にページのタイトルを復元します。
これらの変更の後でページを使用すると、ブラウザ内の URL は製品が選択されるたびに変更され、次のようなものになります。
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&5YLQHC81D2
OEdJU/9ZBdHUip1qx3ooPKDhCLgKogupQ=
これは醜く、とても読みにくいと思われませんか。それは、フレームワークが既定で、ユーザーから提供されたデータは危険であると判断し、改ざんを防ぐために状態をハッシュするからです。ただし、開発者は多くの場合、コードが状態を有効と見なしてユーザーがそれを改ざんできる可能性があるとしても、比較的読みやすく、それほど醜くない URL を好みます。状況によっては、URL の改ざんがむしろプラスであるとさえ考えられています (MSDN ライブラリがその良い例です。ナビゲーションをずっと簡単にするために、予測可能な方法でユーザーが msdn.microsoft.com/library/system.web.ui.scriptmanager.aspx のような独自の URL を構築できるようになっています)。
このようなシナリオを可能にするために、ScriptManager は EnableSecureHistoryState というブール型プロパティを公開しています。これを false に設定するだけで、URL は次のようにずっとわかりやすくなります。
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&product=776
その結果、単に Web フォームの AJAX バージョンに見えるように作られただけではなく、ブックマーク機能や [戻る] ボタンの最適な処理といった多くの便利な追加機能を備えた、より滑らかなページが得られます。そしてそのすべてが、1 行の JavaScript も書かずに達成されたのです。

純粋な AJAX バージョン
このページの UpdatePanel バージョンには良いところがたくさんありますが、まだ ViewState の重みを引きずっています。それを削ぎ落とすために、クライアント側にさらに多くのロジックを移動する必要があります。そのために、おもしろい JavaScript をいくらか記述することが必要になります。
ASP.NET AJAX 3.5 SP1 を使用して純粋な AJAX バージョンを書くこともできますが、データを取り出して HTML 形式に変換するのに手間がかかります。データを HTML に変換するには 2 つの基本的な方法があります。
1 つは、ほとんどのクライアント テンプレート エンジンで行われるように、静的テンプレート コンテンツと動的データ コンテンツの文字列を交互に連結する方法です。この方法は、innerHTML だけを使用して DOM とやり取りするため、比較的単純で高速に見えます。しかし、いくつかの問題点があります。
問題の 1 つは、インジェクション攻撃からの保護です。文字列を連結して HTML を生成する場合には、すべてのデータを使用前にエンコードする必要があります。そうしないと、属性に引用符を含めたり、テキスト ノードにスクリプト タグを含めたりした場合に、それが悪意によるかミスによるかにかかわらず、(セキュリティ上問題な) 任意のコード実行につながる可能性があります。テキスト属性、URL 属性、テキスト ノードのどれを挿入するのかに応じて異なるアルゴリズムが必要になる可能性があるため、エンコードは見かけよりも難しくなります。
また、テンプレート エンジンには式言語が必要です。プレーンなデータ フィールドを変更なしに挿入するのは簡単ですが、それは最も単純なシナリオに過ぎません。多くの場合は、書式文字列を適用したり、複数のフィールドを結合したり、より一般的に言えば、データを表示する前にデータに何らかの操作を加える必要があります。これはデータをテンプレートに送る前に変換を行うことでも実行できますが、その機能をテンプレート エンジンに組み込んでおけばより簡単で効率的です。実際、書式設定などの機能を追加し始めると、完全な式言語の柔軟性が必要であることがすぐにわかります。
ASP の <% %> ブロックで行うように、コードとマークアップを混在させることができれば、HTML フラグメントの周りにループを使用してマークアップを繰り返したり、単純な if ステートメントを使用した条件レンダリングを行うなど、興味深いシナリオを実現できます。そのような場合でも、真に有用なものとするためには完全な言語が必要となります。
最後に、HTML はまだ物語の半分に過ぎません。実際、AJAX アプリケーションとは、単なる DOM へのクライアント側の更新ではなく、アクティブ コンテンツに関するものです。HTML の生成が完了した後も、イベントを要素にフックしてコントロールと動作をアタッチする必要があります。それは HTML 生成後にコードで実行することもできますが、その場合 HTML とロジックの間にいびつな非対称が生じます。HTML が非常に簡単になる一方で、ロジックはより難しくなり、テンプレートの構造に関する知識が要求されます。
言い換えれば、動作をアタッチするには、それをどこにアタッチするかを知る必要があり、それはつまり、HTML テンプレート マークアップの構造に変更を加えた場合、それをアクティブ化するコードにも変更が必要になることを意味します。そのような関連付けを緩和する方法もありますが、それよりも良い解決策は、コンテンツのアクティブ化をテンプレート エンジンの一部に組み込むことです。
データから HTML を生成するもう 1 つの方法は、DOM API を直接操作して、コードから要素、属性、およびテキスト ノードを作成することです。これは一見、いくつかの理由で不適切なオプションに見えます。もちろん、標準を信奉する人々に対して同調できるという利点はありますが、ちょっとした不可思議な理由によって innerHTML よりもずっと低速です。しかし、この方法が一般に使用されない第 1 の理由は、DOM API は表現力がそれほど高くなく、結果のコードが読みにくいうえに、(少なくとも何らかの支援や追加の抽象なしには) 保守が困難であることです。優れた抽象化によってプロセス全体をずっとおもしろいものにする jQuery などのツールキットもありますが、そのようなツールを使用しても、やはり必要以上に困難な作業となります (jQuery にもいくつかのテンプレート プラグインがあるのはそのためです)。
読者の中には、マイクロソフトが既に ASP.NET AJAX Futures にテンプレート エンジンを組み込んでいたことをご存知の方がいるかもしれませんが、これはあまりに遅く、デザインが複雑であったため、大きく改善する必要性を感じていました。この失敗した最初の試みに関して良かったことは、新しいバージョンで何を改善すればよいか (つまり、遅さと複雑さ) について多くを学んだことです。
開発チームでは ASP.NET AJAX の新しいテンプレート エンジンに対して、文字列の連結から完全な DOM 操作まで多くの異なるデザインをテストし、それらのパフォーマンス、簡潔さ、および柔軟性を評価しました。また、どのようなシナリオが不可能になるかという観点からもそれらを比較しました。理想的なソリューションはありませんが、より良い妥協点となるものを選択しました。
新しいエンジンの原理はシンプルです。HTML、データ フィールド、式、宣言型コンポーネントのインスタンス化、および命令型コードを含むテンプレート コードを、等価な HTML を作成する JavaScript へと "魔法のように" 自動的に変換します。これは十分シンプルに見えますし、(ブラウザに奇妙な動作が現れない限りは) 実際にそうです。DOM API を使用することでパフォーマンス上のペナルティはありますが、十分注意して要素を DOM の外部に構築し、できるだけ少ない要素をできるだけ後で追加するようにすれば、パフォーマンスへの影響はそれほど大きくなく、結果として得られる驚くべき柔軟性はそのトレードオフに十分に見合うものです。文字列連結アプローチの問題はすべて自然に解消されるように感じられます。
インジェクション攻撃については、自動的に保護されます。テキスト ノードの作成と属性値の設定にはコードを使用しているため、エンコードを行う必要はありません。使用している API が既に安全であるからです。これは、文字列の連結による SQL の構築と、SQL パラメータの使用とを比べた場合に似ています。健全な知性の持ち主であれば、もはやそのような連結は行いません。したがって、ここで同じような危険を冒す必要もありません。
では、新しい式言語は必要でしょうか。それは既にあります。JavaScript です。テンプレート マークアップを JavaScript コードに変換する場合、生成するコードに JavaScript の式を挿入するのが最も簡単です。
最も一般的なテンプレート開発タスクである、1 回、1 方向のデータ フィールド挿入 (サーバーでは "<%= expression %>"、このシステムでは "{{ expression }}" と表記) を実現するために、しばしば敬遠されている JavaScript の機能、"with" キーワードを使用します。これにより、テンプレート インスタンスに関連付けられたデータ項目のフィールドを挿入する際に "{{ dataItem.myField }}" のような式に頼る必要がなくなります。"with" キーワードのおかげで、テンプレートに対して生成されたコードを "with(dataItem) {…}" のようなもので囲むことができるため、データ項目の任意のメンバがテンプレート関数の最上位スコープに昇格され、式の挿入が "{{ myField }}" のように単純になります。
テンプレートに動作を挿入するには、2 つの方法があります。1 つは、$attachEvent および $create コードを itemCreated イベントから記述するか、または、特別な $element 変数を使用してテンプレートにインラインで記述する方法です。この変数は、テンプレート内から利用でき、最後に作成された要素を参照します。または、ここで示す宣言型構文を使用します。たとえば、オートコンプリートおよび透かしの動作を入力タグに追加する場合は、次のように記述します。
<body xmlns:sys="javascript:Sys"
 xmlns:autocomplete="javascript:AjaxControlToolkit.AutoCompleteBehavior"
 xmlns:watermark="javascript:AjaxControlToolkit.extBoxWatermarkBehavior">
...
<input id="search" sys:attach="autocomplete,watermark"
 autocomplete:servicepath="SearchAutoComplete.asmx"
 watermark:watermarktext="Type your search terms here" />
ここでは、xmlns XHTML 名前空間宣言を使用して、HTML または body タグ上の (またはテンプレートの親タグ上の) 各宣言型動作に対してプレフィックスを登録します。これにより、XHTML マークアップを標準的な方法で拡張でき、これはサーバー コードに対する @Register ディレクティブに似ています。"xmlns:" に続く部分が、各動作またはコントロールに関連付けられるプレフィックスです。名前空間の URL は、"javascript:" プロトコルを使用して、プレフィックスを特定の JavaScript 型にマッピングします。"sys" 名前空間は特別なシステム名前空間であり、AJAX のルート名前空間である Sys 名前空間にマッピングする必要があります。
インスタンス化それ自体は特別な属性 sys:attach によって行われます。この属性の値は、インスタンス化して要素にアタッチする各動作またはコントロールのプレフィックスを並べたコンマ区切りリストです。これにより、すべてが名前空間によって適切に区別されているため、通常の HTML 属性や同じ要素の他の動作と競合するリスクなしに、これらすべての動作に対してプロパティを設定できます。
このエンジンの最も洗練された特徴の 1 つは、JavaScript コードへのテンプレートのコンパイルが実際のコンパイル手順と実によく似ていることです。つまり、コンパイルはテンプレートごとに 1 回しか行う必要がなく、いくつかのタスクをテンプレートがインスタンス化されるたびに実行する代わりに、前もって実行しておく機会が提供されます。しかし、理論の話はそれで十分です。これは、マスタ/詳細ページにどのように適用されるでしょうか。

AJAX バージョンのテンプレート
製品のリストに対するテンプレートは非常にシンプルです。
<ul id="productListTemplate" class="sys-template">
    <li>
        <a href="{{ String.format('3_Client.aspx?product={0}',
        ProductID) }}">{{ Name }}</a>
    </li>
</ul>
項目テンプレートは、単純なリンクを含むリスト項目です。リンクのテキストは単に製品の名前 ("{{ Name }}") であり、href 属性は、製品 ID からプレーンな JavaScript を使用して構築された、書式設定された文字列です。
"{{ String.format('5_Client.aspx?product={0}', ProductID) }}"
クラス "sys-template" は、ページの最初のレンダリングからテンプレートを隠すように CSS で定義されています。この単純なテンプレートのコンパイル済みコードを図 7 に示します。詳細ビューはもう少し複雑で、実際にはいくつかのインライン コードを含んでいます (図 8 を参照)。写真のリストをレンダリングするために入れ子になったテンプレートを使用することもできましたが、1 つの写真のマークアップに対して通常のループを使用する方がよりシンプルです。動的に変更されるデータを扱っていて、変更をマークアップに自動反映する場合 (これはサポートされているシナリオですが、この記事の範囲外です) であれば、入れ子になったテンプレートの使用も妥当かもしれませんが、ここでは 1 方向の 1 回のバインドを扱っているため、インライン コードが適切です。
function(__containerElement, $dataItem, $parentContext, __instanceId) {
   var __context = {}, $component, __app = Sys.Application, 
      __creatingComponents = __app.get_isCreatingComponents(), 
      __components = [], __componentIndex, __e, __f, __topElements = [],
      __p = [__containerElement], $index = __instanceId, 
      $id = Sys.Preview.UI.Template._getIdFunction(__instanceId), 
      $element = __containerElement;
   Sys.Preview.UI.Template._contexts.push(__topElements);
   with(__context) { with($dataItem || {}) {
      $element=__p[1]=document.createElement('LI');
      __topElements.push($element);
      $element=__p[2]=document.createElement('A');
      $component = $element;
      __e = document.createAttribute('href');
      __e.nodeValue = String.format('5_Client.aspx?product={0}',
                                                       ProductID);
      $element.setAttributeNode(__e);
      __p[1].appendChild($element);
      __p[2].appendChild(document.createTextNode(Name));
      $element=__p[2];
      __p[1].appendChild(document.createTextNode(" "));
      $element=__p[1];
   } 
}
   for (var __i = 0, __l = __topElements.length; __i < __l; __i++) {
      __containerElement.appendChild(__topElements[__i]);
   }
Sys.Preview.UI.Template._contexts.pop();
 return new Sys.Preview.UI.TemplateResult(this, __containerElement, __topElements, __components);
}
<div class="sys-template" id="productDetailsTemplate">
    <fieldset>
        <legend>{{ Name }} ({{ ProductNumber }}) 
            {{ String.format("{0:C}", ListPrice) }}</legend>
        <ul class="photoList">
            <!--* for (var i = 0; i < Photos.length; i++) { *-->
            <li><img src="{{ String.format('productphoto.ashx?id={0}',
                Photos[i]) }}" /></li>
            <!--* } *-->
        </ul>
        <table>
            <tr><td class="label">Summary:</td><td>{{ Summary }}</td></tr>
            <tr><td class="label">Experience:</td>
                <td>{{ RiderExperience }}</td></tr>
              ...
            <tr><td class="label">Style:</td><td>{{ Style }}</td></tr>
            <tr><td class="label">Wheel:</td><td>{{ Wheel }}</td></tr>
            <tr><td class="label">Maintenance:</td>
                <td>{{ MaintenanceDescription }}</td></tr>
        </table>
    </fieldset>
</div>
テンプレートは、最初のインスタンス化時には遅延コンパイルされますが、テンプレート マークアップの親要素をコンストラクタのパラメータとして使用して "new Sys.Preview.UI.Template" を作成することにより、準備が整います。テンプレートそれ自体は、サーバー上の Web サービスからデータを返すネットワーク呼び出しからのコールバックによって、インスタンス化されます。
AdventureWorks.GetProducts(1 /* Mountain bikes */,
  function(productArray) {
    renderProductList(productArray, productListTemplate);
    selectProduct(initialProductID, true);
});

function renderProductList(productArray) {
    var target = $get("productList");
    target.innerHTML = "";
    for (var i = 0, l = productArray.length; i < l; i++) {
        productListTemplate.createInstance(target, productArray[i]);
    }
}
これは ASP.NET 4.0 の製品版では不要になります。ASP.NET 4.0 では、DataView コンポーネントによってテンプレートの解析、コンパイル、およびインスタンス化が処理されます。このアプリケーションのコードのほとんどは最終的にはなくなりますが、見えないところでどのような動作が行われているかを示すためには有用です。また、テンプレートのレンダリングを必要とするコンポーネント開発者がその機能をどのように利用できるかも示されています。

イベント バブル
ユーザーが製品の 1 つをクリックしたときに適切な詳細ビューを表示するコードは、どこに置けばよいでしょうか。このコードでは、サーバー側でそれに相当するコードと同様に、イベント バブルを使用しているため、リスト内のすべてのリンクに対して 1 つのシングルクリック イベント ハンドラを記述することができました (それにより、新しいハンドラの作成や古いハンドラのクリーンアップについて気にすることなく、必要に応じてリストのリンクを追加または削除できました)。次のコードは、そのハンドラを示しています。リスト内のリンクに対するすべてのクリック イベントは、リスト自体へとバブルアップ (伝播) され、そこで処理されます。"e.target" は、実際にクリックされた要素への参照です。言い換えれば、これは href 属性から製品 ID を取得して関連する製品を選択できるようにするリンクです。
$addHandler($get("productList"), "click", function(e) {
    var href = e.target.href;
    selectProduct(parseInt(href.substring(href.indexOf('=') + 1), 10));
    e.preventDefault();
    e.stopPropagation();
});
それが実行されると、イベントの既定の動作 (リンクのナビゲーション) がキャンセルされ、イベントはそれ以上バブルアップできなくなります。これは、イベント オブジェクトに対して W3C 標準の stopPropagation および preventDefault メソッドを呼び出すことで行われます。これらは、Internet Explorer を含めたすべてのブラウザで、フレームワークによって使用可能となっています。

[戻る] ボタンの管理
サーバー側バージョンからまだ再現されていない唯一の機能が、履歴です。ScriptManager コントロールで履歴を有効にすると、前に使用したサーバー側 API と厳密に等価なクライアント側 API も有効になり、これらを同時に使用することもできます (それによってクライアント/サーバー状態の混在管理が可能になります)。
履歴ポイントの作成は、状態の変化 (ここでは、リスト内の製品のクリック) に対応したイベントから Sys.Application.addHistoryPoint を呼び出すことで行われます。
Sys.Application.addHistoryPoint({product: productDetails.ProductID}, 
    "AdventureWorks - " + productDetails.Name);
それに対応して、状態は Sys.Application の "navigate" イベントから復元されます。イベント ハンドラが受け取る HistoryEventArgs 引数には、1 つのプロパティがあります。それが状態であり、それによって復元する製品を取得できます。
Sys.Application.add_navigate(function(sender, e) {
    var ProductID = parseInt(e.get_state()["product"], 10);
    selectProduct(ProductID, true);
});

完成した製品
結果のページは、UpdatePanel バージョンと非常によく似た動作をしますが、ネットワーク トラフィックという点では比較になりません。製品が選択されると、UpdatePanel バージョンはサーバーに対して 4 KB 以上のデータを送信して約 8 KB を受信します。一方、純粋な AJAX バージョンは、"{"productId":771}" と標準の HTTP ヘッダーだけを送信して、2 KB の純粋な JavaScript Object Notation (JSON) データを受信します。つまり、ユーザーが製品をクリックするごとに、約 10 KB 分の帯域幅が節約されています。
これは、ASP.NET 4.0 で計画されている魅力的な機能の 1 つに過ぎません。go.microsoft.com/fwlink/?LinkId=126987 で詳細をご確認ください。

Bertrand Le Roy 博士は、マイクロソフトで AJAX を担当するプログラム マネージャです。この領域での開発作業に 5 年間従事してきました。また、OpenAjax アライアンスでのマイクロソフトの代表でもあります。

Page view tracker