次の方法で共有


ASP.NET の階層的なデータ バインディング

Fritz Onion
DevelopMentor

適用対象:
    Microsoft® ASP.NET

要約: ASP.NET のデータ バインディングを実行して、2 次元を超える、本質的に階層的な性質を持ったデータ ソースにバインドする技法について説明します。

HierarchicalDataBindingSample.msi のダウンロード

目次

はじめに
データ バインディング
階層データ
階層データベースのデータにバインドする
XML データにバインドする
ネストされたコントロールにアクセスする
DataGrid および DataList の階層バインディング
制限と効率
まとめ

はじめに

ASP.NET に備わっている便利なアーキテクチャを使用してサーバー側のコントロールにデータをバインドすると、コントロールは予定された表示形式でそのデータをクライアントに表示できます。ASP.NET のデータ バインディングの例は、データベースへのクエリ結果であるフラットなデータ ソースにバインドする例がほとんどです。このようなデータ バインディングは多くのアプリケーションでよく使われていますが、その一方で、単純な 2 次元空間にデータが適合せず、標準のデータ バインディング技法では不十分な場合もあります。

この記事では、2 次元を超える本質的に階層的な性質を持ったデータ ソースにデータ バインディングを実行する技法について説明します。

データ バインディング

ASP.NET のデータ バインディングは、サーバー上でサーバー側のコントロールにデータをバインドして、そのデータをある形式でクライアントに表示するプロセスです。データ バインディングの制約としては、サーバー側のコントロールが DataSource というプロパティおよび DataBind() というメソッドをサポートし、コントロールにバインドされるデータ ソースが IEnumerable インターフェイスを実装している必要があるということだけです。

注: この要件には 2 つの例外があり、注意する必要があります。DataSetDataTable は、どちらも直接バインド可能であり、結果として、既定のテーブルの既定の DataView にバインドされます (DataViewIEnumerable を実装します)。DataSetsDataTable は、データ バインディングのデータ ソースとして頻繁に使用されるため、このような便宜が図られています。

コントロールにデータをバインドするには、コントロールの DataSource プロパティにデータ ソースを割り当て、DataBind() メソッドを呼び出します。

たとえば、次のデータ ソースについて考えた場合、この例では、Item クラスのインスタンスでいっぱいになった ArrayList が返されます。

  public class Item
  {
    private string _name;
    public Item(string name) { _name = name; }

    public string Name { get { return _name; } }
  }

  public class TestDataSource
  {
    public static ArrayList GetData()
    {
      ArrayList items = new ArrayList();
      for (int i=0; i<10; i++)
      {
        Item item = new Item("item" + i.ToString());
        items.Add(item);
      }
      return items;
    }
  }

ArrayListIEnumerable を実装しているため、TestDataSource クラスの GetData() メソッドの結果は、バインディングに有効なデータ ソースです。ここでは、データをバインドするサーバー側のコントロールとして、Repeater を使用します。そのためには、ItemTemplate を準備して、列挙可能なデータ ソースの各アイテムをどのように表示するかを指定する必要があります。このサンプルでは、バインドしている Item クラス インスタンスの Name プロパティがテキストに設定された CheckBox コントロールが表示されます。

<asp:Repeater Runat="server" ID="_itemsRepeater" 
              EnableViewState="false">
  <ItemTemplate>
    <asp:CheckBox Runat="server" 
 Text='<%# DataBinder.Eval(Container.DataItem, "Name") %>'
  />
    <br/>
  </ItemTemplate>
</asp:Repeater>

あとは Repeater にデータを実際にバインドするだけです。この操作は、次に示すように、Page 派生クラスの Load ハンドラで行うのが一般的です。

    private void Page_Load(object sender, EventArgs e)
   {
      _itemsRepeater.DataSource = TestDataSource.GetData();
      _itemsRepeater.DataBind();
    }

次の図は、このページの表示例を示します。

図 1. データ バインディング ページ

階層データ

最初のサンプルでは、データ レベルが 1 つだけのフラットなデータ ソースを使用しました。次の例では、データ ソースの各項目に、サブ項目のコレクションを追加します。

