次の方法で共有


Cutting Edge

ASP.NET AJAX 4.0 における条件付きレンダリング

Dino Esposito

ASP.NET AJAX 4.0 におけるクライアント側レンダリングは、最も刺激的で長く待ち望まれていた新機能でしょう。この機能では、HTML テンプレートを使って目的のレイアウトを定義でき、ランタイム データのプレースホルダーにテキストベースの構文を提供します。外部データが Web サービス経由でダウンロードされ、クライアントのブラウザー内で処理されることを除けば、サーバー側でのデータ バインドするのとほとんど変わりはありません。

先月、DataView という新しいクライアント コントロールの基礎と、最もよく使用されるバインド技法について解説しました。今回はそこからさらに一歩進んで、条件付きテンプレート レンダリングについて説明します。

条件付きテンプレート レンダリング

ASP.NET AJAX 4.0 のデータ バインド用 HTML テンプレートはマークアップのブロックで、そこには ASP.NET マークアップ、HTML リテラル、およびランタイム データのなんらかのプレースホルダーを含めることができます。レンダリング アルゴリズムは非常にシンプルです。つまり、レンダリングは HTML テンプレートに結び付けられ、DataView コントロールがなんらかのデータをフェッチし、そのデータをテンプレートに埋め込みます。その結果生成されるマークアップでは、プレースホルダーが実際のデータに置き換えられ、本来の HTML テンプレートの代わりに表示されます。

DataView コントロールのインスタンスの作成方法には、宣言による方法とプログラムで行う方法の 2 種類がありますが、マークアップの生成に使用するアルゴリズムにとってはどちらも変わりありません。先月はこのあたりまで説明しました。

先に話題を進めると、当然のことながら疑問点が浮かび上がります。テンプレートをレンダリングするなんらかのロジックが必要なのではないでしょうか。さまざまな実行条件基づいて異なるマークアップを作成するには条件付きのレンダリングが必要ではないでしょうか。クライアント側コードの中には、バインドされるデータ項目の値や、JavaScript の他のグローバル オブジェクトの状態をチェックするため、マークアップに密接に結び付ける必要があるコードもあります。

ASP.NET AJAX 4.0 では、HTML テンプレートで独自の動作を組み込む際に使用する一連の特別な名前空間付き属性が定義されています。こうした動作は、レンダリング プロセスの特定の段階で評価および実行される JavaScript 式です。図 1 は条件付きテンプレート レンダリングの定義済みコード属性の一覧です。

これらのコード属性はテンプレート ビルダーによって認識され、そのコンテンツはレンダリング プロセスで適切に使用されます。図 1 の属性は ASP.NET AJAX テンプレートで使用される任意の DOM 要素に結び付けることができます。

図 1 の属性は、ASP.NET AJAX 4.0 ライブラリの Preview 4 および Visual Studio 2010 Beta 1 以前の code: 名前空間に相当します。Preview 5 からは、ASP.NET チームによって code: 名前空間が削除され、すべてに sys: 名前空間のみを使用するようになっています。

図 1 テンプレート内で DOM 要素を条件付きレンダリングするための属性

条件付きレンダリングの動作

sys:if 属性はブール式に割り当てられます。式が true を返せば、その要素はレンダリングされます。false を返せば、アルゴリズムは次のステップに進みます。非常に簡略化していますが、要点を表すと次のようになります。

<div sys:if="false">
:
</div>

マークアップの処理中に、ビルダーはこの DIV タグとそのコンテンツを単にすべて無視します。ある意味では、sys:if 属性を使えば、開発時にテンプレートの一部をコメントアウトできます。つまり、sys:if 属性に定数値 false を割り当てることは、次の C# コードで行っていることとあまり変わりはありません。

if (false)
{
  :
}

sys:if 属性に false を設定すれば HTML 要素がまったく非表示になるわけではありません。初期状態の ASP.NET AJAX テンプレートは、ブラウザーではプレーンな HTML として扱われます。つまり、すべてのテンプレートは、DOM ツリーに完全に処理されます。ただし、ASP.NET AJAX テンプレートが sys-template 属性で修飾されていると、ページを表示する時点では、DOM ツリーが一切表示されません。実際には、sys-template 属性は次の値を含む CSS クラスです。

