devxlogo

Nested DataGrids Simplify Hierarchical Data Display

Nested DataGrids Simplify Hierarchical Data Display

uppose you are asked to write an ASP.NET search page that performs a database search and displays the results?employee names, recent project assignments for each, and hours billed per week per assignment. Would you approach the task by dynamically constructing HTML tables in a code-behind page using code such as Label.Text = “

” + ? + “

“? Or would you instead create table Web controls dynamically at run-time? Either way, don’t do it! There’s a better approach. Nesting multi-record controls such as DataGrids, GridViews, DataRepeaters, or DataLists is not as hard as you might think and offers numerous benefits compared to more traditional approaches.

Using these techniques does require a solid understand of ASP.NET binding techniques such as the data-binding syntax, the hierarchy of data-bound Web controls, and the event model for Web controls. This article reviews these concepts in the context of nested, data-bound, multi-record Web controls and examines a fully-commented, downloadable Northwind solution that accompanies this article (see Figure 1).

?
Figure 1. Orders Advanced Search Screenshot: Three nested loops in the sample application display orders, products, and historical order counts based on the dates users enter into the Date Range fields.

DataSource, DataBinding, DataSets, Oh My!
If you find yourself confused by.NET terminology?where it sometimes seems as if every word starts with “Data”?you aren’t alone. This section attempts to clarify and differentiate the terms while also providing you with a method to approach the problem of displaying hierarchical data.

Early Data Equals Better Data
A na?ve approach to displaying hierarchical data would be to retrieve the data just before you need it in the presentation tier, for example:

   get data from database   for each row {       get more data from database based on the current row       for each new row {           create controls or Response.Write()       }   }

Opening database connections inside of loops as in the preceding code isn’t always a mistake, but it’s rarely the best approach. Furthermore, the preceding code combines display elements and looping logic in the same place. In contrast, retrieving the data for all the loops before dealing with the UI provides two attractive benefits. First, it can lead to significantly better performance because it avoids the overhead of opening database connections inside loops. Second, it lets you separate data retrieval code and put it in a common place?such as a middle tier.

To Normalize or Not to Normalize
After you decide to retrieve all data prior to displaying it you face another question: how do you store it all? The problem actually breaks down into two questions. First, what physical data structure do you use? Second, how do you hold hierarchical data? XML, custom classes, datasets, and strongly typed datasets are all options for the first question. There are pros and cons, but for the most part it comes down to personal preference. This article uses strongly typed datasets.

Figure 2 summarizes the second question.

?
Figure 2. Normalized vs. Denormalized Data Structures: You can hold hierarchical data in either normalized form as related tables, or in denormalized form, with all the data in a single table.

The first illustration in Figure 2 shows the normalized approach for the Orders search page and contains four tables. It is based upon the needs of the UI layer, for instance note that customer name was actually denormalized into the orders table. The second illustration shows the denormalized approach and contains a single table with duplicate order and product information.

The denormalized approach requires looping through all rows and trying to spot when data from an outer loop changes. In pseudo code:

   retrieve denormalized data   sort data so you can tell when outer data changed   for each row {      if this a new product i.e. OldProductId != NewProductId          {           write out a table header with product information      }      write a nested row from inner data (ProductMonthCount)   }   

Not pretty. This approach presents a big disadvantage in that it still doesn’t allow the wonderful benefits of non-dynamically created Web controls?namely, the separation of display from logic.

What’s So Difficult About Nesting?
Non-nested web controls are easy. You have two options. Either, you set your control’s DataSource property and call the DataBind() method in the code (this is the most common approach), or you expose your data source as a protected property in the code and use the data-binding syntax in the page:

   protected DataTable Orders = GetDataSet().Tables[0];      

While the data-binding syntax approach works fine with nested controls, the DataBind() approach doesn’t. The problem is that is that the code behind page cannot easily access a control that is nested inside another, especially for multi-record controls.

Data Binding Syntax and Control Hierarchy
The data-binding syntax can be a little obscure, but it works well for 90 percent of hierarchical problems, so a thorough understanding is important. Suppose you list Orders in a parent Repeater control, then all the Products for that order inside of a nested Repeater control. The DataSource property for the inner repeater might look like:

   DataSource='<%#   ((DataRowView)Container.DataItem).Row.GetChildRows(   "Orders_Products") %>'