public class Item
{
  string _name;
  ArrayList _subItems = new ArrayList();

  public Item(string name) { _name = name; }

  public string    Name     { get { return _name;     } }
  public ArrayList SubItems { get { return _subItems; } }
}

作成したデータ ソース入力メソッド GetData で、各項目に 5 つのサブ項目を追加します。

public class TestDataSource
{
  public static ArrayList GetData()
  {
    ArrayList items = new ArrayList();
    for (int i=0; i<10; i++)
    {
      Item item = new Item("item" + i.ToString());
      for (int j=0; j<5; j++)
      {
        Item subItem = new Item("subitem" + j.ToString());
        item.SubItems.Add(subItem);
      }
      items.Add(item);
    }
    return items;
  }
}

これで、トップレベルの項目のコレクションの下に、データ レベルを 1 つ持った階層構造のデータができました。前に実行したデータ バインディングは、そのまま適切に機能します。ただし、第 1 レベルのデータだけが表示され、サブ項目は表示されません。この時点で、すべてのデータを正しく表示するには、各項目のサブ項目に対してネストされたデータ バインドを実行する必要があります。論理的に言うと、既にある RepeaterItemTemplate 内にもう 1 つデータ連結コントロールを追加し、トップレベルの Repeater によって列挙される各 ItemSubItems コレクションにそれをバインドする必要があることを意味します。ネストされた Repeater を .aspx ファイルに追加すれば、宣言による方法でコントロールを追加できます。ただし、ネストされた RepeaterDataSource プロパティに現在バインドされている ItemSubItems コレクションを正しく割り当てるには、いくらか注意が必要です。これを正しく行うには、宣言を使用して、ネストされた RepeaterDataSource プロパティにデータ バインディング式を設定します。SubItems コレクションは、次のようになります。

<asp:Repeater Runat="server" ID="_itemsRepeater" 
              EnableViewState="false">
  <ItemTemplate>
    <asp:CheckBox Runat="server" 
    Text='<%# DataBinder.Eval(Container.DataItem, "Name") 
      %>' 
         />
    <asp:Repeater Runat="server" ID="_subitemsRepeater"
                  EnableViewState="false"
         DataSource=
   '<%# DataBinder.Eval(Container.DataItem, "SubItems") %>'>
         <ItemTemplate>
           <br/>&nbsp;&nbsp;&nbsp;
           <asp:CheckBox Runat="server"
                         Text=
      '<%# DataBinder.Eval(Container.DataItem, "Name") %>' 
        />
         </ItemTemplate>
    </asp:Repeater>

    <br/>
  </ItemTemplate>
</asp:Repeater>

