Hierarchical Data Binding in ASP.NET

 

Fritz Onion
DevelopMentor

Applies to:
    Microsoft® ASP.NET

Summary: Learn about techniques for performing ASP.NET data binding to data sources that are more than two dimensions and are hierarchical in nature. (27 printed pages)

Download HierarchicalDataBindingSample.msi.

Contents

Introduction
Data Binding
Hierarchical Data
Binding to Hierarchical Database data
Binding to XML Data
Accessing Nested Controls
DataGrid and DataList Hierarchical Binding
Limitations and Efficiency
Conclusion

Introduction

ASP.NET provides a convenient architecture for binding data to server-side controls, which the controls then render to the client in whatever format the control is designed to display. Most examples of data binding in ASP.NET are examples of binding to flat data sources that are the result of queries made to a database. While this type of data binding is the most common in many applications, there are occasions where the data does not fit into a simple two-dimensional space, and standard data binding techniques fall short.

This article looks at techniques for performing data binding to data sources that are more than two dimensions and are hierarchical in nature.

Data Binding

Data binding in ASP.NET is the process of binding data on the server to a server-side control that will then render that data in some form to the client. The only constraints for data binding are that the server-side control must support a property calledDataSourceand a method calledDataBind(), and that the data source to which the control is bound implement theIEnumerableinterface.

Note   There are two notable exceptions to this requirement—theDataSet,DataTablecan both be bound to directly, which results in binding to the defaultDataViewof the default table (DataViewdoes implementIEnumerable). This is for convenience asDataSetsand DataTables are frequently used as a source of data in data binding.

To bind data to a control, you assign the data source to the DataSource property of the control and call itsDataBind()method.

For example, consider the following source of data, which returns anArrayListfull of instances of theItemclass:

  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;
    }
  }

Because theArrayListimplementsIEnumerable, the result of ourGetData()method in theTestDataSourceclass is a valid data source for binding. We will use theRepeateras the server-side control to which we will bind the data, which requires that we provide anItemTemplatedescribing how we would like each item in the enumerable data source rendered. Our sample renders aCheckBoxcontrol with its text set to theNameproperty of the Item class instance to which it is bound:

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

All that now remains is to perform the actual binding of data to ourRepeater, which is typically done in theLoadhandler of thePage-derived class, as shown here:

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

A sample rendering of this page is shown below:

Aa478959.aspn-hierdatabinding_01(en-us,MSDN.10).gif

Figure 1. Data binding page

Hierarchical Data

Our first data source sample was flat with only one level of data. Now, suppose we add a collection of sub-items to each item in our data source, as follows:

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; } }
}

And in our artificial data source population method,GetData, let's add 5 sub-items to each item as shown below:

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;
  }
}

Our data structure is now hierarchical with one level of data below the top-level collection of items. The data binding that we performed earlier will still work properly, but it will only show the first level of data, ignoring the sub-items when it renders. What we need at this point to properly display all of the data is to perform a nested data bind on the sub-items of each item. Logically, this means that we need to place another data-bound control within theItemTemplateof theRepeaterthat we already have, and bind it to theSubItemscollection of eachItemthat is enumerated by the top-levelRepeater. This can be achieved declaratively in our .aspx file by adding a nestedRepeater. The only tricky part is to correctly map theSubItemscollection of theItemcurrently being bound to the DataSource property of the nestedRepeater. This is done by declaratively setting theDataSourceproperty of the nestedRepeaterto a data binding expression, which results in the SubItems collection as shown below:

<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>

