印刷用ページ       送信     
クリックして評価とフィードバックをお寄せください
 Cutting Edge: ListView のヒントと秘訣
Related Articles

.NET RIA Services には、認証、ロール、プロファイル管理などの一連のサーバー コンポーネントおよび ASP.NET の拡張機能が用意されています。ここではそのしくみについて説明します。

Jonathan Carter

MSDN Magazine May 2009

...

Read more!

XNA Game Studio 3.0 を使用して Zune 用のゲームを作成するための基本事項について Mike Calligaro が説明します。

Mike Calligaro

MSDN Magazine May 2009

...

Read more!

この記事では、同じアプリケーションの 2 つのバージョン (オンプレミス データ サービスを利用するアプリケーションと Azure Table データ サービスを利用するアプリケーション) を取り上げて、クラウドのデータにアクセスする方法について説明します。

Elisa Flasko

MSDN Magazine May 2009

...

Read more!

今月は、AJAX アプリケーションでフォームを使用するケースについて説明し、自動保存、JIT 検証、送信回数の削減などの機能を実装するさまざまな方法を紹介します。

Dino Esposito

MSDN Magazine June 2009

...

Read more!

今月は、IronPython を使用して .NET ベースのライブラリを簡単にテストできることを実演します。

James McCaffrey

MSDN Magazine June 2009

...

Read more!

Also by this Author

ASP.NET Web アプリケーションに対して適切な設計パターンを選択すると、プレゼンテーション層とその下位層との間で関心を分離することができます。

Dino Esposito

MSDN Magazine December 2008

...

Read more!

今月は、Dino が引き続き動的な Silverlight コンテンツの管理を取り上げ、キャッシュと分離ストレージについて説明します。

Dino Esposito

MSDN Magazine February 2009

...

Read more!

今月は、Dino Esposito が、Ajax Control Toolkit といくつかの賢いコーディング手法を使用して、Web アプリケーションで Windows スタイルのモーダル ダイアログ ボックスを表示する方法を説明します。

Dino Esposito

MSDN Magazine Launch 2008

...

Read more!

今月は、AJAX アプリケーションでフォームを使用するケースについて説明し、自動保存、JIT 検証、送信回数の削減などの機能を実装するさまざまな方法を紹介します。

Dino Esposito

MSDN Magazine June 2009

...

Read more!

ブラウザ間でイベントの互換性を保持することは簡単な作業ではありません。API を処理する jQuery イベントはブラウザ間のイベント処理の違いに対処するものです。このイベントを使用し、より予測可能な JavaScript を記述できます。

Dino Esposito

MSDN Magazine April 2009

...

Read more!

Popular Articles

この記事では、Windows Presentation Foundation でのプログラムおよび宣言によるデータ バインドと表示の手法を説明します。

Josh Smith

MSDN Magazine July 2008

...

Read more!

Microsoft patterns & practices の Composite Application Guidance for WPF で複合アプリケーションを作成する利点を紹介します。

Glenn Block

MSDN Magazine September 2008

...

Read more!

サイドバー ガジェットは小さいけれど強力なツールで、驚くほど簡単に作成できます。ここでは、Donavon West がその楽しさを教えてくれます。

Donavon West

MSDN Magazine August 2007

...

Read more!

Jason Clark

MSDN Magazine July 2003

...

Read more!

UpdatePanel を使用した方がよいケースと、むしろ WebMethod またはページ メソッドに対する非同期呼び出しを使用するほうがよいケースについて、Jeff Prosise が説明します。

Jeff Prosise

MSDN Magazine June 2007

...

Read more!

Cutting Edge
ListView のヒントと秘訣
Dino Esposito