データ ソースをトップレベルの Repeater に既にバインドしているため、分離コード クラスでは何も変更する必要はありません。ネストされたデータ バインドは、トップレベル コレクションの各 Item に対して 1 度ずつ発生します。このようにネストされた 2 つのデータ連結コントロールを読み取るときは、最も近くにあるコントロールがデータ バインディング式 (<%# %>) の適用範囲となることに注意してください。この例では、最初の 2 つのデータ バインディング式は、外側のトップレベルの Repeater のデータ バインディングに適用され、トップレベル コレクションの現在の Item に解決されます。3 番目のデータ バインディング式は、内側の Repeater に適用され、現在バインドされている ItemSubItems コレクションの要素に解決されます。次の図は、このページの表示例を示します。

図 2. Repeater へのデータ バインディングを使用しているページの表示例

ネストされたデータ バインディングは、1 レベルだけに制限されず、任意の深さに拡張可能です。データ連結コントロールのネストと、データ ソースのコレクションのネストが一致し、データ ソースが規則的に形成されている限り、バインディングは機能します。例として、既存の SubItems コレクションの各 ItemSubItems コレクションを追加して、データ レベルがさらに追加されたデータ ソースに拡張してみます。

public class TestDataSource
{
  public static ArrayList GetData()
  {
    ArrayList items = new ArrayList();
    for (int i=0; i<10; i++)
    {
      Item item = new Item("item" + i.ToString());
      for (int j=0; j<5; j++)
      {
        Item subItem = new Item("subitem" + j.ToString());
        item.SubItems.Add(subItem);
        for (int k=0; k<4; k++)
        {
          Item subsubItem = 
               new Item("subsubitem" + k.ToString());
          subItem.SubItems.Add(subsubItem);
        }
      }
      items.Add(item);
    }
    return items;
  }
}

この場合も先と同様に、ネストされた新しいデータを表示するには、ネストされた Repeater をもう 1 つ追加するだけで十分です。この RepeaterDataSource プロパティは、第 2 レベルの Repeater によって現在列挙されている Item の SubItems プロパティにバインドします。

<asp:Repeater Runat="server" ID="_itemsRepeater"
              EnableViewState="false">
  <ItemTemplate>
    <asp:CheckBox Runat="server" 
 Text='<%# DataBinder.Eval(Container.DataItem, "Name") %>'
   />
    <asp:Repeater Runat="server" ID="_subitemsRepeater"
         EnableViewState="false"
         DataSource=
    '<%# DataBinder.Eval(Container.DataItem, "SubItems") %>'
      >
      <ItemTemplate>
        <br/>&nbsp;&nbsp;&nbsp;
        <asp:CheckBox Runat="server"
             Text=
       '<%# DataBinder.Eval(Container.DataItem, "Name") 
         %>'/>
        <asp:Repeater Runat="server" EnableViewState="false"
             DataSource=
    '<%# DataBinder.Eval(Container.DataItem, "SubItems") 
      %>'>
          <ItemTemplate>
            <br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
            <asp:CheckBox Runat="server"
                          Text=
      '<%# DataBinder.Eval(Container.DataItem, "Name") %>' 
        />
          </ItemTemplate>
        </asp:Repeater>
      </ItemTemplate>
    </asp:Repeater>
    <br />
  </ItemTemplate>
</asp:Repeater>

次の図は、このページの一部分の表示例です。

図 3. ネストされた追加の Repeater が表示されたページ

階層データベースのデータにバインドする

階層的なデータ バインディングの実行方法について基本事項を理解したところで、今度はより実用的なアプリケーションを検討してみましょう。ほとんどのデータ バインディングでは、データベース クエリの結果にバインドするため、ここでは、データベースから取得した階層データを使用します。階層データは通常、テーブル間の一対多のリレーションシップを使用して、リレーショナル データベースに格納されます。たとえば、SQL Server および Microsoft Access の既定のインストールで使用可能な Northwind サンプル データベースでは、Customers テーブルと Orders テーブルの間に一対多のリレーションシップが存在します。同様に、Orders テーブルと Order Details テーブルの間にも、一対多のリレーションシップが存在します。次の図は、これらのリレーションシップを示しています。

図 4. テーブルのリレーションシップ

さまざまな方法でこのデータのクエリを実行できますが、ここでは、データベースのラウンド トリップを 1 回だけに減らす効果もある一番簡単な方法を採用します。つまり、3 つのすべてのテーブルの内容を引き出して DataSet に入れ、DataSet 内でリレーションを定義する機能を利用して、データを階層的に取り出します。次に示すように、Load ハンドラがこれを実行し、結果の DataSet_customerRepeater という ID を持つ Repeater にバインドします。

private void Page_Load(object sender, EventArgs e)
{
  string strConn = 
       "server=.;trusted_connection=yes;database=northwind";

  string strSql  = "SELECT CustomerID, CompanyName FROM " +
                   " Customers; "                         +
                   "SELECT OrderID, CustomerID, "         + 
                   "        EmployeeID FROM Orders;"      +
                   "SELECT OrderID, Products.ProductID,"  +
                   "ProductName, Products.UnitPrice FROM" +
                   " [Order Details], Products WHERE "    +
                   " [Order Details].ProductID = "        +
                   "Products.ProductID";

  SqlConnection conn = new SqlConnection(strConn);
  SqlDataAdapter da = new SqlDataAdapter(strSql, conn);
  da.TableMappings.Add("Customers1", "Orders");
  da.TableMappings.Add("Customers2", "OrderDetails");

  _ds = new DataSet();

  da.Fill(_ds, "Customers");

  _ds.Relations.Add("Customer_Order", 
        _ds.Tables["Customers"].Columns["CustomerID"], 
        _ds.Tables["Orders"].Columns["CustomerID"]);
  _ds.Relations[0].Nested = true;
  _ds.Relations.Add("Order_OrderDetail", 
        _ds.Tables["Orders"].Columns["OrderID"], 
        _ds.Tables["OrderDetails"].Columns["OrderID"]);
  _ds.Relations[1].Nested = true;

  _customerRepeater.DataSource = _ds.Tables["Customers"];
  _customerRepeater.DataBind();
}

DataSet へのデータの読み込みが完了したら、設定したリレーションを使用して階層的にナビゲートできます。たとえば、DataSet 内の Customers テーブルの任意の行で、文字列 "Customer_Order" を指定して GetChildRows() を呼び出すと、Orders テーブルからその顧客に関連付けられた行のコレクションを取得できます。同様に、任意の注文に関連した Order Detail のエントリをすべて検索することもできます。それには、Orders テーブルの任意の行で、文字列 "Order_OrderDetail" を指定して GetChildRows を呼び出し、その注文に関連付けられたすべての Order Detail エントリを取得します。ここでの目的でさらに便利なのは、DataRowView クラスの CreateChildView メソッドです。このメソッドは、特定のリレーションのすべての行が表示された DataView を返します。

バインディング用のデータの準備が完了したら、データを表示するために、適切なネスト レベルのデータ連結コントロールを追加する必要があります。カスタム データ構造を使用した前の例と同様に、ここでバインドするデータにも深さのレベルが 2 つあります。したがって、各下位レベルのデータを表示するには、ネストされたコントロールが 2 つ必要です。具体的に言うと、DataSetCustomers テーブルにバインドするトップレベルの Repeater が 1 つ必要となるほか、各顧客に関連付けられたすべての Orders と、各注文に関連付けられたすべての Order Detail エントリにバインドするために、ネストされた Repeater が 1 つずつ必要になります。2 つのネストされた RepeaterDataSource は、適切なリレーション名を指定して、親の行で CreateChildView を呼び出した結果です。Repeater 宣言で DataView を単一の式として作成する代わりに、親の行とリレーション名を受け取り、DataView を返す関数を分離コード クラスで定義すると便利です。

protected DataView GetChildRelation(object dataItem, 
                                  string relation)
{
  DataRowView drv = dataItem as DataRowView;
  if (drv != null)
    return drv.CreateChildView(relation);
  else
    return null;
}

関数とデータ ソースの準備ができたら、.aspx ファイルに Repeater コントロールの宣言を記述できます。改行や空白を使用して極めて単純なデータ レイアウトを示します。

<asp:Repeater Runat="server" ID="_customerRepeater" 
              EnableViewState="false">
  <ItemTemplate>
    Customer:
    <%# DataBinder.Eval(Container.DataItem, "CustomerID") %>
   &nbsp; &nbsp;
    <%# DataBinder.Eval(Container.DataItem,"CompanyName") %>
    <br />
    <asp:Repeater runat="server" EnableViewState="false"
         DataSource=
            '<%# GetChildRelation(Container.DataItem, 
                                  "Customer_Order")%>'
    >
      <itemTemplate>
       &nbsp;&nbsp;&nbsp;&nbsp;
       Orderid:<b>
       <%#DataBinder.Eval(Container.DataItem, "OrderID")%> 
       </b><br/>
       <asp:Repeater runat="server" EnableViewState="false"
            DataSource=
                '<%# GetChildRelation(Container.DataItem, 
                                     "Order_OrderDetail")%>'
       >
         <itemTemplate>
           &nbsp;&nbsp;&nbsp;&nbsp;
           &nbsp;&nbsp;&nbsp;&nbsp;
           <b><%# DataBinder.Eval(Container.DataItem, 
                                  "ProductName") %></b>
           $<%# DataBinder.Eval(Container.DataItem, 
                                "UnitPrice") %> <br/>
         </itemTemplate>
       </asp:Repeater>
     </itemTemplate>
   </asp:Repeater>
 </ItemTemplate>
