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.
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
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 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 calledDataSource
and a method called DataBind()
, and that the data source to which the control is bound implement theIEnumerable
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 defaultDataView
of the default table (DataView
does implementIEnumerable
). This is for convenience asDataSets
andDataTable
s 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 anArrayList
full of instances of theItem
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 theArrayList
implements IEnumerable
, the result of ourGetData()
method in theTestDataSource
class is a valid data source for binding. We will use theRepeater
as the server-side control to which we will bind the data, which requires that we provide anItemTemplate
describing how we would like each item in the enumerable data source rendered. Our sample renders aCheckBox
control with its text set to theName
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 theLoad
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:
Figure 1. Data binding page
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 theItemTemplate
of theRepeater
that we already have, and bind it to theSubItems
collection of eachItem
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 theSubItems
collection of theItem
currently being bound to the DataSource
property of the nested Repeater
. This is done by declaratively setting theDataSource
property of the nestedRepeater
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/> <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 perItem
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-levelRepeater
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 theSubItems
collection of the currentItem
being bound. The rendering of this page is shown below:
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
whoseDataSource
property is bound to theSubItems
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/> <asp:CheckBox Runat="server" Text= '<%# DataBinder.Eval(Container.DataItem, "Name") %>'/> <asp:Repeater Runat="server" EnableViewState="false" DataSource= '<%# DataBinder.Eval(Container.DataItem, "SubItems") %>'> <ItemTemplate> <br/> <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:
Figure 3. Page showing additional nested Repeater
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 theCustomers
table and theOrders
table. Similarly, there is a one-to-many relationship between theOrders
table and theOrder Details
table. These relationships are shown in the figure below:
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 followingLoad
handler does this, and then binds the resultingDataSet
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 theCustomers
table inside our DataSet
, 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 Detail
entries associated with any given order by callingGetChildRows
with the string "Order_OrderDetail
" on a row from theOrders
table to retrieve allOrder Detail
entries associated with that order. Even more useful, for our purposes, is theCreateChildView
method of theDataRowView
class, which returns aDataView
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-levelRepeater
to bind to theCustomers
table in our DataSet
, one nestedRepeater
to bind to all of theOrders
associated with each customer, and another nestedRepeater
to bind to all of theOrder Detail
entries associated with each order. TheDataSource
for the two nestedRepeaters
will be the result of callingCreateChildView
on the parent row, with the appropriate relation name. Instead of trying to create theDataView
in a single expression in theRepeater
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 theRepeater
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") %> <%# DataBinder.Eval(Container.DataItem,"CompanyName") %> <br /> <asp:Repeater runat="server" EnableViewState="false" DataSource= '<%# GetChildRelation(Container.DataItem, "Customer_Order")%>' > <itemTemplate> Orderid:<b> <%#DataBinder.Eval(Container.DataItem, "OrderID")%> </b><br/> <asp:Repeater runat="server" EnableViewState="false" DataSource= '<%# GetChildRelation(Container.DataItem, "Order_OrderDetail")%>' > <itemTemplate> <b><%# DataBinder.Eval(Container.DataItem, "ProductName") %></b> $<%# DataBinder.Eval(Container.DataItem, "UnitPrice") %> <br/> </itemTemplate> </asp:Repeater> </itemTemplate> </asp:Repeater> </ItemTemplate> </asp:Repeater>
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.
TheXmlDocument
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 inXmlDocument
is XmlNode
, which represents an element in the document. Fortunately for us, theXmlNode
class implementsIEnumerable
to return an enumerator over its children, which means we can use anyXmlNode
as a data source in data binding. It turns out thatXmlDocument
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 anXmlDocument
class in our page'sLoad
handler and bind the top-levelpublishers
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 nestedRepeaters
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 threeRepeater
controls, with the authorsRepeater
nested within the publishers Repeater
, and the titlesRepeater
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> Author: <%# ((XmlNode)Container.DataItem) .Attributes["name"].Value %><br/> <asp:Repeater runat="server" EnableViewState="false" DataSource='<%# Container.DataItem %>' > <itemTemplate> <i> <%# ((XmlNode)Container.DataItem). Attributes["name"].Value %> </i><br /> </itemTemplate> </asp:Repeater> </itemTemplate> </asp:Repeater> <hr /> </itemTemplate> </asp:Repeater>
This renders as:
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 theContainer.DataItem
toXmlNode
and 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:
Figure 6. Rendering of animals.xsl
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 aRepeater
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/> <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/> <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, ourOnCheckedItem
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); }
All of the examples so far have focused on theRepeater
control for simplicity. It is possible, however, to perform hierarchical data binds with both theDataList
as well as theDataGrid
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 aRepeater
with a nested DataList
, for example. The code samples for this article include identical samples to the ones presented using theRepeater
for both theDataList
andDataGrid
classes.
TheDataList
class renders a table where each cell in the table is a rendering of a row in the result set. Using theDataList
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 ourDataSet
populated with the Northwind data using three levels of hierarchical data binding to aDataList
is shown (one top-level cell only) below.
Figure 7. DataSet populated with Northwind data
TheDataGrid
class renders a table where each row in the table is a rendering of a row in the result set. Using theDataGrid
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 nestedDataGrid
as part of the template column's definition. A sample rendering of ourDataSet
populated with the Northwind data using three levels of hierarchical data binding to aDataGrid
is shown (one top-level row only) below.
Figure 8. DataSet populated with Northwind data
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 theEnableViewState
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 turnViewState
off for those controls. This technique is particularly susceptible toViewState
bloat because the entire content of each data-bound control and all of its child controls is stored inViewState
by default. In most cases, the contents of thisViewState
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 setEnableViewState
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 leaveViewState
enabled for nested server-side controls within anItemTemplate
of a data-bound control, as we did for theCheckBox
controls nested within ourRepeater
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 theRepeater
controls in our sample that bound to a DataSet
, theViewState
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.
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.