Nothing would have to change in our code-behind class because we are already binding our data source to the top-levelRepeater. The nested data bind will happen once perItemin the top-level collection. It is important to keep in mind when you are reading a pair of nested data-bound controls like this, that the data-binding expressions (<%# %>) are scoped by their nearest control. In our example, the first two data-binding expressions are scoped to the outer data binding of the top-levelRepeaterand resolve to the current Item in the top-level collection. The third data-binding expression is scoped to the inner Repeater and resolves to an element in theSubItemscollection of the currentItembeing bound. The rendering of this page is shown below:

Aa478959.aspn-hierdatabinding_02(en-us,MSDN.10).gif

Figure 2.Rendering of page with data binding to a Repeater sample

Note that this nested data binding is not restricted to just one level, but can be extended arbitrarily deep. As long as the nesting of the data-bound controls matches the nesting of the collections in the data source, and the data source is regular in shape, the binding will work. As an example, let's extend our data source to include another level of data, giving each Item in the existing SubItems collections its own collection ofSubItems.

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;
  }
}

Again, the only change in our page necessary to display this new nested data is to add another nested Repeater whoseDataSourceproperty is bound to theSubItemsproperty of the Item currently being enumerated by the second-levelRepeater.

<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>

A partial rendering of this page is shown below:

Aa478959.aspn-hierdatabinding_03(en-us,MSDN.10).gif

Figure 3. Page showing additional nested Repeater

Binding to Hierarchical Database data

Now that we have the fundamentals of how to perform hierarchical data binding down, it's time to explore a more practical application. Since most data binding involves binding to the results of a database query, we will use hierarchical data retrieved from a database next. Hierarchical data is typically stored in relational databases by using one-to-many relationships between tables. For example, in the sample Northwind database that is available in default SQL Server and Microsoft Access installations, there is a one-to-many relationship between theCustomerstable and theOrderstable. Similarly, there is a one-to-many relationship between theOrderstable and theOrder Detailstable. These relationships are shown in the figure below:

Aa478959.aspn-hierdatabinding_04(en-us,MSDN.10).gif

Figure 4. Table relationships

There are several ways we could query for this data, but for our purposes the simplest way, which also reduces the number of round trips to the database to one, is to pull the contents of all three tables into a DataSet and leverage the ability to define relations within a DataSet to extract the data hierarchically. The followingLoadhandler does this, and then binds the resultingDataSetto a Repeater with the ID of_customerRepeater.

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();
}

Once the data is loaded into theDataSet, it can now be navigated hierarchically by using the relations that we established. For example, given any row in theCustomerstable inside ourDataSet, we can callGetChildRows()with the string "Customer_Order" to retrieve a collection of rows from the Orders table associated with that customer. Similarly, we can find all of theOrder Detailentries associated with any given order by callingGetChildRowswith the string "Order_OrderDetail" on a row from theOrderstable to retrieve allOrder Detailentries associated with that order. Even more useful, for our purposes, is theCreateChildViewmethod of theDataRowViewclass, which returns aDataViewdisplaying all of the rows for a particular relation.

Now that we have the data prepared for binding, we need to add data bound controls with appropriate levels of nesting to render the data. Much like our previous example using a custom data structure, the data we are binding to here also has two levels of depth, meaning that we will need two nested controls to render each sub-level of data. More specifically, we will need one top-levelRepeaterto bind to theCustomerstable in ourDataSet, one nestedRepeaterto bind to all of theOrdersassociated with each customer, and another nestedRepeaterto bind to all of theOrder Detailentries associated with each order. TheDataSourcefor the two nestedRepeaterswill be the result of callingCreateChildViewon the parent row, with the appropriate relation name. Instead of trying to create theDataViewin a single expression in theRepeaterdeclaration, it will be convenient to define a function in our code behind class that takes the parent row and the name of the relation, and returns theDataView.

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

With that function and our data source in place, we can now write theRepeatercontrol declarations in our .aspx file, presenting a very simple layout of the data using breaks and spaces:

<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>

Binding to XML Data

Any discussion of hierarchical data would be incomplete without a discussion of XML, as that is the dominant format for hierarchical data in most systems today. There are a few options for binding server controls to XML data in ASP.NET. One option is to read the XML data into a DataSet and then use the techniques shown in the previous section. Another option is to use the XML API in .NET to load the data directly and bind against enumerable classes in the loaded data. The last, and perhaps most appealing option, is to use the specialized Xml web control that renders itself by applying an XSL transform to an XML document.