</asp:Repeater>

XML データにバインドする

今日のほとんどのシステムでは XML が階層データの主要形式であるため、XML を考慮しなければ階層データの説明としては完全とはいえません。ASP.NET でサーバー コントロールを XML データにバインドするには、いくつかの方法があります。その 1 つとして、XML データを DataSet に読み込んで、前に説明した技法を使用する方法があります。または、.NET の XML API を使用してデータを直接読み込んで、読み込まれたデータの列挙可能なクラスに対してバインドを行う方法もあります。最後になりましたが、XML ドキュメントに XSL 変換を適用して XML を表示する特殊な XML Web コントロールを使用する方法もあり、これが最も魅力的な方法であると言えます。

XmlDocument クラスは、.NET の XML DOM を実装し、データ バインディングをサポートするコントロールに対するバインディングに直接使用できます。XmlDocument の DOM のナビゲーションに使用される主なクラスは、ドキュメントの要素を表す XmlNode です。幸いなことに、XmlNode クラスは、子の列挙子を返す IEnumerable を実装します。これは、任意の XmlNode をデータ バインディングのデータ ソースとして使用できることを意味します。ドキュメントは実際には子を持つ単一のノードにすぎないため、XmlDocumentXmlNode から派生することは明らかであり、ナビゲーションがたいへん簡単になります。例として、publishers.xml ファイルに格納された次の XML ドキュメントについて考えてみましょう。

