次の方法で共有


Cutting Edge

jQuery による先行フェッチと ASP.NET Ajax Library

Dino Esposito

先月のコラムでは、ASP.NET Ajax Library で提供される新機能を使用してマスター/詳細ビューを実装する方法について説明しました。使用した新機能には、DataView クライアント コントロールで具現化される、クライアント側でのライブ データ バインドの構文や豊富なレンダリング コンポーネントがあります。これらの機能を 1 つにまとめることで、入れ子になったビューを簡単に作成し、一対多のデータの関係を表すことができます。

ASP.NET Ajax Library でのマスター/詳細ビューのメカニズムは、主として、DataView コンポーネントのロジックで定義されます。このロジックでは、イベントを処理および公開します。

今月のコラムでは、さらに一歩進めて、ASP.NET Ajax Library の上位に、一般的によく使用される AJAX のデザイン パターンである "先行フェッチ" を実装する方法について説明します。基本的には、先月のコラムで使用したサンプル アプリケーション (顧客の詳細を表示する比較的標準のドリル ダウン ビュー) を拡張して、顧客に関連する注文があれば、それらを自動的かつ非同期にダウンロードして表示します。その過程で、jQuery のいくつかの機能について触れ、ASP.NET Ajax Library における新しい jQuery の統合 API について説明します。難しい話はこのくらいにして、以前のサンプル アプリケーションの状況を復習してから、最初バージョンのサンプル アプリケーションを作成しましょう。

拡張の対象とするデモ

図 1 に示すアプリケーション シナリオの上位に先行フェッチ機能を追加します。

image: The Initial Stage of the Sample Application

図 1 初期段階のサンプル アプリケーション

メニュー バーの頭文字を使って顧客をフィルター選択できます。頭文字を選択することで、顧客の人数が絞り込まれ、HTML の箇条書き機能を使用して短い一覧が表示されます。これがマスター ビューです。

レンダリングされる各項目を選択できるようにしています。ある顧客をクリックすると、隣接する詳細ビューにその顧客の詳細が表示されます。先月のコラムはここで終わりました。図 1 からおわかりのように、ユーザー インターフェイスには、注文を表示するためのボタンを表示するようになりました。今月はこの状態から先に進めます。

まず、アーキテクチャと、現在検討しているユースケースに関連する決定を行います。注文情報をどのように読み込むか、ダウンロード済みの情報には顧客情報が含まれているのか、注文内容は顧客情報に結び付いているのか、遅延読み込みのオプションを使用するのか、などです。

ここで検討しているコードはクライアント側での実行を想定しているため、Entity Framework や NHibernate といった一部のオブジェクト/リレーショナル マッピング (O/RM) ツールに組み込まれている遅延読み込み機能を使用することができません。注文の遅延読み込みを行うのであれば、そのコードをすべて記述することになります。それに対して、注文情報が既にクライアントで利用できる (つまり、注文情報が顧客情報と共に既にダウンロードされている) とすると、ほとんどすることはありません。後は、注文データを HTML テンプレートにバインドして、次の処理に進むだけです。

言うまでもなく、遅延読み込みを採用する方がはるかにおもしろそうです。そこで、このシナリオについて考えてみましょう。

ちなみに、AdoNetDataContext オブジェクト経由でデータを取得すれば、遅延読み込みが完全にサポートされることを知っておいてください (これについては、今後のコラムで取り上げます)。詳細については、asp.net/ajaxlibrary/Reference.Sys-Data-AdoNetServiceProxy-fetchDeferredProperty-Method.ashx (英語) を参照してください。

スクリプト ライブラリを読み込むための新しい方法

何年もの間、Web 開発者たちは、ページにどのスクリプト ファイルが必要になるかなどと考えることはありませんでした。それまで使用されていた限られた量のシンプルな JavaScript コードでは、必要なファイルが不足しているかどうかを実に簡単にチェックできたため、特に考えるほどの作業でもありませんでした。しかし、Web ページで使用される複雑な JavaScript コードの量が増えたことにより、スクリプトが異なるファイルに分割されるようになりました。その結果、分割されたファイルを正しく参照しないと、オブジェクトが定義されていないことを示すたちの悪い実行時エラーが発生するという問題が出てきました。