.sys-template { display:none; visibility:hidden; }

sys:if 属性は、テンプレートの実際のマークアップからその HTML 要素を取り除きます。すべての ASP.NET AJAX テンプレートの外部の HTML 要素にアタッチされた sys:if 属性は無視されます。現時点では、sys:if 属性に関連付けられる else 分岐はありません。

sys:codebefore 属性と sys:codeafter 属性を定義すると、定義したコードが HTML 要素のレンダリング前後に実行されます。この 2 つの属性は、必要に応じて、組み合わせても、個別にも使用できます。属性のコンテンツには実行可能な JavaScript コードでなければなりません。

以上のことをまとめると、コード属性を使えば、必ずしもわかりやすいソリューションにはならなくても、可能性のあるほぼすべてのシナリオに対応することができます。では、sys:if と sys:codebefore を使ったもう少し複雑な例を見てみましょう。

ところで、ここからのコード例の中の変数にプレフィックスとして $ の付いた変数があることに気が付かれるでしょう。そこで、それらの変数について簡単に説明しておきます。

テンプレートの擬似変数

テンプレートのコード属性内で使用するカスタム コードでは、データ項目のすべてのパブリック プロパティにアクセスできます。さらに、テンプレート ビルダーによって適宜定義、公開される付加的な変数にもアクセスできます。

現状のドキュメントではこれらを "擬似列" と呼んでいますが、個人的には "擬似変数" という呼び方が気に入っています。図 2 に擬似変数をすべて示します。

これらの擬似変数を使って、実行中のレンダリング エンジンの内部状態を把握できます。ASP.NET AJAX テンプレートでは、JavaScript 変数と同様にこれらの変数を使用できます。

図 2 ASP.NET AJAX テンプレート ビルダーによってサポートされる擬似変数

テーブル内で条件に一致する行の色分け

ドロップダウン リストで国/地域を選択できる機能を備えた、顧客リストを表示するページを例として取り上げましょう。新しい国/地域を選択すると、必ず、該当する国/地域の顧客を書式を変えてレンダリングするために顧客リストが更新されるものとします (図 3 参照)。


図 3 条件付きテンプレート レンダリングを行う ASP.NET AJAX のサンプル ページ

図 4 にこのサンプル ページのマークアップを示します。ご覧のように、このページはマスター ページに関連付けられるコンテンツ ページです。必要な Sys 名前空間はマスター ページの Body タグ内で宣言されます。

図 4 コード属性の動作

<asp:Content ContentPlaceHolderID="PH_Body" runat="server">
<asp:ScriptManagerProxy runat="server" ID="ScriptManagerProxy1">
<Scripts>
<asp:ScriptReference Name="MicrosoftAjax.js"
Path="~/MicrosoftAjax.js" />
<asp:ScriptReference Path="~/MicrosoftAjaxTemplates.js" />
</Scripts>
</asp:ScriptManagerProxy>
<div>
<asp:DropDownList ID="listOfCountries" runat="server"
ClientIDMode="Static"
onchange="listOfCountries_onchange()">
</asp:DropDownList>
<table id="gridLayout">
<tr>
<th>ID</th>
<th>COMPANY</th>
<th>COUNTRY</th>
</tr>
<tbody id="grid" class="sys-template">
<tr sys:if="$dataItem.Country != currentCountry">
<td align="left">{{ ID }}</td>
<td align="right">{{ CompanyName }}</td>
<td align="right">{{ Country }}</td>
</tr>
<tr sys:if="$dataItem.Country == currentCountry"
class="highlight">
<td align="left"
sys:codebefore="if($dataItem.Country == 'USA') {
$element.style.color = 'orange';
$element.style.fontWeight=700;
}">
{{ ID }}
</td>
<td align="right">{{ CompanyName }}</td>
<td align="right">{{ Country }}</td>
</tr>
</tbody>
</table>
</div>
</asp:Content>