<publishers>
  <publisher name="New Moon Books" city="Boston"
             state="MA" country="USA">
    <author name="Albert Ringer   ">
      <title name="Is Anger the Enemy?" />
      <title name="Life Without Fear" />
    </author>
    <author name="John White   ">
      <title name="Prolonged Data Deprivation " />
    </author>
    <author name="Charlene Locksley   ">
      <title name="Emotional Security: A New Algorithm" />
    </author>
    <author name="Marjorie Green   ">
      <title name="You Can Combat Computer Stress!" />
    </author>
  </publisher>
  <publisher name="Binnet and Hardley" city="Washington" 
             state="DC" country="USA">
    <author name="Sylvia Panteley   ">
      <title name="Onions, Leeks, and Garlic" />
    </author>
    <author name="Burt Gringlesby   ">
      <title name="Sushi, Anyone?" />
    </author>
    <author name="Innes del Castillo   ">
      <title name="Silicon Valley Gastronomic Treats" />
    </author>
    <author name="Michel DeFrance   ">
      <title name="The Gourmet Microwave" />
    </author>
    <author name="Livia Karsen   ">
      <title name="Computer Phobic AND Non-Phobic" />
    </author>
  </publisher>
  <!-- ... -->
</publishers>

ページの Load ハンドラで、このファイルを XmlDocument クラスに読み込み、次に示すように、トップレベルの publishers 要素を Repeater にバインドできます。

   private void Page_Load(object sender, EventArgs e)
   {
      XmlDocument doc = new XmlDocument();
      doc.Load(Server.MapPath("~/Publishers.xml"));

      _rep1.DataSource = doc.FirstChild;
      _rep1.DataBind();
    }

今度は、XML ドキュメントのデータを取り出してクライアントに表示するために、必要となるネストされた Repeater を記述する方法を考える必要があります。前の 2 つの例を利用して、ほとんど同じ方法でこのデータを作成できます。このドキュメントには 3 レベルのデータ (publishers、authors、titles) があるため、ここでは 3 つの Repeater コントロールを定義します。authors の Repeater は publishers の Repeater の内部にネストし、titles の Repeater は authors の Repeater の内部にネストします。この配置は、次のようになります。