?
Figure 3. Hierarchy of Multi-Record Controls: The figure shows how the Container property of nested multi-row controls refers to an item in its parent multi-row control.

Or:

   DataSource='<%#   ((DataRow)Container.DataItem).GetChildRows(   "Orders_Products") %>'

Both code snippets bind to all “child rows” (the Products) for the current Order, which they retrieve from their “Container” controls. But what is a Container? And which snippet should you use?

When the .Net framework binds multi-record controls such as a DataRepeater to data, it creates child controls for each record. For DataGrids each child is a DataGridItem, for DataLists a DataListItem, and for Repeaters a RepeaterItem. Within nested controls the Container always refers to the next closest ControlItem (for lack of a better word). Thus, if you have two nested Repeaters and the inner Repeater uses Container, it references a RepeaterItem of the outer Repeater. You can think of ControlItems as the HTML rows that are created by multi-record controls and Container as referencing one of those rows. The diagram in Figure 3 shows a DataGrid with a nested Repeater. The DataGrid has been bound to a table with two orders (#1 and #2). The nested Repeater uses the data-binding syntax and the Container property to reference the current DataGridItem and ultimately retrieve products in the order.

When the logic reaches the ControlItem, you can reference its DataItem property, which represents the corresponding data record. When a control’s DataSource is a DataSet or a Typed DataSet, DataItem is either of type DataRow or DataRowView, (say that 10 times fast) and so you must cast it appropriately.

?
Figure 4. Data Relations: The relationship between tables lets you call GetChildRows() to retrieve child rows of a row in the parent table.

Unfortunately, it is hard to predict whether you should cast a DataItem to a DataRow or a DataRowView (as in the two code snippets shown earlier). As a guideline, DataItems tend to be DataRowViews in controls whose parent was bound in the code behind and DataRows in controls whose parent used the data-binding syntax. If you cast to a DataRowView, you need to take the additional step of retrieving the View’s Row. Once you have the DataRow you can get all related child rows (Orders in the example) with a call to GetChildRows(), which takes a relation name as a parameter (see Figure 4).

When you thoroughly understand this data-binding syntax, you can solve ninety percent of your hierarchical data problems. If you don’t, I highly recommend re-reading this section; understanding it will greatly enhance your ability to write useful solutions.

Incidentally, when using data-binding syntax remember to add this Imports statement at the top of your page.

   <%@ Import Namespace="System.Data" %>

If you omit the preceding Imports statement, you’ll get the exception: “The type or namespace name ‘DataRowView’ could not be found.”

Other Data-Binding Syntax Applications
Once you understand the hierarchy of nested controls, you’ll find numerous additional applications for nested DataGrids. For example, suppose you’re displaying orders and their products, but you don’t want to display the nested DataGrid if the order contains no products. The DataGrid’s Visible property is the obvious place to begin. You can combine it with data-binding syntax as follows:

      Visible='<%# ((DataRowView)Container.DataItem).Row.GetChildRows(       "Orders_Products").Length > 0 %>'   

This is nearly identical to the syntax used for the DataSource property, except that it returns True only if the length of the array returned by GetChildRows() is greater than zero?in other words, the Visible property is set to True only if there are one or more products for the current order.

The Parent of a Parent’s Parent Problem
Unfortunately understanding the data-binding syntax is not sufficient for all hierarchical problems. For example, suppose you want to sort the data in an innermost loop in an order other than the way the database returned it. The GetChildRows() method does not specify a sort order. As another example, consider an innermost loop that must call GetChildRows() on the data of a higher-level loop, for example, two loops up:

   DataSource='<%#     ((DataRowView)     ((RepeaterItem)Container.Parent.Parent.Parent.Parent)     .DataItem).Row.GetChildRows("Orders_Shippers")   

That's just messy! The appropriate approach for both of these problems is to go back to the code-behind approach and use the OnItemDataBound event instead. The OnItemDataBound event occurs for multi-record controls when the control creates a ControlItem and binds it to a record; therefore, the event is an excellent place to discover any nested controls, set their DataSource and call DataBind().

   protected void rptOuter_ItemDataBound(     object sender, RepeaterItemEventArgs e)    {     if (e.Item.ItemType == ListItemType.Item ||           e.Item.ItemType == ListItemType.AlternatingItem) {       Repeater rptInner =      (Repeater)e.Item.FindControl("rptInner");       rptInner.DataSource =         GetInnerDataSource(e.Item.DataItem)       rptInner.DataBind()
?
Figure 5. Northwind's Data Model: The sample orders advanced search sample primarily displays orders and order details.
} }

Notice the check for e.Item.ItemType. The .NET Framework calls the ItemDataBound event for headers and footers as well as ControlItems, so the if statement is very important.

This approach requires more code and is slightly more complicated than the data-binding syntax, but works in all data-binding scenarios, and is less error-prone because it can take advantage of compiler-provided type checking. One could make a case for using the code-behind approach exclusively, thereby choosing consistency and reliability over convenience. It would ultimately be a matter of personal preference.

Putting It All Together
The accompanying source code, the majority of which was generated by Blue Ink, a rapid application development tool, compiles to a fully working five-tier application with View/Add/Update/Search pages for all the tables in the Northwind sample database (you can review Northwind's data model in Figure 5). The Orders_SearchAdvanced.aspx page (not generated), is relevant to this article. It illustrates all the concepts mentioned thus far plus a few extra (such as searching using multi-value search criteria), within an enterprise-level, real world environment. You can try out this working version to get a better sense of how the application acts from the user point of view.

Finding Your Orders
The Orders Advanced Search page allows users to locate customers' previous orders and does double duty as an order history report. The search criteria page takes zero or more customer demographics, an optional customer, and a required date range. More relevant is what the page returns:

?
Figure 6. Orders Advanced Search Schema: This schema shows the tables and relationships returned by the Orders Advanced Search page.
  • Orders?including order date, customer and shipper.
  • Products related to each Order?including product name and quantity.
  • A count of products ordered for the current customer and current product, for each month in the date range.

The strongly typed dataset that holds the search results is shown in Figure 6. You can find the schema in the download that accompanies this article.

The normalized approach to holding data greatly simplifies displaying it hierarchically in the user interface. Figure 7 shows the loops in the page.

ProductMonthCounts?Where Things Get Interesting
There is a lot going on in the page with five separate multi-record controls nested three levels deep. First, each is bound using a different approach. The outermost loop for products is bound in the code-behind page; the nested loops are bound using data-binding syntax; and the innermost loop, for ProductMonthCounts, is bound in the ItemDataBound event of the nested OrderDetails (products) Repeater.

?
Figure 7. Loops In Search Results: The arrows show the inner and outer loops required to display the hierarchical data.

The innermost loop is the most complex. You have to bind it in the ItemDataBound event primarily because it requires a custom sort order. The custom sorting is required because the database doesn't return months where no products were sold; the middle tier inserts those records. Consequently, the order of records is incorrect in the dataset. The relevant section of code sorts by the FirstDayInMonth field and binds to records that match the current product and customer:

   string strCriteria = String.Format(     "ProductId={0} AND CustomerID='{1}'",      intProductId,     strCustomerID);   rptProductCountMonths.DataSource =      tdsSearchResults.ProductMonthCounts.Select(     strCriteria,      "FirstDayInMonth");   rptProductCountMonths.DataBind();

Another interesting aspect of the innermost loop is the code that dynamically sets the background color to a deeper shade depending on the number of orders sold.

    ? 

GetBgColorFromQuanity() takes a quantity of products sold, computes red, green, and blue values and returns an HTML color with a call to:

   System.Drawing.ColorTranslator.ToHtml(     System.Drawing.Color.FromArgb(intRed, intGreen, intBlue)     ); 

This is yet another application of the data-binding syntax.

Even if you don't need to display hierarchical data frequently, a thorough understanding of control hierarchy, data-binding syntax, and events of multi-record controls will empower your code and help you create more robust and maintainable solutions. At first, using nested controls may seem more complicated than the traditional approach of building HTML tables in code, but when you're asked to change your UI the day the application is due, you'll be glad you did.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist