Export (0) Print
Expand All

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 called DataSource and a method called DataBind(), and that the data source to which the control is bound implement the IEnumerable interface.

Note   There are two notable exceptions to this requirement—the DataSet, DataTable can both be bound to directly, which results in binding to the default DataView of the default table (DataView does implement IEnumerable). This is for convenience as DataSets and 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 its DataBind() method.

For example, consider the following source of data, which returns an ArrayList full of instances of the Item class:

  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 the ArrayList implements IEnumerable, the result of our GetData() method in the TestDataSource class is a valid data source for binding. We will use the Repeater as the server-side control to which we will bind the data, which requires that we provide an ItemTemplate describing how we would like each item in the enumerable data source rendered. Our sample renders a CheckBox control with its text set to the Name property 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 our Repeater, which is typically done in the Load handler of the Page-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 the ItemTemplate of the Repeater that we already have, and bind it to the SubItems collection of each Item that is enumerated by the top-level Repeater. This can be achieved declaratively in our .aspx file by adding a nested Repeater. The only tricky part is to correctly map the SubItems collection of the Item currently being bound to the DataSource property of the nested Repeater. This is done by declaratively setting the DataSource property of the nested Repeater to 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-level Repeater. The nested data bind will happen once per Item in 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-level Repeater and 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 the SubItems collection of the current Item being 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 of SubItems.

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 whose DataSource property is bound to the SubItems property of the Item currently being enumerated by the second-level Repeater.

<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 the Customers table and the Orders table. Similarly, there is a one-to-many relationship between the Orders table and the Order Details table. 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 following Load handler does this, and then binds the resulting DataSet to 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 the DataSet, it can now be navigated hierarchically by using the relations that we established. For example, given any row in the Customers table inside our DataSet, we can call GetChildRows() 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 the Order Detail entries associated with any given order by calling GetChildRows with the string "Order_OrderDetail" on a row from the Orders table to retrieve all Order Detail entries associated with that order. Even more useful, for our purposes, is the CreateChildView method of the DataRowView class, which returns a DataView displaying 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-level Repeater to bind to the Customers table in our DataSet, one nested Repeater to bind to all of the Orders associated with each customer, and another nested Repeater to bind to all of the Order Detail entries associated with each order. The DataSource for the two nested Repeaters will be the result of calling CreateChildView on the parent row, with the appropriate relation name. Instead of trying to create the DataView in a single expression in the Repeater declaration, 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 the DataView.

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 the Repeater control 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.

The XmlDocument class 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 in XmlDocument is XmlNode, which represents an element in the document. Fortunately for us, the XmlNode class implements IEnumerable to return an enumerator over its children, which means we can use any XmlNode as a data source in data binding. It turns out that XmlDocument also derives from XmlNode, 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 an XmlDocument class in our page's Load handler and bind the top-level publishers element 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 nested Repeaters to 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 three Repeater controls, with the authors Repeater nested within the publishers Repeater, and the titles Repeater nested within the authors Repeater. 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 an XmlNode, which implements an enumerator over its children. Also notice that in order to extract data from the current data item we had to cast the Container.DataItem to XmlNode and extract (in this case) its attributes. The usually convenient DataBinder.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 a Repeater to 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 a Label. 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, our OnCheckedItem handler 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 the Repeater control for simplicity. It is possible, however, to perform hierarchical data binds with both the DataList as well as the DataGrid controls 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 a Repeater with a nested DataList, for example. The code samples for this article include identical samples to the ones presented using the Repeater for both the DataList and DataGrid classes.

The DataList class renders a table where each cell in the table is a rendering of a row in the result set. Using the DataList to 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 our DataSet populated with the Northwind data using three levels of hierarchical data binding to a DataList is shown (one top-level cell only) below.

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

Figure 7. DataSet populated with Northwind data

The DataGrid class renders a table where each row in the table is a rendering of a row in the result set. Using the DataGrid to bind hierarchically results in nested table renderings as well, but unlike the DataList, it is up to you to decide which cell contains the nested tables by making that column a template column and adding a nested DataGrid as part of the template column's definition. A sample rendering of our DataSet populated with the Northwind data using three levels of hierarchical data binding to a DataGrid is 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 the EnableViewState flag in each data bound control to false. ViewState is 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 turn ViewState off for those controls. This technique is particularly susceptible to ViewState bloat because the entire content of each data-bound control and all of its child controls is stored in ViewState by default. In most cases, the contents of this ViewState is 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 set EnableViewState to 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 leave ViewState enabled for nested server-side controls within an ItemTemplate of a data-bound control, as we did for the CheckBox controls nested within our Repeater example, 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 the Repeater controls in our sample that bound to a DataSet, the ViewState field 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.

Show:
© 2014 Microsoft