ここ数年、よく使用される多く JavaScript ライブラリが、こうしたファイル分割に関連する機能を提供するようになりました。たとえば、jQuery UI ライブラリでは、モジュール設計が導入され、ライブラリの中で実際に必要な部分だけをダウンロードしてリンクできます。ASP.NET Ajax Library を構成するスクリプトでも、これと同じ機能が提供されます。しかし、スクリプト ローダーを使えば、さらに多くの機能を利用できます。

スクリプト ローダーには、いくつか追加のサービスが用意されていて、大きなスクリプト ライブラリを複数の小さな要素にパーティション分割するという考え方を基に構築されています。必要なライブラリをローダーに指示するだけで、必要なファイルを適切な順序で読み込む作業をすべてローダーに委任できます。スクリプト ローダーは、必要なすべてのスクリプトを並列に読み込んでから、適切な順序で実行します。このようにして、ローダーは、オブジェクトが不足していることを示す例外が発生しないようにして、最も速い方法でスクリプトを処理します。必要なことは、希望するスクリプトを一覧することだけです。

ちょっと待ってください。必要なすべてのスクリプトを一覧しなければならないなら、ローダーを使用するメリットは何なのでしょう。いえ、アセンブリをプロジェクトにリンクするおなじみのプロセスに必要な作業に比べれば、ローダーに必要なこのような作業など取るに足らないことです。アセンブリ A をリンクする場合、Visual Studio 2008 のローダーが静的な依存関係をすべて調べることになります。以下に、スクリプト ローダーの操作方法を示すコード スニペットを示します。

Sys.require([Sys.components.dataView, Sys.scripts.jQuery]);

Sys.require メソッドは、ページにリンクするスクリプトへの参照を含む配列を受け取ります。このサンプルでは、ローダーに 2 つのスクリプト (dataView と jQuery) を処理するよう指示しています。

ただし、ご覧のように、Sys.require メソッドの呼び出しには、物理的な .js ファイルへの Web サーバー パスが含まれていません。それでは、パスはどこにあるのでしょう。

ASP.NET Ajax Library のローダーと連携するスクリプトは、ローダーに対してスクリプト自体を定義し、スクリプトの読み込を完了するタイミングをローダーに通知する必要があります。スクリプトをローダーに登録する際にローダーとのやり取りが生じることはなく、新しいスクリプトを管理するよう求めていることをローダーに知らせるだけです。図 2 は、jQuery と jQuery.Validate をローダーに登録する方法を、MicrosoftAjax.js から抜粋して示しています。

図 2 jQuery と jQuery.Validate をスクリプト ローダーに登録する

loader.defineScripts(null, [
     { name: "jQuery",
       releaseUrl: ajaxPath + "jquery/jquery-1.3.2.min.js",
       debugUrl: ajaxPath + "jquery/jquery-1.3.2.js",
       isLoaded: !!window.jQuery
     },
     { name: "jQueryValidate",
       releaseUrl: ajaxPath + 
                            "jquery.validate/1.5.5/jquery.validate.min.js",
       debugUrl: ajaxPath + "jquery.validate/1.5.5/jquery.validate.js",
       dependencies: ["jQuery"],
       isLoaded: !!(window.jQuery && jQuery.fn.validate)
     }
    ]);

もちろん、この手法はカスタム スクリプトやクライアント コントロールでも使用できます。その場合、実際のスクリプトに加えて、そのスクリプトのローダー固有の定義を参照する必要があります。ローダー固有の定義には、スクリプトのリリース バージョンとデバッグ バージョンのサーバー パス、スクリプトの参照に使用するパブリック名、ライブラリが正しく読み込まれたかどうかをテストするために評価される依存関係と式などがあります。