<asp:Repeater id="_rep1" runat="server"
              EnableViewState="false">
  <itemTemplate>
    Publisher: <%# ((XmlNode)Container.DataItem).
                    Attributes["name"].Value %><br/>
    <asp:Repeater runat="server" EnableViewState="false"
         DataSource='<%# Container.DataItem %>' >
      <itemTemplate>
        &nbsp;&nbsp;Author: <%# 
          ((XmlNode)Container.DataItem)
                            .Attributes["name"].Value 
                              %><br/>
        <asp:Repeater runat="server" EnableViewState="false"
                   DataSource='<%# Container.DataItem %>' >
           <itemTemplate>
              &nbsp;&nbsp;&nbsp;&nbsp;<i>
           <%# ((XmlNode)Container.DataItem).
                         Attributes["name"].Value %>
           </i><br />
           </itemTemplate>
         </asp:Repeater>
      </itemTemplate>
    </asp:Repeater>
    <hr />
  </itemTemplate>
 </asp:Repeater>

表示は次のようになります。

図 5. データ バインディングのテスト

XML データにバインドするプロセスは、前の 2 つの例とはかなり異なります。第 1 に注目すべき点は、宣言の DataSource の式が、Container.DataItem のように非常にシンプルなことです。それは、データ バインドの各レベルのデータ ソースが、子の列挙子を実装する単なる XmlNode であるためです。また、現在のデータ項目からデータを取り出すには、Container.DataItemXmlNode にキャストし、この場合には属性を取り出す必要があったという点にも注目してください。一般に便利な DataBinder.Eval() メソッドは、XML ソースではなく、データベース ソースで機能するようにデザインされているため、この場合には役立ちません。

一般に、データ連結コントロールを使用して任意の XML データをバインドするのはわずらわしい作業です。前の例ではデータベース テーブルのセットから取り出したデータを使用したので、データは規則的であり適切に構造化されていました。そのため、データの構造に合致した、ネストされたコントロールのセットを定義できました。データが不規則であったり、階層データでなかったりすると、バインドは非常に困難になります。たとえば、次の XML ドキュメントを見てみましょう。

<animals>
  <animal>
    <name>Dog</name>
   <sound>woof</sound>
   <hasHair>true</hasHair>
  </animal>
  <animal>
    <name>Cat</name>
   <sound>meow</sound>
   <hasHair>true</hasHair>
  </animal>
  <animal>
    <name>Pig</name>
   <sound>oink</sound>
   <hasHair>false</hasHair>
  </animal>
</animals>

前の例と同じ技法を使用する場合は、1 つのトップレベル Repeater を定義して各 animal 要素を列挙し、もう 1 つのネストされた Repeater を使用して animal の各サブ要素の表示を試みます。

<asp:Repeater ID="_animalRep" Runat="server" 
              EnableViewState="false">
   <ItemTemplate>
     <asp:Repeater Runat="server" EnableViewState="false"
                   DataSource='<%# Container.DataItem %>' >
       <ItemTemplate>
         <%# ((XmlNode)Container.DataItem).InnerText %><br 
           />
       </ItemTemplate>
     </asp:Repeater>
     <hr />
   </ItemTemplate>
 </asp:Repeater>

しかし、要素名をまったく使用せずに、単に子ノードの内容を表示するのは、あまり有力な方法ではありません。要素が name のときはある表示方法を使用し、sound のときは別の表示方法を使用するように Repeater に指定するのは、容易なことではありません。それどころか、意図するとおりに XML を表示するには、多数の条件式を記述することになるでしょう。

この時点で、ASP.NET のデータ連結コントロールは、任意の XML ドキュメントにバインドするようにデザインされていないことが明らかになります。代わりに、既存の XML 変換言語の XSL を利用して XML を表示したほうが、はるかに便利です。ASP.NET には、XML コントロールを使用して、ページの一部分の場合でも、XML を表示する便利な方法が準備されています。XML コントロールは、XML ドキュメントと XSL 変換を入力として受け取り、ドキュメントに変換を適用して、XML を表示します。この animal XML ドキュメントでは、次のような animal.xsl を記述できます。

<xsl:transform 
   version="1.0" 
   xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:template match="animal">
    <hr />
   <xsl:apply-templates />
  </xsl:template>

  <xsl:template match="name">
    <i><xsl:value-of select="." /></i><br/>
  </xsl:template>

  <xsl:template match="sound">
    <b><xsl:value-of select="." />!!!</b><br/>
  </xsl:template>

  <xsl:template match="hasHair">
    Has hair? <xsl:value-of select="." /><br/>
  </xsl:template>