TheXmlDocumentclass provides an implementation of the XML DOM in .NET, and can be used directly for binding against controls that support data binding. The primary class used to navigate the DOM inXmlDocumentisXmlNode, which represents an element in the document. Fortunately for us, theXmlNodeclass implementsIEnumerableto return an enumerator over its children, which means we can use anyXmlNodeas a data source in data binding. It turns out thatXmlDocumentalso derives fromXmlNode, as a document is actually just a single node with children, making it quite straightforward to navigate. As an example, consider the following XML document stored in the file 'publishers.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>

We can load this file into anXmlDocumentclass in our page'sLoadhandler and bind the top-levelpublisherselement to a repeater as follows:

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

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

Next we need to figure out how to write the necessary nestedRepeatersto extract the data from the XML document and render it to the client. Drawing on our previous two examples, we can model this data in much the same way. Given that our document has three levels of data (publishers, authors, and titles) we will define threeRepeatercontrols, with the authorsRepeaternested within the publishersRepeater, and the titlesRepeaternested within the authorsRepeater. This arrangement is shown below:

<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>

This renders as:

Aa478959.aspn-hierdatabinding_05(en-us,MSDN.10).gif

Figure 5. Data binding test

The process of binding to XML data is quite different from the previous two examples. First of all, note that our declarative DataSource expressions were remarkably simple—Container.DataItem. This is because the data source at each level of the data bind is simply anXmlNode, which implements an enumerator over its children. Also notice that in order to extract data from the current data item we had to cast theContainer.DataItemtoXmlNodeand extract (in this case) its attributes. The usually convenientDataBinder.Eval()method is useless in this case because it is designed to work with database sources, not XML sources.

In general, binding arbitrary XML data using data binding controls is a rather cumbersome task. The previous example used data extracted from a set of database tables, and was thus quite regular and well structured, making it possible to define a set of nested controls that matched the structure of the data. This becomes more difficult if the data is irregular, or even if the data is not hierarchical. For example, consider the following XML document:

<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>

If we used the same technique as our previous example, we might try defining one top-level repeater to enumerate each animal element with another nested repeater to display each sub-element of 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>

This is not very compelling, however, as it simply renders the contents of each of the child nodes without using the element name at all—there is no easy way to tell the Repeater to render itself one way if the element is name and another way if it is sound. Instead, we would end up writing many conditional expressions to get the XML to render the way we wanted.

At this point it should be obvious that the data binding controls in ASP.NET were not designed to bind to arbitrary XML documents. It is instead much more convenient to leverage the existing XML transform language XSL to render XML. ASP.NET does provide a convenient way of doing this, even for a portion of a page, through the Xml control. It takes as input an XML document and an XSL transform, and renders by applying the transform to the document. For our animal XML document, we might write animal.xsl that looked like:

<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>

Which we could then specify as input to an Xml control on our page:

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

And would render as:

Aa478959.aspn-hierdatabinding_06(en-us,MSDN.10).gif

Figure 6. Rendering of animals.xsl

Accessing Nested Controls

In our examples so far we have focused on the presentation of the data only, with no collection of data from the user. Retrieving data from within the depths of a hierarchically bound control can be quite laborious as you must navigate through the hierarchy of dynamically created controls and retrieve their state. Another, more convenient option, is to add change notification handlers to controls embedded in the data-bound control. When the notification handler fires, you can extract the data associated with the control.

To demonstrate this technique, we can use our first example of binding aRepeaterto custom data and rendering check boxes for each item and sub-item. If the user checks one of the check boxes and submits the page, we simply print the fact that it was checked at the bottom of the page to aLabel. The .aspx file for this looks like:

<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" />