先月号では、ASP.NET 3.5 のコントロール ツールボックスに新たに追加された ListView コントロールを紹介しました。おさらいしておくと、ListView は、DataList コントロールの拡張版です。これを使用すると、生成されたマークアップに対してより詳細な制御が可能で、ページングをサポートでき、データ ソース ベースのバインディング モデルと完全に統合できます。
このコラムでは、ListView テンプレートとデータ バインドの高度な使い方の紹介として、実際のページでかなり一般的に使用されるものの、追加のコーディングが必要とされる機能を実装します。具体的には、入れ子になった ListView コントロールを使用して階層的なデータのビューを作成し、カスタム ListView クラスを派生させて ListView のイベント モデルを拡張する方法について説明します。
特に、バインドされたデータ項目の各グループで異なるテンプレートを使用できるように、イベント モデルを洗練させます。たとえば、与えられたいくつかの条件に一致するデータ セット内のすべてのデータ項目に対して、異なるテンプレートを使用できます。これは、単に特定の項目に異なるスタイルを与えるだけではありません。ItemDataBound イベントを処理するだけで、任意のビュー コントロールで簡単に行うことができます。
通常、メニューは、CSS でスタイル設定された一連の <li> タグとして実装されます。フラットなメニューを表示するだけなら特にバインドの問題は発生しませんが、サブメニューが必要な場合はどうなるでしょうか。この場合、組み込みの Menu コントロールを使用するか、ListView を使用して、よりカスタマイズされた表示方法を作成します。なお、Menu コントロールの既定では、ListView で得られる CSS と親和性が高い出力ではなく、テーブルベースの出力を使用することにも注意してください (Menu コントロールで CSS との親和性が高い出力を得るには、CSS Control Adapter Toolkit (www.asp.net からダウンロードできます) をインストールして構成する必要があります。

階層型メニューを構築する
多くの Web アプリケーションでは、ページの左側または右側に垂直型のメニューがあります。このメニューを使用して、ユーザーは、2 つ以上の入れ子になったレベルのページ間を移動することができます。ASP.NET の Menu コントロールは、そのための選択肢として明らかに有望ですが、私は、メニューにデータを設定するための階層型データ ソース (一般には XML ファイル) があり、飛び出し型のサブメニューを作成する必要がある場合にだけ Menu コントロールを使用する傾向にあります。
複数レベルの静的な項目リストに対しては、UI デザイン チームによって作成されたマークアップを出力するために、繰り返し型のコントロールを使用します。ASP.NET 3.5 では、繰り返し型のコントロールとして ListView コントロールを選びます。
図 1 に示すようなメニューを考えます。これは、oswd.org からダウンロード可能な、CoffeeNCream の無料の HTML テンプレートを使って表示されています。サンプル ページでは、ASP.NET のマスタ ページに HTML マークアップを取り込んだだけです。
Figure 1 A Standard Menu (画像を拡大するには、ここをクリックします)
右側のメニューの HTML ソース コードは以下のようになります。
<h1>Something</h1>
<ul>
    <li><a href="#">pellentesque</a></li>
    <li><a href="#">sociis natoque</a></li>
    <li><a href="#">semper</a></li>
    <li><a href="#">convallis</a></li>
</ul>
ご覧のように、最上位の文字列の後にリンクのリストが続いています。最初の ListView を使用して H1 要素を作成し、次に入れ子になった ListView (または同様のデータバインド コントロール) を使用してリンクのリストを作成します。まず、メニューに表示するデータを取得する必要があります。以下の疑似タイプ オブジェクトのコレクションを使用して各項目を生成するのが理想です。
class MenuItem {
  public string Title;
  public Collection<Link> Links;
}

class Link  {
  public string Url;
  public string Text;
}
MenuItem コレクションにデータを設定する妥当な方法は、XML ファイルから情報を表示することです。ドキュメントのスキーマは以下のようになります。
<Data>
  <RightMenuItems>
     <MenuItem>
       <Title>Something</Title>
       <Link url="..." text="pellentesque" />
         :
     </MenuItem>
  </RightMenuItems>
</Data>
以下のコードは、LINQ to XML を使用して内容を読み込んで処理する方法を示します。
var doc = XDocument.Load(Server.MapPath("dataMap.xml"));
var menu = (from e in doc.Descendants("RightMenuItems")
            select e).First();
var menuLinks = from mi in menu.Descendants("MenuItem")
                select new
                {
                   Title = mi.Value,
                   Links = (...)
                };
ドキュメントを読み込んだ後、最初のノード RightMenuItems を選択して、その下位の MenuItem すべてを取得します。各 MenuItem ノードの内容は、2 つのプロパティ Title と Links を持つ新しい匿名タイプに読み込まれます。それでは、Links コレクションにはどのようにしてデータを設定するのでしょうか。以下にコードを示します。
Links = (from l in mi.Descendants("Link") 
         select new {Url=l.Attribute("url").Value, 
                     Text=l.Attribute("text").Value})
次の手順は、この複合データをユーザー インターフェイスにバインドすることです。前述のように、外側の ListView を使用してタイトルを表示し、入れ子になった ListView を使用して下位のリンクの一覧を表示します (図 2 を参照)。最も内側の ListView は、Eval メソッドを使用してデータにバインドする必要があります。それ以外の方法はうまく機能しません。
<asp:ListView runat="server" ID="RightMenuItems"
  ItemPlaceholderID="PlaceHolder2">
  <LayoutTemplate>
      <asp:PlaceHolder runat="server" ID="PlaceHolder2" /> 
  </LayoutTemplate>

  <ItemTemplate>
    <h1><%# Eval("Title") %></h1>

      <asp:ListView runat="server" ID="subMenu"
        ItemPlaceholderID="PlaceHolder3"
          DataSource='<%# Eval("Links") %>'>
          <LayoutTemplate>
            <ul>
              <asp:PlaceHolder runat="server" ID="PlaceHolder3" /> 
            </ul>
          </LayoutTemplate>
          <ItemTemplate>
            <li>
              <a href='<%# Eval("Url") %>'><%# Eval("Text") %></a>
            </li>
          </ItemTemplate>
      </asp:ListView>
  </ItemTemplate>
</asp:ListView>

<asp:ListView runat="server" ID="subMenu" 
    ItemPlaceholderID="PlaceHolder3"
    DataSource='<%# Eval("Links") %>'>
    ...
</asp:ListView>
データ バインド処理を開始するには、データを最上位の ListView にアタッチします。その際、入れ子になった ListView を含め、ListView の本体全体が表示されます。理論的には、親の ListView の ItemDataBound イベントを取得し、コントロール ツリーをたどって、下位の ListView の参照を取得し、それをプログラム的にデータにバインドすることができます。その場合、例外はスローしませんが、内側の ListView に対するバインド コマンドは失われます。表示に影響を与えるにはバインド コマンドの発生が遅すぎるためです。これに対しデータ バインド式は、データ バインド イベントの中で、コントロールのライフサイクルの正しい時点で、自動的に評価されます。これにより、正しいデータがユーザー インターフェイスに適切にバインドされます。

階層型ビューを構築する
階層型メニューにデータを設定するための同じモデルを、階層型のデータ ビューを構築するためにも使用できます。この場合、TreeView コントロールを使用して、複数レベルのデータ表現を準備することもできますが、TreeView コントロールに対するデータ バインドでは、階層型のデータ ソースが必要になります。入れ子になった ListView コントロールを使用することで、データ ソースの構造と、得られるユーザー インターフェイスの両方の点で柔軟性が高まります。この概念について詳しく説明します。
顧客、注文、および注文の詳細を、既存のテーブルのリレーションシップに応じて表示するために、階層型のデータ グリッドを作成する必要があるとします。データを取得してコントロールにバインドするにはどうすればよいでしょうか。図 3 のコードを参照してください。LINQ to SQL を使用して、データ階層を格納するのに適しているオブジェクト モデルにデータを簡単に読み込むことができます。LINQ to SQL でクエリを実行するとき、実際には明示的に要求したデータだけが取得されます。言い換えれば、グラフの最初のレベルだけが取得されます。関連するオブジェクトが同時に自動的に読み込まれることはありません。
Public Class DataCache
{
    public IEnumerable GetCustomers()
    {
        NorthwindDataContext db = new NorthwindDataContext();
        DataLoadOptions opt = new DataLoadOptions();
        opt.LoadWith<Customer>(c => c.Orders);
        opt.LoadWith<Order>(o => o.Order_Details);
        db.LoadOptions = opt;

        var data = from c in db.Customers
                   select new { c.CompanyName, c.Orders };

        return data.ToList();
    }

    public int GetCustomersCount()
    {
        // Return the number of customers
        NorthwindDataContext db = new NorthwindDataContext();
        return db.Customers.Count();  
    }

    public IEnumerable GetCustomers(int maxRows, int startRowIndex)
    {
        if (maxRows < 0)
            return GetCustomers();

        NorthwindDataContext db = new NorthwindDataContext();
        DataLoadOptions opt = new DataLoadOptions();
        opt.LoadWith<Customer>(c => c.Orders);
        opt.LoadWith<Order>(o => o.Order_Details);
        db.LoadOptions = opt;

        var data = (from c in db.Customers
                    select new { 
                      c.CompanyName, 
                      c.Orders 
                    }).Skip(startRowIndex).Take(maxRows);
        return data.ToList();
    }
}
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;

DataLoadOptions クラスを使用して LINQ to SQL エンジンの既定の動作を変更し、特定のリレーションシップによって参照されるデータがすぐに読み込まれるようにすることができます。図 3 のコードでは、顧客と同時に注文が読み込まれ、注文と同時にその詳細が読み込まれます。
LoadWith メソッドは、指定されたリレーションシップに従ってデータを読み込みます。AssociateWith メソッドでは、以下に示すように、関連するプリフェッチされたオブジェクトのフィルタリングが可能です。
opt.AssociateWith<Customer>(
     c => c.Orders.Where(o => o.OrderDate.Value.Year == 1997));
この例では、顧客データをフェッチするときに、1997 年に発行された注文だけがプリフェッチされます。関連データをプリフェッチして、フィルタを適用する必要がある場合は、AssociateWith メソッドを使用します。テーブル間で循環する参照がないことを確認するのはユーザーの役目です。たとえば、以下のように顧客の注文を読み込み、その後注文の顧客を読み込むような場合です。
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer> (c => c.Orders);
opt.LoadWith<Order> (o => o.Customer); 
これですべてのデータが準備できたため、バインドについて考えましょう。この例では、2 つのレベルの ListView コントロールが巧みに機能します。最上位の ListView を Customer オブジェクトのコレクションにバインドし、最も内側の ListView をバインドされた各 Customer オブジェクトの Orders プロパティにバインドします。図 4 のコードは、3 つのレベルの階層型ビューのマークアップを示します。顧客が最初のレベルに表示され、最も外側の ListView の ItemTemplate プロパティによって表示されます。埋め込まれた ListView は、注文にバインドされます。最後に、埋め込まれた ListView の ItemTemplate に GridView が格納され、各注文の詳細が表示されます。
<asp:ListView ID="ListView1" runat="server" 
  DataSourceID="ObjectDataSource1"
  ItemPlaceholderID="lvItemPlaceHolder">

  <LayoutTemplate>
    <asp:PlaceHolder runat="server" ID="lvItemPlaceHolder" />
  </LayoutTemplate>

  <ItemTemplate>
    <asp:Panel runat="server" ID="panelCustomerInfo"
      cssclass="customerInfo"> 
      <%# Eval("CompanyName") %>
    </asp:Panel>    
    <asp:panel runat="server" ID="panelCustomerDetails"
      cssclass="customerDetails">
      <asp:ListView runat="server" 
        DataSource='<%# Eval("Orders") %>' 
        ItemPlaceholderID="lvOrdersItemPlaceHolder">

        <LayoutTemplate>
          <ul>
            <asp:PlaceHolder runat="server" 
              ID="lvOrdersItemPlaceHolder" />
          </ul>
        </LayoutTemplate>

        <ItemTemplate>
          <li>
            Order #<%# Eval("OrderID") %> 
            <span class="orderDate"> 
              placed on <%#
              ((DateTime)Eval("OrderDate")).ToString
              ("ddd, dd MMM yyyy") %> 
            </span>
            <span class="orderEmployee"> 
              managed by <b><%# Eval("Employee.LastName") %></b>
            </span>
            <asp:GridView runat="server" 
              DataSource='<%# Eval("Order_Details") %>' 
              SkinID="OrderDetailsGridSkin" >
            </asp:GridView>
          </li>
        </ItemTemplate>
      </asp:ListView>
    </asp:panel>
  </ItemTemplate>
</asp:ListView>


エクステンダを使用してユーザー体験を向上させる
率直に言うと、図 4 のコードから得られるユーザー インターフェイスは、あまり魅力的ではありません。階層型データ ビューを構築する際には、展開/折りたたみパネルが、ユーザー体験を向上させるために特に有効なソリューションとなります。ASP.NET AJAX Control Toolkit では、作成済みのエクステンダが提供されており、これを Panel サーバー コントロールに適用することで、各顧客と注文に関連付けられた情報に対してドロップダウン効果を追加できます。
CollapsiblePanelExtender コントロールを使用して、ページ コントロール ツリー内に、展開と折りたたみが実行されるパネルをスクリプトで定義します。言うまでもなく、ページの開発者が JavaScript を記述する必要はありません。パネルの展開と折りたたみで必要なスクリプトは、エクステンダ コントロールによってすべて自動的に挿入されます。では、エクステンダに対して設定可能なプロパティについて見てみましょう。
<act:CollapsiblePanelExtender runat="server" ID="CollapsiblePanel1"  
     TargetControlID="panelCustomerDetails" 
     Collapsed="true"
     ScrollContents="true"
     SuppressPostback="true"
     ExpandedSize="250px"
     ImageControlID="Image1"
     ExpandedImage="~/images/collapse.jpg"
     CollapsedImage="~/images/expand.jpg"
     ExpandControlID="Image1"
     CollapseControlID="Image1">
</act:CollapsiblePanelExtender>
折りたたみ可能なパネル エクステンダをサポートするには、図 4 のコードをわずかに変更することが必要です。特に、パネル panelCustomerInfo を編集して、下位のビューを展開したり折りたたんだりするために使用するボタンを追加する必要があります。パネルのマークアップを書き換える 1 つの方法を以下に示します。
<asp:Panel ID="panelCustomerInfo" runat="server"> 
  <div class="customerInfo">
    <div style="float: left;"><%# Eval("CompanyName") %></div>
    <div style="float: right; vertical-align: middle;">
      <asp:ImageButton ID="Image1" runat="server" 
               ImageUrl="~/images/expand.jpg"
               AlternateText="(Show Orders...)"/>
    </div>
  </div>
</asp:Panel> 
ボタンは、顧客名と同じ行にある、右揃えのイメージを使用して表示されます。エクステンダ上の TargetControlID プロパティは、折りたたみと展開の対象となる、ページ内のパネルを参照します。これは、注文とその詳細が物理的に格納されているパネルです。図 4 に示すように、これは panelCustomerDetails という名前のパネルです。
ExpandControlID 属性と CollapseControlID 属性は要素の ID を示し、これらの要素をクリックするとターゲット パネルを展開したり折りたたんだりできます。パネルの状態を反映させるために別々のイメージを使用する場合は、イメージ コントロールの ID も指定する必要があります。この情報は、ImageControlID 属性に格納されます。ImageControlID は、他の 2 つのプロパティ CollapsedImage および ExpandedImage に関連付けられ、これらのプロパティにイメージの URL が格納されます。
ExpandedSize プロパティは、展開されたパネルの最大の高さをピクセル単位で設定します。既定では、この高さを超える内容はすべて切り取られます。ただし、ScrollContents プロパティに true を設定すると、垂直スクロールバーが追加され、内容全体をスクロールできるようになります。
最後に、ブール型のプロパティ Collapsed でパネルの初期状態を設定でき、SuppressPostback では、パネルの展開を完全にクライアント側で操作するかどうかを示します。SuppressPostback が true の場合、パネルの展開または折りたたみで使用するポストバックは使用されません。これは、表示されるデータを更新できないことを意味します。頻繁に変化しない比較的静的なデータでは、ページのちらつきやネットワーク トラフィックが減るため、この方法は明らかに優れています。ただし、コントロール内にデータを動的に表示する必要がある場合でも、UpdatePanel コントロールを使用することで、ちらつきを最小限にすることができます。図 5 は、3 つのレベルのデータ ビューのユーザー インターフェイスを示します。
Figure 5 Data View with Three Levels (画像を拡大するには、ここをクリックします)

DataPager と ListView
ListView コントロールは、新しい DataPager コントロールを通じたページング機能を提供します。DataPager は汎用のページング コントロールであり、IPageableItemContainer インターフェイスを実装する任意のデータバインド コントロールで使用できます。ASP.NET 3.5 では、ListView がこのインターフェイスをサポートする唯一のコントロールです。
DataPager コントロールでは、組み込みまたはテンプレートベースのユーザー インターフェイスを表示できます。新しいページにジャンプするためにユーザーが DataPager コントロールをクリックすると、IPageableItemContainer インターフェイス上のメソッドが呼び出されます。このメソッドでは、次のデータバインド操作の中でデータの特定のページだけが表示されるように、ページ分割されたコントロール内の内部変数を設定することになっています。
ここで、データの右側のページを選択する際に、データバインド コントロール (この場合は ListView) が引き続き問題であることがわかります。ASP.NET の他のビュー コントロールと同様に、ListView コントロールはページング動作を外部コードに依存します。データ ソース プロパティを通じてデータをバインドする場合は、ユーザー コードでデータのページ分割を実現する必要があります。また、データ ソース コントロールを通じてデータをバインドする場合は、ページングをサポートするように、データ ソース コントロールを正しく構成する必要があります。
LinqDataSource コントロールと ObjectDataSource コントロールには、どちらもページング機能が組み込まれています。LinqDataSource には、既定のページングを有効または無効にするための AutoPage プロパティがあります。階層型のデータでは、LINQ データ コンテキストに適切な読み込みオプションが設定されていることも必要です。LinqDataSource のプログラミング インターフェイスには、データ コンテキスト オブジェクトに対して LoadOptions プロパティを設定するためのプロパティはありません。ただし、以下のように ContextCreated イベントを処理することで、新たに作成したデータ コンテキストにアクセスし、自由に構成することができます。
void LinqDataSource1_ContextCreated(
    object sender, LinqDataSourceStatusEventArgs e)
{
    // Get a reference to the data context
    DataContext db = e.Result as DataContext;

    if (db != null)
    {
       DataLoadOptions opt = new DataLoadOptions();
       opt.LoadWith<Customer>(c => c.Orders);
       opt.LoadWith<Order>(o => o.Employee);
       opt.LoadWith<Order>(o => o.Order_Details);
       db.LoadOptions = opt;
    }
}
この動作の代替方法として、ObjectDataSource コントロールを使用してデータを提供し、ページング ロジックを実装することもできます。その後、ビジネス オブジェクトで、LINQ to SQL または通常の ADO.NET を使用してデータにアクセスします。
DataPager と ListView を同時に使用するうえで私が遭遇した問題については説明しておく価値があります。最初は、同じコンテンツ プレースホルダでホストされる ListView と DataPager を含むコンテンツ ページがありました。以下に示すように、このページでは、PagedControlID プロパティを使用して、DataPager 内で ListView コントロールを参照していました。これは正常に動作していたのです。
<asp:DataPager ID="DataPager1" runat="server" 
     PagedControlID="ListView1"
     PageSize="5" 
     EnableViewState="false">
  <Fields>
     <asp:NextPreviousPagerField 
        ShowFirstPageButton="true" 
        ShowLastPageButton="true" />
  </Fields>
</asp:DataPager>
次に、DataPager を同じマスタ内の別のコンテンツ領域に移動しました。すると突然、DataPager が ListView コントロールと通信できなくなりました。問題は、ページ分割されたコントロールを探すために DataPager コントロールが使用するアルゴリズムにあったのです。このアルゴリズムは、2 つのコントロールが異なる命名コンテナによってホストされているとうまく動作しません。この問題を回避するには、ページ分割されたコントロールを、その完全な一意の ID (命名コンテナ情報を含む) を使用して識別する必要があります。ただし、残念ながら、この情報を宣言的に設定する簡単な方法はありません。
これには、ASP スタイルのコード ブロックは使用できません。サーバー コントロールのプロパティを設定するために使用した場合、リテラルのように扱われるためです。データ バインド式 <%# ... %> も使用できません。DataPager で必要なタイミングよりも遅く式が評価されるためです。つまり、Load イベントが遅すぎて、DataPager で例外が発生します。最も簡単な回避策は、以下のように、ページの Init イベントで PagedControlID プロパティをプログラム的に設定することです。
protected void Page_Init(object sender, EventArgs e)
{
   DataPager1.PagedControlID = ListView1.UniqueID;
}

複数の項目テンプレート
ListView は、他のテンプレートベースのコントロールやデータがバインドされたコントロールと同様に、バインドされた各データ項目に対して同じ項目テンプレートを繰り返し適用します。それでは、特定の項目のサブセットに対して項目テンプレートを変更したい場合はどうすればよいでしょうか。正直に言うと、私は、何年も ASP.NET プログラミングを行ってきましたが、複数の項目テンプレートを使用する必要があったことは一度もありません。実行時の条件に基づいて、DataGrid コントロールと GridView コントロール内の小規模な項目のグループの外観をカスタマイズしたことは何度かありますが、常に異なるスタイル属性を適用する必要があっただけです。
既存のテンプレートにプログラム的に新しいコントロール (ほとんどは Label コントロールやテーブル セル) を追加したのはごくわずかです。データ バインド イベントを発行するデータがバインドされたコントロールでは、少なくとも操作しようとしているコントロールの内部構造に関する詳しい知識を持っていれば、これはあまり難しい作業ではありません。
プログラム的にコントロールを挿入することは実際に有効に動作するソリューションの 1 つではありますが、私にとって魅力のあるものではありませんでした。そのため、Web ページ内の ListView ベースのメニューを変更するよう顧客に依頼されたとき、私は別の方法を試すことにしました。図 1 に示したのと同様のメニューにおいて、1 つのサブメニューの項目を垂直ではなく水平に表示する必要がありました。
ListView コントロールは、データ ソースをループして、以下のアルゴリズムを適用しながらマークアップを生成します。まず、項目の区切りが必要かどうかを確認します。区切りが必要な場合、テンプレートをインスタンス化し、データ項目オブジェクトを作成します。データ項目オブジェクトは、項目テンプレートのコンテナであり、ビュー内の項目のインデックスおよびバインドされたデータ ソースに関する情報が格納されます。項目テンプレートがインスタンス化されると、ItemCreated イベントが発生します。次にデータのバインドを行います。これが完了すると、ItemDataBound イベントが発生します。
ご覧のように、各項目のテンプレートをプログラム的に変更するために利用できる公開イベントはありません。ページの Init イベントや Load イベントでテンプレートを変更することはできますが、それではバインドされたすべての項目が対象になってしまいます。ItemCreated を処理し、そこで ItemTemplate プロパティを設定すると、変更は現在処理中の項目ではなく次の項目に影響します。そうすると、ItemCreating イベントが必要となりますが、ListView コントロールではそのようなイベントは発生しません。そのため、解決方法は、図 6 のように独自の ListView コントロールを作成することになります。
namespace Samples.Controls
{
  public class ListViewItemCreatingEventArgs : EventArgs
  {
    private int _dataItemIndex;
    private int _displayIndex;

    public ListViewItemCreatingEventArgs(int dataItemIndex,
                                         int displayIndex) {
      _dataItemIndex = dataItemIndex;
      _displayIndex = displayIndex;
    }

    public int DisplayIndex {
      get { return _displayIndex; }
      set { _displayIndex = value; }
    }

    public int DataItemIndex {
      get { return _dataItemIndex; }
      set { _dataItemIndex = value; }
    }
  }

  public class ListView : System.Web.UI.WebControls.ListView
  {
    public event EventHandler<ListViewItemCreatingEventArgs>
                                               ItemCreating;

    protected override ListViewDataItem CreateDataItem(int
                           dataItemIndex, int displayIndex) {
      // Fire a NEW event: ItemCreating
      if (ItemCreating != null)
        ItemCreating(this, new ListViewItemCreatingEventArgs
                             (dataItemIndex, displayIndex));

      // Call the base method
      return base.CreateDataItem(_dataItemIndex, displayIndex);
    }
  }
}

CreateDataItem メソッドをオーバーライドすることで、項目テンプレートがインスタンス化される直前にコードを実行する機会ができます。CreateDataItem メソッドは、ListView クラス内で protected および virtual として宣言されています。図 6 に示すように、メソッドのオーバーライドは簡単です。まず、カスタム ItemCreating イベントを発生させ、基本メソッドを呼び出します。
ItemCreating イベントは、ユーザー コードにいくつかの整数 (データ ソース内の項目の絶対インデックスと、ページ固有のインデックス) を渡します。たとえばページ サイズが 10 の場合、ListView が第 2 ページの最初の項目の表示を処理している場合は、dataItemIndex に 11 個の項目が格納され、displayIndex に 1 個の項目が格納されます。新しい ItemCreating イベントを使用するには、以下のコードに示すように、カスタム ListView コントロール上でメソッドとハンドラを宣言するだけです。
<x:ListView runat="server" ID="ListView1" 
   ItemPlaceholderID="itemPlaceholder"
   DataSourceID="ObjectDataSource1"
   OnItemCreating="ListView1_ItemCreating">
   <LayoutTemplate>
      <div>
         <asp:PlaceHolder runat="server" ID="itemPlaceholder" /> 
      </div>
   </LayoutTemplate>
</x:ListView>
コードでイベントを処理します。
void ListView1_ItemCreating(
     object sender, ListViewItemCreatingEventArgs e)
{
    string url = "standard.ascx";
    if (e.DisplayIndex % DataPager1.PageSize == 0)
        url = "firstItem.ascx";

    ListView1.ItemTemplate = Page.LoadTemplate(url);
}
ここでは、データ項目を表示するために 2 つの異なるユーザー コントロールを使用しています。具体的なユーザー コントロールは、表示インデックスで特定されます。最初の項目を除き、すべての項目が同じテンプレートを共有します。図 7 に、ページの動作の様子を示します。
Figure 7 Multiple Item Templates (画像を拡大するには、ここをクリックします)
実際の一般的なページの複雑さを考えると、このソリューションは簡単すぎるように思われます。たいていの場合、表示するコンテンツに基づいて、異なるテンプレートを使用する必要があります。これには、カスタム ListView コントロールをさらに拡張して、データバインド処理内の項目テンプレートを変更する必要があります。図 8 のコードを参照してください。
namespace Samples.Controls
{
  public class ListViewItemCreatingEventArgs : EventArgs
  {
    private int _dataItemIndex;
    private int _displayIndex;

    public ListViewItemCreatingEventArgs(int dataItemIndex,
                                         int displayIndex) {
      _dataItemIndex = dataItemIndex;
      _displayIndex = displayIndex;
    }

    public int DisplayIndex {
      get { return _displayIndex; }
      set { _displayIndex = value; }
    }

    public int DataItemIndex {
      get { return _dataItemIndex; }
      set { _dataItemIndex = value; }
    }
  }

  public class ListView : System.Web.UI.WebControls.ListView
  {
    public event EventHandler<ListViewItemCreatingEventArgs>
     ItemCreating;

    private int _displayIndex;
    private bool _shouldInstantiate = false;

    protected override void InstantiateItemTemplate(Control container,
     int displayIndex) {
      if (_shouldInstantiate) {
        base.InstantiateItemTemplate(container, displayIndex);
        _shouldInstantiate = false;
      }
    }

    protected override ListViewDataItem CreateDataItem(int
     dataItemIndex, int displayIndex) {
      // Fire a NEW event: ItemCreating
      if (ItemCreating != null)
        ItemCreating(this, new
          ListViewItemCreatingEventArgs(dataItemIndex,
          displayIndex));

      // Cache for later
      _displayIndex = displayIndex;

      // Call the base method
      return base.CreateDataItem(_dataItemIndex, displayIndex);
    }

    protected override void OnItemCreated(ListViewItemEventArgs e) {
      base.OnItemCreated(e);

      // You can proceed with template instantiation now
      _shouldInstantiate = true;
      InstantiateItemTemplate(e.Item, _displayIndex);
    }
  }
}

CreateDataItem メソッドは ItemCreating イベントを発生させ、後で使用するために表示インデックスをキャッシュします。また、InstantiateItemTemplate メソッドがオーバーライドされ、テンプレートの実際のインスタンス化が遅れています。これにはプライベートなブール型フラグが使用されています。前述のように、ListView は、項目テンプレートをインスタンス化した後でデータバインド処理を開始します。
ただし、図 8 のコードに示す実装では、ItemCreated イベントが発生するまで、項目テンプレートは実際にはインスタンス化されません。ItemCreated イベントが発生すると、データ項目オブジェクトは DataItem プロパティを通じて ListView 項目コンテナにバインドされます。以下のように、コードで ItemCreated イベントを処理することで、バインドされたデータ項目に基づいて使用する項目テンプレートを決定できます。
protected override void OnItemCreated(ListViewItemEventArgs e)
{
   base.OnItemCreated(e);

   _shouldInstantiate = true;
   InstantiateItemTemplate(e.Item, _displayIndex);
}
この場合、ベース メソッドが ItemCreated イベントをページに通知します。その後、カスタム ListView コントロールがブール型のフラグをリセットし、メソッドを呼び出して項目テンプレートをインスタンス化します。最後に、組み込みの ListView コントロールよりも若干後に項目テンプレートがインスタンス化されますが、ItemCreated イベント ハンドラ内で、バインドされたデータ項目の内容を参照した後に、各項目の ItemTemplate プロパティをプログラム的に設定できます (図 9 を参照)。図 10 にサンプル ページを示します。男性には青いテンプレートが使用され、女性にはピンクのテンプレートが使用されます。
void ListView1_ItemCreated(object sender, ListViewItemEventArgs e)
{
    // Grab a reference to the data item
    ListViewDataItem currentItem = (e.Item as ListViewDataItem);
    Employee emp = (Employee) currentItem.DataItem;
    if (emp == null)
        return;

    // Apply your logic here
    string titleOfCourtesy = emp.TitleOfCourtesy.ToLower();
    string url = "forgentlemen.ascx";
    if (titleOfCourtesy == "ms." || titleOfCourtesy == "mrs.")
        url = "forladies.ascx";

    // Set the item template to use
    Samples.ListView ctl = (sender as Samples.Controls.ListView);
    ctl.ItemTemplate = Page.LoadTemplate(url);
}

Figure 10 A Standard Menu (画像を拡大するには、ここをクリックします)

まとめ
結局のところ、ASP.NET 3.5 の新しい ListView コントロールは、ASP.NET 1.0 から存在していた DataList コントロールを刷新したものです。ListView を使用すると、生成されるマークアップを詳細に制御し、データ ソース オブジェクトを完全にサポートすることができます。
このコラムでは、入れ子になった ListView コントロールを使用して、ページ分割可能な複数レベルのデータ ビューを構築する方法と、カスタム コントロールを派生させ、いくつかのメソッドをオーバーライドすることで、標準の ListView の表示処理を変更する方法について説明しました。最終的な結果として、複数の項目テンプレートをサポートするコントロールが得られました。ASP.NET の他のデータバインド コントロールでは、このような柔軟性は得られません。

ご意見やご質問は、こちらまで英語でお送りください。 cutting@microsoft.com.


Dino Esposito は、IDesign 社のアーキテクトであり、『Programming ASP.NET 3.5 Core Reference』の著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。連絡先は cutting@microsoft.com (英語) ですが、weblogs.asp.net/despos のブログにも参加してみてください。

Page view tracker