スクリプト ローダー コンポーネントを使用するために、start.js という新しい JavaScript ファイルを参照する必要があります。以下に、スクリプトを読み込む従来の技法と新しい技法を混在して使用するサンプル アプリケーションからの抜粋を示します。

<asp:ScriptManagerProxy runat="server" ID="ScriptManagerProxy1">
    <Scripts>
        <asp:ScriptReference Path="~/Scripts/Ajax40/Preview6/start.js"/>
        <asp:ScriptReference Name="MicrosoftAjax.js" 
                       Path="~/Scripts/MicrosoftAjax.js"/>
        <asp:ScriptReference Path=
                                 "~/Scripts/MicrosoftAjaxTemplates.js"/>
     <asp:ScriptReference Path="~/MasterDetail4.aspx.js"/>
   </Scripts>
</asp:ScriptManagerProxy>

従来の <script> 要素を使用して start.js ファイルを参照します。その他のスクリプトは、ScriptManager コントロール、単純な <script> 要素、または Sys.require メソッドを使用して参照できます。上記のコード スニペットからおわかりのように、jQuery ライブラリへの参照はありません。実際には、ScriptManager によってリンクされる、ページ固有の JavaScript ファイルのプログラムから jQuery ライブラリが参照されます。

ASP.NET Ajax Library に含まれるもう 1 つの興味深い機能は、Sys 名前空間を使用して jQuery の機能を使用できることと、逆に、マイクロソフトのクライアント コンポーネントを jQuery プラグインとして公開できることです。たとえば、次に示すように、Sys.onReady 関数を使用して、ready イベント用のイベント ハンドラーを登録できます (これは、一般的な jQuery のタスクです)。

Sys.onReady(
    function() {
        alert("Ready...");
    }
);

これらの新機能について考慮すると、Web ページの拡張機能として使用される JavaScript ファイルは、次のように開始するのが一般的です。

// Reference external JavaScript files

Sys.require([Sys.scripts.MicrosoftAjax, 

             Sys.scripts.Templates, 

             Sys.scripts.jQuery]);
Sys.onReady(
    function() {
        // Initialize scriptable elements 

        // of the page.
    }
);

ただし、もっと単純な手法も使用できます。Sys.require を使用して、DataView などのコントロールを実装するファイルではなく、コントロールそのものを読み込むことができます。スクリプト ローダーは、DataView に定義された依存関係を基に、必要なファイルを自動的に読み込みます。では、先行フェッチに話を移しましょう。

顧客の選択を処理する

図 1 に示すユーザー インターフェイスを用意するには、HTML テンプレートを使用し、DataView コンポーネントによってデータがバインドされるプレースホルダーにデータをアタッチします。一覧表示した顧客がクリックされると、DataView ベースのデータ バインドによって、その顧客の詳細が自動的に表示されます。ただし、注文は DataView によっては直接バインドされません。これはコラムの初めに決めた要件によるものです。設計上、注文は顧客情報と一緒にダウンロードされません。

したがって、注文をフェッチするには、DataView に関連付けられているテンプレート内での選択の変更を処理する必要があります。今のところ、DataView では、選択の変更時にイベントが発生しません。DataView では、マスター/詳細のシナリオが強力にサポートされ、カスタムのコマンドやハンドラーを作成できるにもかかわらず、サポートのほとんどが自動的に行われます。詳細については、asp.net/ajaxlibrary/Reference.Sys-UI-DataView-onCommand-Method.ashx (英語) を参照してください。具体的には、詳細ビューをトリガーできるクリック可能な要素の sys:command 属性を "select" に設定します (下図参照)。

<li>
   <span sys:command="Select" 
         id="itemCustomer" 

         class="normalitem">
   <span>{binding CompanyName}</span>
   <span>{binding Country}</span>
   </span>        
</li>

要素がクリックされると、DataView 内で onCommand イベントが発生します。その結果、selectedData プロパティの内容が更新され、選択内容が反映されます。続いて、selectedData にバインドされているテンプレートの一部が最新の情報に更新されます。ただし、データ バインドでは、コードを実行しなくても、表示データが更新されます。