</xsl:transform>

次に、この XSL 変換を、ページの XML コントロールへの入力として指定します。

<asp:Xml Runat="server" 
         DocumentSource="animals.xml" 
         TransformSource="animals.xsl" />

次のように表示されます。

図 6. animals.xsl の表示

ネストされたコントロールにアクセスする

これまでの例では、ユーザーからデータを収集することなしに、ただデータを表示することだけに焦点を当ててきました。さまざまな階層の深さでバインドされたコントロールからデータを取得するには、動的に作成されたコントロールの階層をナビゲートしてそれらの状態を取得する必要があるので、非常に骨の折れる作業です。より便利な方法としては、データ連結コントロールに埋め込まれたコントロールに、変更通知ハンドラを追加する方法があります。通知ハンドラが起動したときに、コントロールに関連付けられたデータを取り出せます。

この技法を説明するために、最初の例を使用して、Repeater をカスタム データにバインドし、各項目とサブ項目のチェック ボックスを表示します。ここでは、ユーザーがいずれかのチェック ボックスをオンにしてページを送信した場合は、チェック ボックスがオンになっていることをページの下部にある Label に印刷します。この .aspx ファイルは、次のようになります。

<asp:Repeater Runat="server" ID="_itemsRepeater" 
              EnableViewState="False">
  <ItemTemplate>
    <asp:CheckBox Runat="server" 
         Text='<%# DataBinder.Eval(Container.DataItem, 
                   "Name") %>'
            OnCheckedChanged="OnCheckedItem" />
    <asp:Repeater Runat="server" ID="_subitemsRepeater" 
                  EnableViewState="False"
         DataSource='<%# DataBinder.Eval(Container.DataItem, 
                                         "SubItems") %>'>
      <ItemTemplate>
        <br/>&nbsp;&nbsp;&nbsp;
        <asp:CheckBox Runat="server"
             Text='<%# DataBinder.Eval(Container.DataItem, 
                                       "Name") %>'
             OnCheckedChanged="OnCheckedItem" />
        <asp:Repeater Runat="server" EnableViewState="False"
          DataSource='<%# 
            DataBinder.Eval(Container.DataItem, 
                                          "SubItems") %>'>
          <ItemTemplate>
            <br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
            <asp:CheckBox Runat="server"
               Text='<%# DataBinder.Eval(Container.DataItem, 
                                         "Name") %>' 
               OnCheckedChanged="OnCheckedItem"/>
          </ItemTemplate>
        </asp:Repeater>
      </ItemTemplate>
    </asp:Repeater>
    <br />
  </ItemTemplate>
</asp:Repeater>

<asp:Button Runat="server" Text="Submit" />

<asp:Label EnableViewState="False" Runat="server"
           ID="_message" />

クライアントがページをポストすると、オンからオフ、またはオフからオンに状態が変更された各項目に対して、OnCheckedItem ハンドラが 1 度ずつ呼び出されます。イベント ハンドラへの sender パラメータを調べると、どのコントロールがクライアントによって変更されたかを識別できます。チェック ボックスの状態が変更されたことを示すメッセージをページに書き込むハンドラの実装サンプルは次のとおりです。

protected void OnCheckedItem(object sender, EventArgs e)
{
  CheckBox cb = sender as CheckBox;
  if (cb.Checked)
      _message.Text += string.Format("{0} was checked<br 
        />", 
                                     cb.Text);
  else
     _message.Text += string.Format("{0} was 
       unchecked<br/>", 
                                    cb.Text);
}

DataGrid および DataList の階層バインディング

これまでのすべての例では、簡潔にするために Repeater コントロールに焦点を当てていましたが、 DataList コントロールと DataGrid コントロールを使用して、階層的なデータ バインドを実行することも可能です。実際には、使用するコントロールにかかわらず、データ バインディングの詳細は同じです。Repeater とネストされた DataList を使用するなど、コントロールを組み合わせることも可能です。この記事のコード サンプルには、DataList クラスと DataGrid クラスについて、Repeater を使用したサンプルとまったく同じものが含まれています。