Now, whenever the client posts the page, ourOnCheckedItemhandler will be called once for each item whose state was changed (checked to unchecked or unchecked to checked). We can identify which control was changed by the client by looking at the sender parameter to our event handler. A sample implementation of such a handler that writes a message to the page indicating that a check box changed state is shown here:

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 and DataList Hierarchical Binding

All of the examples so far have focused on theRepeatercontrol for simplicity. It is possible, however, to perform hierarchical data binds with both theDataListas well as theDataGridcontrols as well. In fact, the details of binding the data are identical regardless of which control you use. You can even mix and match them by having aRepeaterwith a nestedDataList, for example. The code samples for this article include identical samples to the ones presented using theRepeaterfor both theDataListandDataGridclasses.

TheDataListclass renders a table where each cell in the table is a rendering of a row in the result set. Using theDataListto bind hierarchically results in nested table renderings where a cell in the top-level control will contain an entire table rendered by the nested control. A sample rendering of ourDataSetpopulated with the Northwind data using three levels of hierarchical data binding to aDataListis shown (one top-level cell only) below.

Aa478959.aspn-hierdatabinding_07(en-us,MSDN.10).gif

Figure 7. DataSet populated with Northwind data

TheDataGridclass renders a table where each row in the table is a rendering of a row in the result set. Using theDataGridto bind hierarchically results in nested table renderings as well, but unlike theDataList, it is up to you to decide which cell contains the nested tables by making that column a template column and adding a nestedDataGridas part of the template column's definition. A sample rendering of ourDataSetpopulated with the Northwind data using three levels of hierarchical data binding to aDataGridis shown (one top-level row only) below.

Aa478959.aspn-hierdatabinding_08(en-us,MSDN.10).gif

Figure 8. DataSet populated with Northwind data

Limitations and Efficiency

It is important to keep in mind that the data binding mechanism in ASP.NET was designed for binding to flat data sources, and although it can be used to bind hierarchically, it may not always be the best choice for rendering data that is truly hierarchical in nature. The data source must be regular in shape—it is not feasible to bind to a data source that is 2 levels deep in some places and 4 or 5 levels deep in others. XSL is much better suited to perform rendering of data with arbitrary hierarchical shape, so converting your data to XML and using XSL transforms with the ASP Xml control is probably your best option.

You may have noticed that all of the samples in this article were careful to set theEnableViewStateflag in each data bound control to false.ViewStateis used to store state on behalf of controls between POST requests to the same page. By default, all server side controls in ASP.NET retain all of their state between POST requests, which is convenient because you can rely on state retention for all of your controls. It can also add significantly to the rendering size of your pages, however, and if you are not taking advantage of the state retention you should take steps to turnViewStateoff for those controls. This technique is particularly susceptible toViewStatebloat because the entire content of each data-bound control and all of its child controls is stored inViewStateby default. In most cases, the contents of thisViewStateis not used at all because the data-bound controls are re-bound to their data sources with each request, whether it's the first GET request to a page or a subsequent POST back to the same page. So, unless you have a good reason to do otherwise, I recommend that you setEnableViewStateto false in all of your data-bound controls, especially if you are using the hierarchical data binding techniques outlined in this article. It is fine to leaveViewStateenabled for nested server-side controls within anItemTemplateof a data-bound control, as we did for theCheckBoxcontrols nested within ourRepeaterexample, just remember to disable it on the actual data-bound control itself. As an example of how dramatic this can be, if you enable view state on all of theRepeatercontrols in our sample that bound to aDataSet, theViewStatefield grew to 250,000 characters. This is in contrast with the number of visible characters that rendered to the page was on the order of 100,000 characters.

Conclusion

This article presented a technique for binding to hierarchical data by nesting templated data-bound controls and dynamically assigning their data sources. This technique can be quite useful for rendering regular, structured hierarchical data such as that found in table relations within a database. It is also possible to render other hierarchical data sources using this technique, but it is cumbersome to use if the data is not regular in all dimensions. Alternative techniques using XSL with XML data sources are typically more concise and give you more control over the rendering.

© Microsoft Corporation. All rights reserved.