前述のように、DataView 内でコマンドが起動されるときに、onCommand イベントが内部で発生しています。開発者は、このイベント用に独自のハンドラーを登録できます。残念ながら、少なくとも現時点のプレリリース バージョンの DataView コンポーネントでは、選択したインデックス プロパティが更新される前に、コマンド ハンドラーが呼び出されます。事実上、詳細ビューの表示前にインターセプトできますが、インデックスが更新されていないため、表示すべき新しいコンテンツがわかりません。このイベント目的は、いくつか重要な条件が検証されない状態で選択が変更されてしまうのを防ぐ方法を、開発者に提供することに限定されるように思われます。

現時点でも有効で、DataView コンポーネントが改善されなくても今後も継続的に機能する手法は、マスター ビューのクリック可能な要素に onclick ハンドラーをアタッチし、追加の属性をバインドして役に立つ重要な情報を含めることです。以下に、マスター ビューの反復可能な部分の新しいマークアップを示します。

<li>
   <span sys:command="Select" 

         sys:commandargument="{binding ID}"

         onclick="fetchOrders(this)"
         id="itemCustomer" 

         class="normalitem">
   <span>{binding CompanyName}</span>
   <span>{binding Country}</span>
   </span>        
</li>

このマークアップには、2 点の変更があります。1 つは、新しい sys:commandargument 属性が含まれるようになった点で、もう 1 つは、クリック イベント用のハンドラーが指定されている点です。sys:commandargument 属性には、現在選択されている顧客の ID が含まれます。ID は、データ バインドによって設定されます。ID を配置する属性は、必ずしも sys:commandargument でなければならないわけではありません。カスタム属性も使用できます。

クリック ハンドラーは、設定した読み込みのポリシーに基づいて、注文をフェッチします。図 3 に、注文を読み込むためのソース コードを示します。

図 3 注文をフェッチするコード

function fetchOrders(elem)
{
    // Set the customer ID
    var id = elem["commandargument"];
    currentCustomer = id;
    
    // Check the jQuery cache first
    var cachedInfo = $('#viewOfCustomers').data(id);
    if (typeof (cachedInfo) !== 'undefined') 
        return;

    // Download orders asynchronously
    $.ajax({
         type: "POST",
         url: "/mydataservice.asmx/FindOrders",
         data: "id=" + id,
         success: function(response) {

              var output = response.text;
              $('#viewOfCustomers').data(id, output);
              if (id == currentCustomer)
                  $("#listOfOrders0").html(output);
         }
    });
}

fetchOrders 関数は、クリックされた DOM 要素を受け取ります。まず、顧客 ID が含まれることになっている属性から値を取得します。次に、jQuery のクライアント キャッシュに注文が既に存在しているかどうかチェックします。注文が存在していなければ、最終的に、非同期ダウンロードを開始します。jQuery の AJAX メソッドを使用して、Web サービスへの POST 要求を作成します。この例では、Web サービスが AJAX パターンの "HTML メッセージ" を使用して、ページにマージできるプレーンな HTML を返すものと想定しています (これは必ずしも最適な手法というわけではありませんが、従来のシナリオでもほぼ機能します。純粋に設計上の観点から言うと、JSON データのエンドポイントに照会する方が、はるかに軽量なペイロードが生成されます)。

要求が成功すると、最初に注文のマークアップがキャッシュされてから、目的の場所に表示されます (図 4 参照)。

image: Fetching and Displaying Orders

図 4 注文のフェッチと表示

図 4 にはスクリーンショットのみが示されていて、何が行われているかはあまり明らかになっていません。顧客をクリックして選択し、ドリル ダウンするときに、注文の要求が非同期に発生します。それと同時にその顧客の詳細が表示されます。覚えておられると思いますが、要求に応じて顧客情報をダウンロードする必要はありません。顧客情報は、ユーザーが頭文字という大まかなメニューをクリックしたときにまとめてダウンロードされます。