DataList クラスによって表示されるテーブルでは、テーブルの各セルに結果セットの行が表示されます。DataList を階層的なバインドに使用すると、ネストされたテーブルが表示されます。トップレベル コントロールのセルには、ネストされたコントロールによって表示されるテーブル全体が含まれます。次の表示例では、3 つのレベルの階層を使用して DataList にデータをバインドすることで、Dataset に Northwind データが挿入されています (1 つのトップレベルのセルのみ)。

図 7. Northwind データが挿入された DataSet

DataGrid クラスによって表示されるテーブルでは、テーブルの各行に結果セットの行が表示されます。DataGrid を階層的なバインドに使用した場合も、ネストされたテーブルが表示されます。ただし、DataList の場合とは異なり、ユーザーの判断しだいでどのセルにネストされたテーブルを含めるかを決定できます。それには、列をテンプレート列として作成し、ネストされた DataGrid をテンプレート列の定義の一部として追加します。次の表示例では、3 つのレベルの階層を使用して DataGrid にデータをバインドすることで、Dataset に Northwind データが挿入されています (1 つのトップレベルの行のみ)。

図 8. Northwind データが挿入された DataSet

制限と効率

ASP.NET のデータ バインディング メカニズムは、フラットなデータ ソースとのバインド用にデザインされている点に注意してください。階層のバインドにも使用できますが、本質的に階層データの性質を持つデータの表示には最適な選択ではない場合もあります。データ ソースは規則的に形成されている必要があります。つまり、ある場所では深さのレベルが 2 で他の場所では 4 か 5 の場合、そのデータ ソースはバインドに適しません。任意の階層で形成されたデータの表示には、XSL が最適です。したがって、XML へのデータの変換および ASP XML コントロールでの XSL 変換の使用が、通常では最適な方法になります。

この記事のすべてのサンプルでは、各データ連結コントロールの EnableViewState フラグが、注意深く false に設定されています。 ViewState は、同一ページへの POST 要求の合間に、コントロールに代わって状態を格納するために使用されます。既定では、ASP.NET のすべてのサーバー側のコントロールでは、POST 要求の合間のすべての状態が保存されます。これによって、すべてのコントロールの状態が保存されるため、たいへん便利です。ただし、ページのサイズが著しく増加する可能性があるため、状態の保存を利用しない場合は、それらのコントロールの ViewState をオフにする方法を取ってください。この技法を使用すると、各データ連結コントロールとそのすべての子コントロールの全内容が ViewState に既定で格納されるため、特に ViewState が膨張しやすくなります。ほとんどの場合、この ViewState の内容は、まったく使用されません。というのは、ページへの最初の GET 要求であれ、それ以降の同じページへの POST 要求であれ、データ連結コントロールは各要求ごとにデータ ソースに再びバインドされるからです。したがって、特にこの記事で説明している階層的なデータ バインディング技法を使用する場合は、他に特別な理由がない限り、すべてのデータ連結コントロールの EnableViewState を false に設定することをお勧めします。Repeater の例では、内部にネストされた CheckBox コントロールの ViewState を無効にしませんでした。これと同様に、データ連結コントロールの ItemTemplate の内部にネストされたサーバー側のコントロールについては、ViewState が有効なままであっても構いません。ただし、実際のデータ連結コントロールでは、無効にすることを忘れないでください。どれだけ深刻な問題になり得る可能性があるかを理解するための例として、サンプルで DataSet にバインドされるすべての Repeater コントロールの ViewState を有効にした場合、ViewState フィールドは 250,000 文字に膨張しました。これに対して、ページに表示可能な文字数は、およそ 100,000 文字です。

まとめ

この記事では、テンプレート化されたデータ連結コントロールをネストし、動的にデータ ソースを割り当てて、階層データにバインドするための技法について説明しました。この技法は、データベース内のテーブル リレーションで見られるような、規則的で構造化された階層データの表示に役立ちます。この技法を使用して他の階層データ ソースを表示することも可能ですが、一部の次元でデータが不規則な場合は、扱いづらい技法です。代替の技法として、XML データ ソースで XSL を使用すると、通常ではより簡潔に表示できるうえ、表示を詳細に制御できます。