ASP.NET AJAX 4.0 の Preview 5 では、ScriptManager コントロールや Beta 1 に付属する MicrosoftAjax.js ファイルをオーバーライドする必要があります。これは一時的な解決策で、Beta 2 にアセンブリが更新されてリリースされたら、この解決策は不要です。

ASP.NET AJAX テンプレートについて解説する前に、ドロップダウン リスト コントロールのマークアップ コードを見ておきましょう。

ドロップダウン リストの設定

国/地域を選択するドロップダウン リストのコードは次のとおりです。

<asp:DropDownList ID="listOfCountries" runat="server" 
     ClientIDMode="Static" 
     onchange="listOfCountries_onchange()">
</asp:DropDownList>

ご覧のように、コントロールでは値を新しい ClientIDMode プロパティに代入し、DOM レベルの onchange イベントのクライアント側ハンドラーを指定しています。コントロールはサーバーでデータにバインドされます。正確には、従来の Page_Load メソッド内で次のようにバインドされます。

protected void Page_Load(object sender, EventArgs e)
{
   if (!IsPostBack)
   {
      // Fetch data
      string[] countries = new string [] {"[All]", "USA", ... "};

      // Serialize data to a string
      string countriesAsString = "’[All]’, ‘USA’, ...’";

      // Emit the array in the page
      this.ClientScript.RegisterArrayDeclaration(
           "theCountries", countriesAsString);

      // Bind data to the drop-down list
      listOfCountries.DataSource = countries;
      listOfCountries.DataBind();
   }
}

バインドは 2 手順で行われます。まず、プログラムから DropDownList コントロールにバインドされているのと同じデータを含む応答で、JavaScript 配列が設定されます。次に、従来のサーバー側データ バインドが実行されます。

この手法は "Dual-Side Templating" として知られ、標準的なクライアント側データ バインド パターンのバリエーションで、ページへの初回アクセス時にバインド対象のデータがサーバー上でフェッチされ、埋め込みの JavaScript 配列としてクライアントに提供されるという点が異なります。

それ以降に必要となるクライアント側のデータ バインドは、この埋め込み配列を使用して行われます。このようにして、データ取得のための余分なラウンドトリップを省いています。この従来のクライアント側データ バインドのバリエーションは、ユーザー操作時に変化しない静的データを表示する場合に便利です。上述の例では、この手法を国/地域のリスト取得にだけ使いました。顧客リストは Web サービス経由で Web サーバーからフェッチしています。

サーバー側コントロールを使って HTML マークアップを設定する際にマスター ページを使用している場合、実際の ID の制御を十分行えないことがあります。ASP.NET AJAX 4.0 では、新しい ClientIDMode プロパティを使って、この問題に柔軟に対処できるようにしています。

具体的には、今回の例のように ClientIDMode を Static に設定すると、HTML 要素のクライアント ID がサーバ ID と正確に一致します。ただし、テンプレートでデータ バインドされるコントロールのコンテキストでそのサーバー コントロールを繰り返し利用する予定があるときは、この手法では役に立ちません。

次のコードはドロップダウン リストの選択イベントの変化に対処します。

<script language="javascript" type="text/javascript">
    var currentCountry = "";

    function listOfCountries_onchange() {
        // Pick up the currently selected item
        var dd = $get("listOfCountries");
        currentCountry = dd.options[dd.selectedIndex].value;
        
        // Refresh the template
        refreshTemplate();
    }

    function refreshTemplate() {
        var theDataView = $find("grid");
        theDataView.refresh();
    }
</script>

DropDownList コントロールの ClientIDMode プロパティを Static に設定しないと、このコードで JavaScript エラーが発生します。これは、マスター ページを使用するときに、生成される各 HTML 要素に固有の ID が与えられるように、ASP.NET によって通常行われる複雑な ID 操作が原因です。

onchange イベントのハンドラーでは、現在選択されている国/地域の名前を JavaScript のグローバル変数に保存してから、ASP.NET AJAX テンプレートを更新します。それでは、テンプレートについて説明することにしましょう。

条件付きテンプレート

テンプレートは、以下のようにプログラムで作成および設定します。

<script language="javascript" type="text/javascript">
   function pageLoad()
   {
      dv = $create(Sys.UI.DataView,
              {
                 autoFetch: true,
                 dataProvider: “/ajax40/mydataservice.asmx",
                 fetchOperation: “LookupAllCustomers"
              },
              {},
              {},
              $get(“grid")
       );
    }
</script>

DataView クライアント コントロールでは、指定した Web サービスへの呼び出しを行い、特定のフェッチ操作を実行し、返されたすべてのデータを "grid" という DOM 要素のルートにある ASP.NET AJAX テンプレートに設定します。

テンプレート全体のレンダリングは、サンプル Web サービスの LookupAllCustomers メソッドの戻り値にバインドされる DataView コントロールのインスタンスを使用して行われます。このメソッドは ID、CompanyName、Country などのプロパティを持つ Customer オブジェクトのコレクションを返します。

データに生じる変化にかかわらず、ページの有効期間中、テンプレートは同じデータにバインドされた状態になります。では、データの更新が行われなくても、実行時の特定の条件変化に応じて、テンプレートのレンダリングを変更するにはどうすればよいでしょう。そのためには、テンプレートにコード属性を挿入する必要があります。

実際ここで必要なのが真の条件付きレンダリングです。ここでは、特定の条件が満たされた場合と満たされない場合でテンプレートのレンダリング方法を変えることになります。前述のように、sys:if 属性では "if-then-else" の意味合いになる構文はサポートされません。ブール式の値に基づいき、親要素をすべてレンダリングするか、すべて無視するかのいずれかです。

1 つの条件からの 2 つの分岐のシミュレーションを行う回避策は、それぞれ個別に異なるブール式によって制御される、相互排他的な 2 つの部分をテンプレートに含めることです。図 4 に示したコードにも、次のような構造が含まれています。

<tr sys:if="$dataItem.Country != currentCountry">
  :
</tr>
<tr sys:if="$dataItem.Country == currentCountry" 
    class="highlight">
  :
</tr>

変数 currentCountry は、現在選択している国/地域の名前を保持する JavaScript のグローバル変数です。この変数は、サーバー側の DropDownList コントロールの HTML マークアップによって、onchange イベントが発生するたびに更新されます。

上記のコード スニペットでは、バインドされるデータ項目の Country プロパティ値に基づき、条件に応じて 1 つ目の TR 要素がレンダリングされます。変数 currentCountryが選択した国/地域と一致していれば、1 つ目の TR 要素のレンダリングはスキップされます。この動作が可能なのは、グローバル変数が空文字列に初期化され、それ以降のどの値とも一致しないためです。結果として、どの顧客についても、テーブルの行テンプレートが最初にレンダリングされます。

ユーザーがドロップダウンリストから国/地域を選択すると、グローバル変数 currentCountry が更新されます。ただし、選択を行っただけでは図 3 のようなテンプレートの更新は自動的に行われません。テンプレートの更新は、onchange イベント ハンドラー内で明示的に指示しなければなりません。その方法を以下に示します。

var theDataView = $find("grid");
theDataView.refresh();

Microsoft AJAX ライブラリ内でコンポーネントのインスタンスを取得する検索用の関数に、簡単な $find 関数を使っています。$find (または $get) を使用するには、ScriptManager コントロールを使って、MicrosoftAjax.js のコア ライブラリを参照するように設定する必要があります。"grid" テンプレートに関連付けられた DataView インスタンスを取得したら、refresh メソッドを呼び出すだけです。refresh メソッドの内部では、テンプレートの再コンパイルと DOM の更新が行われます。DataView のインスタンスは、厳密には、登録済みのコンポーネントのリストから取得する必要はありません。DataView コントロールのインスタンスは、ページ読み込み時に作成し、グローバル変数に保存することもできます。

var theDataView = null;
function pageLoad()
{
   theDataView = $create(Sys.UI.DataView, ...); 
   :
}

次に、onchange ハンドラー内でグローバル インスタンスの refresh メソッドを呼び出します。

theDataView.refresh();

ここまでの例では、ページの残りの部分の変更をトリガーするユーザー インターフェイスとして、ドロップダウン リストを使いました。ドロップダウン リスト要素は、要素の中の 1 つが選択されると変更イベントを発生させるロジックが組み込まれた特殊な要素です。

ASP.NET AJAX 4.0 には、変更/通知のイベントをトリガーする、ページ操作に特化した汎用性の高いメカニズムがあります。そこで、ドロップダウン リストの代わりに独自のリストを使って上記の例を作り直してみましょう。

sys:command 属性

今度は、国/地域リストを HTML タグを使って、番号付きではない箇条書きとして生成します。テンプレートは次のとおりです。

<fieldset>
   <legend><b>Countries</b></legend>
   <ul id="listOfCountries" class="sys-template">
       <li>
           {{ $dataItem }}
       </li>
   </ul>
</fieldset>

このテンプレートは、レンダリングを目的として、プログラムから DataView コントロールにアタッチされます。テンプレートへのデータの設定には、JavaScript の埋め込み配列を使用します。この国のリストを含む JavaScript 配列は、Page クラスの ClientScript オブジェクトのサービスを使って、サーバーから提供されます。前のコード例とは異なり、次に示す Page_Load メソッドのコードにはサーバー側のバインド操作がありません。

protected void Page_Load(object sender, EventArgs e)
{
   if (!IsPostBack)
   {
      string[] countries = new string [] {"[All]", "USA", ... "};
      string countriesAsString = "’[All]’, ‘USA’, ...’";
      this.ClientScript.RegisterArrayDeclaration(
           "theCountries", countriesAsString);
   }
}

2 つ目の DataView コントロールは、ページが読み込まれるときにクライアントでインスタンスが作成されます。図 5 に JavaScript の pageLoad 関数を変更したコードを示します。

図 5 JavaScript の pageLoad 関数

<script language="javascript" type="text/javascript">
    function pageLoad()
    {
        $create(Sys.UI.DataView,
            {
                autoFetch: true,
                dataProvider: "/ajax40/mydataservice.asmx",
                fetchOperation: "LookupAllCustomers"
            },
            {},
            {},
            $get("grid")
        );
        $create(Sys.UI.DataView,
            {
                autoFetch: true,
                initialSelectedIndex: 0,
                selectedItemClass:"selectedItem",
                onCommand: refreshTemplate,
                data:theCountries
            },
            {},
            {},
            $get("listOfCountries")
        );
    }
</script>

ご覧のように、UL ベースのテンプレートに国/地域をバインドするために使用する 2 つ目の DataView コントロールは、1 つ目のものとはかなり構造が異なります。

最も顕著な違いは、データをインポートするのに data プロパティが使われている点です。埋め込みデータを使用する場合、これが適切な手順です。

データ ソースがユーザー定義オブジェクトの配列の場合、{{ 式 }} という構文でバインドを実行します。この式のコンテンツは、一般に、データ項目によって公開されるパブリック プロパティの名前です。この例では、データのバインド元は単純な文字列の配列です。そのため、バインド式でのデータ項目は、参照するパブリック プロパティのない文字列です。コードで表すと次のようになります。

<ul>
  <li>{{ $dataItem }}</li>
</ul>

initialSelectedIndex プロパティと selectedItemClass プロパティでは、画面に表示された項目の選択に関して、DataView コントロールに想定する動作を構成します。

DataView では、テンプレートを組み込みの選択動作にアタッチすることができます。initialSelectedIndex によって指定される位置の項目は、selectedItemClass プロパティで設定された CSS クラスに応じてスタイルが設定されます。最初に表示する際にどの項目も選択しておく必要がなければ、initialSelectedIndex プロパティに -1 を設定します。

このようにして作成したテンプレートで表示されるリストは、単純な UL ベースのリストであるため、次のコードでも明らかなように、選択操作に対応するロジックがネイティブに組み込まれていません。

<ul>
  <li>[All]</li>
  <li>USA</li> 
  :
</ul>

テンプレート内の HTML 要素に sys:command 属性を使用すると、次のように、複数のイベント ハンドラーを要素に動的にアタッチするよう、テンプレート ビルダーに指示できます。

<ul id="listOfCountries" class="sys-template">
    <li sys:command="select">
        {{ $dataItem }}
    </li>
</ul>

このようにして変更したLI 要素は、Internet Explorer 8 の [開発者ツール] ウィンドウに図 6 のように表示されます。sys:command 属性は、トリガーされるコマンドの名前を表す文字列値を受け取ります。この名前は自由に決めることができます。要素をクリックすることでコマンドがトリガーされます。一般的なコマンドは、select、delete、insert、および update です。コマンドがトリガーされると、DataView コントロールから onCommand イベントが発生します。onCommand イベントを処理し、テンプレートを更新するコードを次に示します。

<script type="text/javascript">
    var currentCountry = "";
    function refreshTemplate(args) 
    {
      if (args.get_commandName() == "select") 
      {
        // Pick up the currently selected item
        currentCountry = args.get_commandSource().innerHTML.trim();
                
        // Refresh
        var theDataView = $find("grid");
        theDataView.refresh();
      }
    }
</script>


図 6 sys:command 属性の効果として動的に追加されたイベント ハンドラー

HTML 内で直接発行すれば、ドロップダウン リストにも同じ手法を使えます。

<select>
  <option sys:command="select"> {{ $dataItem }} </option>
</select>

上記のコードは、Beta 1 ではバグによって想定どおりに機能しません (図 7 に動作中のサンプル ページを示します)。


図 7 選択操作にコマンドを使用

HTML 属性

ASP.NET AJAX 4.0 では、一連の特別な sys: 属性によって HTML 属性のアドホックなバインドを指定します。機能的には、これらの属性は HTML 属性と同じで、動作に目立った違いはありません。図 8 に、名前空間付き HTML 属性を示します。

テンプレート内の要素の属性にはすべて、sys: 名前空間プレフィックスが付きます。そもそも、名前空間をマップした属性を使用する理由はなんでしょう。図 8 には、ほんの一部の属性しか示されていないのはなぜでしょう。

図 8 HTML のマップ先属性

HTML 要素にバインドする必要があっても、{{...}} というバインド式には属性自体を設定したくはありません。DOM の観点からは、バインド式はある属性に値を代入することに過ぎず、また、そのように処理されます。当然、これには好ましくない副作用が生じる可能性があります。たとえば、入力要素の value 属性や要素のコンテンツにバインドする場合、ページの読み込み時にバインド文字列がほんの少しの間ユーザーに表示されることがあります。テンプレート外部でバインド式 (ライブ バインドや双方向バインド) を使用している場合にも同じことが起こります。また、図 8 に示した HTML 属性でも、バインド式で使用すると好ましくない影響が出ることがあります。たとえば、次のマークアップを考えてみましょう。

<img src="{{ URL }}" />

このマークアップでは、データ項目の URL プロパティの値ではなく、"URL" という文字列に対する要求をトリガーしてしまいます。

他の問題としては、一般にブラウザーによって不適切な属性の解決が行われる、XHTML の検証の問題が挙げられます。そのような重要な属性に sys 名前空間プレフィックスを付ければ、問題は解決します。

つまり、バインド式に代入される属性にはすべて sys 名前空間プレフィックスを付けるというのがベスト プラクティスです。DOM は名前空間付きのプレフィックスについてなんら関知しないので、属性はテンプレート ビルダーによって処理されるまで、副作用なしにバインド式を保持します。

クライアント側レンダリングでは、名前空間付き属性の使用をお勧めします。ただし、HTML の解析が不適切に行われる可能性がある場合を除けば、必須ではありません。

まったく新たな未知の世界

テンプレートとデータ バインドによって、ASP.NET AJAX 開発者にまったく新たな未知の世界が開かれます。来月は、ライブ バインドやマスター/詳細ビューなど、さまざまな種類のバインドを取り上げます。   

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