注文のダウンロードにはしばらく時間がかかることがあります。この操作では、ユーザーへの通知は行われません (必要ありません)。ダウンロード操作は成り行きで行われ、ユーザーはまったく意識する必要がないためです。先行フェッチ パターンで肝心なのは、ユーザーが要求する可能性がある情報を前もってフェッチすることです。実際のメリットを表すために、この機能を非同期方式で実装する必要があります。また、ユーザビリティの観点から、ユーザーに意識させないようにすることが好ましいと考えます。

図 4 のユーザー インターフェイスで、ユーザーがごく自然に行う操作を中心に考えてみましょう。通常、ユーザーは顧客をクリックして選択します。次に、表示された情報を読むのにしばらく時間がかかるでしょう。ユーザーが画面を見ているときに、暗黙のうちに選択した顧客の注文がダウンロードされることになります。

すぐに注文を表示しようとするユーザーがいるかもしれませんし、しないかもしれません。別の顧客に切り替えてその顧客情報を読んでから、最初の顧客に再度切り替えるかもしれませんし、別の顧客に移動するかもしれません。どのような場合でも、顧客をクリックしてドリル ダウンするだけで、ユーザーは関連する注文のフェッチをトリガーしたことになります。

ダウンロードされた注文情報はどうなるのでしょう。ダウンロードした後、注文情報をどのように処理するのが望ましいでしょう。

フェッチした注文情報の処理

率直に言うと、このようなシナリオで事前に読み込んだたデータを処理するための、明確な推奨方法を見つけることはできませんでした。事前に読み込んだ情報をどのように扱うかは、主に、関係者やエンド ユーザーの入力によって決まります。

ただし、注文をダウンロードした顧客をユーザーが表示し続けている場合は、その注文を自動表示することをお勧めします。

$.ajax メソッドは非同期に機能し、成功時のコールバックがアタッチされます。このコールバックは、ダウンロードした特定顧客の注文を受け取りますが、コールバックが実行される時点では、表示されている顧客が異なっている場合があります。ここで使用するポリシーでは、ユーザーが現在の顧客を参照していれば、注文が表示されるようにします。それ以外の場合は、注文をキャッシュして、ユーザーが現在の顧客を再表示して [View orders] をクリックしたときに利用できるようにします。

フェッチ処理の成功時のコールバックを再度見てみましょう。

function(response) 
{
  // Store orders to the cache

  $('#viewOfCustomers').data(id, response.text);

  // If the current customer is the customer for which orders

  // have been fetched, update the user interface 

  if (id == currentCustomer)
     $("#listOfOrders0").html(response.text);
}

id 変数は $.ajax メソッドのローカル変数で、注文がフェッチされる顧客の ID が設定されます。これに対して、currentCustomer 変数はグローバル変数で、フェッチ処理が実行されている間いつでも設定されます (図 3 参照)。グローバル変数はさまざまな時点で更新される可能性があるため、コールバックでダウンロードの終わりにこの変数をチェックするようにします。

図 1図 4 に表示されている [View orders] には、どのような役割があるのでしょう。このボタンは、特定顧客の注文を表示したいと考えるユーザーのために用意しています。設計上、この例では注文の表示はオプションです。したがって、ビューをトリガーするボタンをユーザー インターフェイスに配置することにはそれなりの意味があります。

ユーザーが [View orders] をクリックした時点で、注文が読み込まれているかもしれませんし、まだ読み込まれていないかもしれません。読み込まれていなければ、設計上は、ダウンロードが保留されているか、なんらかの理由で失敗したことになります。そのため、ユーザーに 図 5 のようなユーザー インターフェイスを表示します。

image: Orders are Not Yet Available

図 5 注文が読み込まれていないことを示すメッセージ

ユーザーが同じページを表示し続けていれば、ダウンロードが正常に完了したときに注文が自動的に表示されます (図 4 参照)。

最初に表示される顧客

このマスター/詳細のシナリオを先行フェッチ機能で強化するデモを完成する作業が 1 つ残っています。DataView コンポーネントを使用すると、特定のデータ項目を、選択したモードで画面上にレンダリングするよう指定できます。DataView コンポーネントの initialselectedindex 属性によって、最初に選択される項目を制御することができます。この処理を実行するコードを次に示します。

<ul class="sys-template" sys:attach="dataview" id="masterView"
    dataview:dataprovider="/aspnetajax4/mydataservice.asmx"
    dataview:fetchoperation="LookupCustomers"
    dataview:selecteditemclass="selecteditem" 
    dataview:initialselectedindex="0">

この場合、まず頭文字が選択され、その頭文字によって取得される最初の顧客が自動的に表示されます。ユーザーは最初の顧客をクリックする必要がないため、注文を自動フェッチする処理は実行されません。最初の顧客を再度クリックすれば、その顧客の注文にアクセスできます。このようにして、表示されている他の顧客と同様に最初の顧客が処理されます。このような動作を防ぐ方法はあるでしょうか。

ユーザーにとっては、注文を表示するために最初の顧客をクリックすることは歓迎できません。実際、ボタン ハンドラーでは、キャッシュ内の情報しか表示できません。これは、コード内で重複する動作を避け、すべての処理を 1 回で行おうとしているためです。これを行うコードを次に示します。

function display() 
{
    // Attempt to retrieve orders from cache
    var cachedInfo = $('#viewOfCustomers').data(currentCustomer);
    if (typeof (cachedInfo) !== 'undefined')  
        data = cachedInfo;
    else
        data = "No orders found yet. Please wait ...";

    // Display any data that has been retrieved
    $("#listOfOrders0").html(data);
}

上記の表示機能を少し手直しして、現在の顧客が選択されていない場合に注文のフェッチをトリガーするようにします。これが、グローバルな currentCustomer 変数を使用しているもう 1 つの理由です。以下に、このように表示機能を編集したコードを示します。

function display() 
{
    if (currentCustomer == "") 
    {
        // Get the ID of the first item rendered by the DataView
        currentCustomer = $("#itemCustomer0").attr("commandargument");

        // The fetchOrders method requires a DOM element.
        // Extract the DOM element from the jQuery result.
        fetchOrders($("#itemCustomer0")[0]);    
    }

    // Attempt to retrieve orders from cache
    ...

    // Display any data that has been retrieved
    ...
}

顧客が手動で選択されていない場合は、最初にレンダリングする項目の sys:commandargument を読み取ります。これを実行する最速の方法は、DataView によってレンダリングされる項目の ID の名前付け規則を利用することです。オリジナルの ID に連続番号が付加されます。オリジナルの ID が itemCustomer であれば、最初の要素の ID は itemCustomer0 になります (これは、ASP.NET Ajax Library の最終リリース バージョンで変更される可能性がある DataView の側面です)。また、fetchOrders を DOM 要素に渡す必要があります。jQuery クエリは、DOM 要素のコレクションを返します。上記のコードに項目のセレクターを追加する必要があるのはこのためです。

最後になりますが、データ バインド後に、顧客が最初に表示されなくてもかまわなければ別のソリューションも使用できます。DataView の initialselectedindex 属性を -1 に設定すると、最初に顧客が選択されません。そのため、どのような場合でも、注文の詳細を表示するには、顧客をクリックして関連する注文のフェッチをトリガーすることになります。

まとめ

DataView は、Web クライアント アプリケーションのコンテキストでデータ バインドを行うための強力な手段です。マスター/詳細ビューなど、よくあるシナリオ用に特別に設計されています。ただし、考えられるすべてのシナリオがサポートされるわけではありません。今回のコラムでは、"先行フェッチ" パターンを実装することによって DataView ソリューションを拡張するコードについて説明しました。

[ASP.NET Ajax Library のベータ版は、ajax.codeplex.com (英語) からダウンロードできます。Visual Studio 2010 と同時にリリースされる予定です。―編集者注]

Dino Esposito は、近々発売される『Programming ASP.NET MVC』(Microsoft Press) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) です。

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