Cutting Edge

Nested Grids for Hierarchical Data

Dino Esposito

Code download available at:CuttingEdge0310.exe(135 KB)

Contents

Building Nested Grids
The NestedGrid Class
The ExpandCommandColumn Class
Rendering the Child Grid
Creating the Child View
Conclusion

In the August 2003 installment of Cutting Edge, I discussed how to extend the ASP.NET DataGrid server control to use a multi-table data container such as a DataSet object as its data source. If the DataSet contains pairs of interrelated tables, the control adds a dynamically created button column whenever the displayed table is the parent of one of these relations. When the column button is clicked, a child DataGrid is displayed, listing the child rows of the selected record according to the relation. The overall behavior is illustrated in Figure 1 and is similar to how the Windows® Forms DataGrid control works in similar situations.

Figure 1 Parent and Child DataGrids

Figure 1** Parent and Child DataGrids **

The application shown in Figure 1 is a user control that comprises two DataGrid controls working together. The user control (see the August 2003 source code) contains all of the logic necessary to keep the two grids in sync. The parent DataGrid is bound to a DataSet and displays the contents of the parent table. When this happens, the user control ensures that a relation exists within the DataSet where the displayed table acts as the parent. The child DataGrid is bound to a view of the data that includes all the records in the child table which are only related to the selected record. As a result, if you have a DataSet with two tables whose relations have been set, the user control saves you from coding any extra display mechanism.

So what's wrong with this approach? Nothing, if you're just looking at basic functionality. Some readers, however, noted that they would rather not have two physically separate DataGrid controls. The user control builds a wall around the constituent controls, so the only way you can access them is by either mirroring properties and methods or by exposing the internal control as a whole. From the standpoint of programmability, having a single DataGrid control to display hierarchical data would make life significantly simpler. For one thing, you wouldn't have to worry about the configuration of the parent table. You could just use the standard interface of the DataGrid control. Any child grids that display related data can be created dynamically and displayed within the layout of the main grid.

Figure 2 Embedded Child DataGrid

Figure 2** Embedded Child DataGrid **

On the other hand, remember that the DataGrid control is not designed to contain hierarchical data. Its internal layout is optimized for tabular data. The DataList control might be a good choice, but it doesn't provide native paging support and it requires a bit of code to work like a DataGrid. A quick search on Google for "nested DataGrid" returned links to articles discussing how to embed DataGrids in a DataList control, which gave me some ideas for this column. Here, I'll build a custom control that inherits from the DataGrid class. The control implements a custom column type (ExpandCommandColumn) and contains all the logic needed to display records associated with the clicked item. The expanded view is represented by a child DataGrid embedded in the parent. Figure 2 shows what it will look like.

Building Nested Grids

A hierarchical DataGrid control makes sense only if the data source is a DataSet object that contains relations between tables. For example, consider a DataSet with Customers and Orders tables with a DataRelation set between the two on the CustomerID column. As long as the DataGrid contains a button column, when you click it you could create a child view for the selected customer and bind the resulting DataView object to the child grid.

Because the new control (named NestedGrid in the sample code) inherits from the DataGrid class, you can use it whenever a DataGrid object is appropriate. However, take this last statement with a grain of salt. In general, when you derive a control from a base class there might be situations in which the derived control can't just replace the original due to its specific extensions and additions. In this column, I won't spend much time making the NestedGrid component backward compatible with the base DataGrid class. For simplicity, I'll assume that you always bind it to a DataSet object.

I make a few more assumptions regarding the NestedGrid control, which will become clear later on. In particular, you are responsible for adding a button column that will dictate the expanded/collapsed state of each row. In theory, this column can be placed anywhere in the grid. However, I assume here that the expand column is the first column in the grid. (As discussed in the August 2003 issue, you could modify the behavior to dynamically generate the column only if the DataGrid is bound to a DataSet that has related tables.)

If you've played with the DataGrid control a bit, you know that although it's extremely powerful and customizable, it doesn't happily support changes in its layout. The layout of a grid represents a table of data—a regular succession of equally sized rows. How can you embed a child grid with this restriction?

The key point to recall here is that the grid is rendered as a standard HTML table. Once the cells form a regular table layout, you can place anything within each of them, including a child table that represents a child grid (using a rowspan tag). First, you modify the number of cells that form the selected row—that is, the row where the expand command button that the user clicked is located—by removing all the cells except the one that contains the command button. This is pretty easy to do if you assume that the expand command column is the leftmost one. Once all the cells have been removed, then you can create a brand new one that spans the necessary number of columns, equal to the number of items in the Columns collection of the DataGrid control.

At this point, you have a completely custom cell for the row to expand. This custom cell can be programmatically populated with any combination of server controls. For example, you can insert a table in which the top row mimics the structure of the removed cells (typically, information about the parent row) and the bottom row contains the child DataGrid. The control in Figure 2 is created based on this scheme.

The NestedGrid Class

As I mentioned earlier, the NestedGrid class inherits from the System.Web.DataGrid class and adds a few extra properties (see Figure 3). The control also raises the custom UpdateView event whenever data binding is required. To exercise strict control over the object type assigned to the DataSource property (and ensure it is a DataSet), you can override the DataSource property, as shown in the following code:

public override object DataSource { get {return base.DataSource;} set { if (!(value is DataSet)) { // throw an exception } base.DataSource = value; } }

Figure 3 NestedGrid Properties

Property Description
ExpandedItemIndex Gets or sets the zero-based index of the item currently selected. (The control supports only one item expanded at a time.)
RelationName Gets or sets the name of the relation within the bound DataSet in which the DataMember table is the parent. This relation determines the contents of the child grid.
ScrollChildren Indicates whether the contents of the child grid should be scrollable. By default, the contents are pageable.

A NestedGrid control is instantiated when a user clicks on a row button to expand a record (a customer) to see its details (related orders). For this reason, a nested grid must contain a button column with some specific features. For one thing, the grid must provide a handler for the ItemCommand event to handle the expand/collapse request. The handler sets the ExpandedItemIndex property to the zero-based index of the record that was clicked and updates the grid's view. So when is a good time to modify the layout of the clicked row?

The ItemDataBound event fires at the bottom of the chain of events that occur when the grid layout is created. After ItemDataBound fires, the data binding phase is almost complete and all the cells are ready for display. The layout and data you see from this point will not change any further. For exactly this reason, I decided to implement any needed changes before handling the ItemDataBound event.

Before going deep into the implementation of the control, let me share a few more notes. First, the ExpandedItemIndex property is zero-based but represents the absolute position of the clicked row. This property differs from similar grid properties (such as SelectedItemIndex and EditItemIndex) only in that it doesn't represent a page-based value. Second, the NestedGrid also implements paging internally. For the control to move through pages of the member table, you don't have to do anything but handle the UpdateView event and pass the binding data:

void UpdateView(object sender, EventArgs e) { BindData(); } void BindData() { dataGrid.DataSource = (DataSet) Cache["MyData"]; dataGrid.DataBind(); }

The NestedGrid class has a built-in handler for the PageIndexChanged event, as shown here:

void PageIndexChanged(object sender, DataGridPageChangedEventArgs e) { CurrentPageIndex = e.NewPageIndex; SelectedIndex = -1; EditItemIndex = -1; ExpandedItemIndex = -1; if (UpdateView != null) UpdateView(this, EventArgs.Empty); }

The key element in the architecture of the NestedGrid control is the button column. For the sake of simplicity, this version of the control supports only a single expanded item. This feature can be easily extended by changing the ExpandedItemIndex property from an integer to an array or collection.

The ExpandCommandColumn Class

The expand column can be rendered using either a string (such as "+/-", or "Expand/Collapse") or a bitmap. You might want to use different pictures for different applications. The most flexible way to implement this feature is to use a couple of properties such as ExpandText and CollapseText. So, should you define these properties on the NestedGrid class? In a similar scenario—in-place editing—the ASP.NET team created a custom DataGrid column and placed properties such as EditText, CancelText, and UpdateText on it. Based on this, I created my own ExpandCommandColumn class with a couple of text properties representing the HTML output for expanding and collapsing the view. The following code snippet shows how to integrate the custom column with the grid.

<cc1:NestedDataGrid id="dataGrid" runat="server" ...> <Columns> <cc1:ExpandCommandColumn CollapseText="<img src=images/collapse.gif>" ExpandText="<img src=images/expand.gif>" > <ItemStyle Width="15px" /> </cc1:ExpandCommandColumn> ••• </Columns> </cc1:NestedDataGrid>

Building a custom DataGrid column is not difficult. It doesn't require much work beyond creating a new class that inherits from DataGridColumn. In the new class, you must code any extra properties you need and override the InitializeCell method. This method is called whenever a cell is being created for the column. Everything that appears by default within a column cell is dictated by this method. The following code demonstrates the implementation of the ExpandText property:

public class ExpandCommandColumn : DataGridColumn { public string ExpandText { get { object data = ViewState["ExpandText"]; if (data != null) return (string) data; return "+"; } set { ViewState["ExpandText"] = value; } } ••• }

The CollapseText property differs only in the name of the viewstate slot and its default value ("-").

It is worth noticing that the default value of server control properties should be set in the get accessor instead of in the constructor or in an initialization event such as Init or Load. This is a practice that Microsoft used throughout ASP.NET. By insulating such code inside the get accessor of the property you gain code encapsulation and a neater separation between the logic behind the value of a property and the rest of the control. Especially when the default value of a property is subject to sophisticated rules, you have a single point of control, which makes the overall code more maintainable. Speaking of best practices, bear in mind that the value returned for a property must be checked for null and normalized if needed. For example, a property of type string should never return null; it should return an empty string instead.

A DataGrid column is centered around the InitializeCell method. The method is declared public and virtual (that is, it can be overridden in derived classes) and the code within the DataGrid control calls it whenever the column needs to be rendered. Despite being declared as public, the method is usually used only by control developers. Let's have a look at the signature:

public override void InitializeCell( TableCell cell, int columnIndex, ListItemType itemType)

The DataGrid code calls the method, passing the object that represents the cell being created, the index of the column in the grid's Columns collection, and the type of the cell to be rendered (header, footer, item, and so on). As you can see, there's no information about the index of the cell in the grid's page. Is this information really important? Looking at the predefined types of grid columns, the answer would seem to be no. (In fact, this information is not passed along.) The predefined grid columns (bound, button, hyperlink, template columns) use one of two algorithms to populate cells. If their Text properties are set, all the cells contain a fixed and constant value; otherwise, if a data-bound property (like the DataField) is set, the contents of each cell is resolved through a data binding process.

So which category does the ExpandCommandColumn type fall into? Well, neither. Ideally, this type will render the cell text using ExpandText or CollapseText, depending on the state of the item being rendered. If the cell index matches the ExpandedItemIndex property (or belongs to the collection of expanded items), the CollapseText value is used; otherwise, the default ExpandText property is employed. How can the column's InitializeCell method know about the cell index?

The first idea that crossed my mind was to take the TableCell object that gets passed to the method, invoke its NamingContainer property, and cast the results to DataGridItem. If the object obtained is not null, that would be the container of the cell and its ItemIndex property would contain the required information. Unfortunately, things are not that easy. The naming container of the cell object is null because when InitializeCell is called, the TableCell object has not yet been added to the grid item container. Consequently, it doesn't belong to any parent container, so the NamingContainer property returns null.

To find a workaround, I pored over the list of internal overridable methods of the DataGrid control. The DataGrid's InitializeItem method proved to be the one I wanted. This method is responsible for initializing the grid columns when the grid layout is being created. The InitializeItem method is mentioned in the ASP.NET documentation on MSDN®, but not fully described. As of ASP.NET 1.x, its behavior is fairly simple. InitializeItem takes two parameters: the DataGridItem object that represents the grid row that's being rendered and an array of DataGridColumn objects (the columns of the row):

protected virtual void InitializeItem( DataGridItem item, DataGridColumn[] columns );

The InitializeItem method loops over the columns and creates a new TableCell object for each one. This object is passed to the column-specific InitializeCell method and then added to the Cells collection of the DataGridItem object. (As you can guess, it's only at this point that the TableCell's naming container is set to a non-null value.) The code in Figure 4 shows the overridden version of InitializeItem that passes an extra flag down to the ExpandCommandColumn column class.

Figure 4 Passing a Flag to the Column Class

public class NestedGrid : DataGrid { ••• // DataGrid::InitializeItem (overridden) // If the column is of type ExpandCommandColumn, this calls an // overloaded version of the DataGridColumn::InitializeCell // method. The extra information passed on indicates // whether the column cell is to be rendered // as expanded or collapsed. protected override void InitializeItem(DataGridItem item, DataGridColumn[] columns) { for (int i=0; i<columns.Length; i++) { TableCell cell = new TableCell(); // Check the column type if (columns[i] is ExpandCommandColumn) { // Grab the column object ExpandCommandColumn col = (ExpandCommandColumn) columns[i]; // Determine if the cell is expanded/collapsed bool expanded = (item.ItemIndex==(ExpandedItemIndex % this.PageSize)); // Call the overloaded method (see ExpandCommandColumn.cs) col.InitializeCell(cell, i, item.ItemType, expanded); } else columns[i].InitializeCell(cell, i, item.ItemType); // Add the newly configured cell to the grid item item.Cells.Add(cell); } } ••• }

Figure 5 contains the code that is used to initialize the cells of the ExpandCommandColumn class. Each cell is rendered as a link button with the text determined by the extra Boolean argument (see Figure 6). As with many other elements of the DataGrid control, HTML text is fully supported, so you can use images to implement the expand/collapse feature.

Figure 5 Initializing the ExpandCommandColumn Class

public class ExpandCommandColumn: DataGridColumn { ••• // Initializes the cells in the column public void InitializeCell(TableCell cell, int columnIndex, ListItemType itemType, bool expanded) { // Call the base method base.InitializeCell(cell, columnIndex, itemType); // Determine how to populate the cell if (itemType == ListItemType.Item || itemType == ListItemType.AlternatingItem) { // Create a link button LinkButton link = new LinkButton(); // Set the command name link.CommandName = "Expand"; // Determine the command text to show if (expanded) link.Text = this.CollapseText; else link.Text = this.ExpandText; // Add the button to the Controls collection of the cell link.CausesValidation = false; cell.Controls.Add(link); } } ••• }

Figure 6 Cell as Link Button

Figure 6** Cell as Link Button **

What happens when the column's link button is clicked? If you need a column-specific behavior, then add a handler for the Click event. That code will be the first to execute after the click. Next, the event will bubble up by way of the DataGridItem class, and will result in an ItemCommand event at the DataGrid level.

Rendering the Child Grid

Although highly customizable, the DataGrid control doesn't provide facilities to modify the HTML layout of the rows. The DataGrid customization logic is built around the idea that the grid is made of columns, and rows are simply the result of columns next to each other. But here you need to modify the structure of the selected row to encompass a child DataGrid control. There are only two places in the process where the grid's layout can be altered—the ItemCreated event or, better yet, the ItemDataBound event. The ItemDataBound event fires a little later in the grid item's lifetime and is the last event you see before a new row is added to the final HTML table.

When the user clicks one of the link buttons in the command column, the ItemCommand event bubbles up to the grid. If the command name equals Expand—the command name of the column's buttons—the code first checks to see whether the index of the clicked item matches the ExpandedItemIndex property. If so, the user has clicked on an already expanded item which will then be collapsed. Figure 7 shows the code that implements this mechanism. As mentioned earlier, the ExpandedItemIndex property is an absolute index ranging from 0 to the number of items in the data source. That's why you need to compare its modulus against the item's index while composing the grid's page.

Figure 7 Expand/Collapse Mechanism

// Command handler private void NestedDataGrid_ItemCommand(object source, DataGridCommandEventArgs e) { if (e.CommandName != "Expand") return; ExpandItem(e.Item); } // Adjust the index of the expanded item private void ExpandItem(DataGridItem item) { if (item.ItemIndex == (ExpandedItemIndex % this.PageSize)) SetExpandedItem(item, false); else SetExpandedItem(item, true); OnUpdateView(); } // Adjust the index of the expanded item private void SetExpandedItem(DataGridItem item, bool expand) { // ExpandedItemIndex is an absolute index // (take care of the current page index and page size) if (expand) ExpandedItemIndex = (this.PageSize*this.CurrentPageIndex+item.ItemIndex); else ExpandedItemIndex = -1; }

The final step in the code shown in Figure 7 fires the UpdateView event. For the NestedGrid control, this event represents the entry point to the process of UI rendering. The client is expected to handle the event, bind any necessary data, and finally call into the grid's DataBind method. At this point, the control's lifetime is a succession of events, the first of which is DataBinding. Next, ItemCreated and ItemDataBound are fired for each item in the grid, including the header, data rows, and footer. At this stage, InitializeItem is invoked to populate the cells of each bound column.

In Figure 2, you can see that the grid makes room for another embedded grid which shows the child rows of the expanded record. Assuming that the expand column is the leftmost one, you remove all subsequent cells and replace them with a new cell in which the RowSpan attribute is properly set. You can determine the contents of this new cell, but it should contain at least the following information: the removed cells (that is, information about the record you're going to expand) and the child grid. That a handful of cells are to be removed and added again later may sound bizarre, but this compromise is necessary to wed two contrasting requirements: inserting a child table while preserving the rest of the table layout.

In my implementation, I cache text and the width of each cell that is being removed. As an alternative approach, you can consider moving the TableCell object from one Cells collection to another (see Figure 8). The new cell contains a two-row table in which the first row repeats the original cells and the second row spans the width to display a child DataGrid.

Figure 8 Moving the TableCell Object

// Modify the layout of the cell being expanded private void BuildChildLayout(DataGridItem item) { DataGridItem row = item; // Assumes the Expand column is the first // Remove all cells but one int cellsToSpanOver = row.Cells.Count-1; ArrayList listOfText = new ArrayList(); ArrayList listOfWidth = new ArrayList(); for (int i=row.Cells.Count-1; i>0; i—) { listOfText.Add(row.Cells[i].Text); if (i==1) // Add the width of the column whose width is not declared listOfWidth.Add(HostColumnWidth); else listOfWidth.Add(this.Columns[i].ItemStyle.Width); row.Cells.RemoveAt(i); } // Add the new cell that will host the child grid TableCell newCell = new TableCell(); newCell.ColumnSpan = cellsToSpanOver; newCell.BackColor = Color.SkyBlue; // MUST BE empty. If you set a fixed width declaratively, that value // will override this one. For this reason, we set the width of the // first column after the EXPAND column dynamically. We also assume // that the first column after the EXPAND column is the host cell, // where the child grid is inserted. newCell.Width = Unit.Empty; row.Cells.Add(newCell); // The child layout is made of a 2-row table: header (same as the // previous unexpanded row) and the subgrid Table t = new Table(); t.Font.Name = this.Font.Name; t.Font.Size = this.Font.Size; t.CellSpacing = this.CellSpacing; t.CellPadding = this.CellSpacing; t.BorderWidth = this.BorderWidth; TableRow rowHeader = new TableRow(); t.Rows.Add(rowHeader); TableRow rowSubGrid = new TableRow(); t.Rows.Add(rowSubGrid); newCell.Controls.Add(t); // Fill the header row for (int i=listOfText.Count-1; i>=0; i—) { TableCell c = new TableCell(); c.Text = listOfText[i].ToString(); c.Width = (Unit) listOfWidth[i]; rowHeader.Cells.Add(c); } // Fill the second row Panel outerPanel = null; if (ScrollChildren) { outerPanel = new Panel(); outerPanel.Height = Unit.Pixel(100); outerPanel.Style["overflow"] = "auto"; } TableCell cellSubGrid = new TableCell(); cellSubGrid.ColumnSpan = cellsToSpanOver; cellSubGrid.BackColor = Color.LightCyan; rowSubGrid.Cells.Add(cellSubGrid); detailsGrid = new DataGrid(); detailsGrid.ID = "detailsGrid"; detailsGrid.BackColor = Color.LightCyan; detailsGrid.Font.Name = this.Font.Name; detailsGrid.Font.Size = this.Font.Size; detailsGrid.HeaderStyle.Font.Bold = true; detailsGrid.Width = Unit.Percentage(100); if (!ScrollChildren) { detailsGrid.AllowPaging = true; detailsGrid.PageSize = 5; detailsGrid.PageIndexChanged += new DataGridPageChangedEventHandler(detailsGrid_PageIndexChanged); } BindDetails(detailsGrid); if (ScrollChildren) { outerPanel.Controls.Add(detailsGrid); cellSubGrid.Controls.Add(outerPanel); } else { cellSubGrid.Controls.Add(detailsGrid); } }

When I first tested this code, it caused trouble. Before coding to ASP.NET, I verified my idea using plain HTML. I was so sure that a layout such as the one described previously made sense, I coded it within the ItemDataBound event of an ASP.NET DataGrid. To my great surprise, it did not span properly. It took me a while to figure out what was going on. The rub lies in the fact that I assigned an explicit width in pixels to each column, including the first column after the expand column:

<asp:boundcolumn runat="server" headertext="ID" datafield="ID" itemstyle-width="150px" />

Consider now that the new cell—the one destined to contain the child grid—is still the first after the expand column and as such inherits the width in pixels of the original ID column. To have this cell spanned over the available space, the Width property shouldn't be set; it should be left empty. No matter what you do in code (see the ItemDataBound event handler), the DataGrid internal framework always generates a style attribute in which the original width in pixels is retained. If you look at the source HTML for the cell, you see a similar design:

style="width:150px;...;width='';"

The width property is set twice (the second assignment is what you made in ItemDataBound), but the first value is the only one that matters for the browser. For this issue, I haven't found a better workaround than dropping the static width assignment. If the column needs to have a width, you can define a custom property (called HostColumnWidth in the sample) representing the width in units of the host column—the first column after the expand column. The following code snippet shows how to dynamically set the width of a column, obtaining an effect that is equivalent to setting the item's style:

if (e.Item.ItemIndex != (ExpandedItem % this.PageSize)) { // Equivalent to setting itemstyle-width declaratively e.Item.Cells[1].Width = HostColumnWidth; return; }

Creating the Child View

At this point, the end of the game is fast approaching and there is one more thing left to do—populate the child DataGrid. The way you accomplish this depends on the internal layout of the hierarchical data you're managing. However, assuming that you hold your multi-level data in an ADO.NET DataSet with pairs of related tables, using child DataView objects is a viable option. (This approach is similar to what I discussed in my August 2003 column.)

The user selects the parent table to show in the grid and the relation that determines child views. A child view is created by calling the CreateChildView method on the DataRowView object that represents the parent record:

DataTable dt = ds.Tables[this.DataMember]; DataView theView = new DataView(dt); DataRowView drv = theView[ExpandedItemIndex]; DataView detailsView = drv.CreateChildView(this.RelationName);

The set of records associated with the expanded row are grouped in a new DataView object. For example, given a customer-to-orders relationship, the set of child records are the orders issued by a given customer. The child grid is created and configured dynamically, as shown here:

detailsGrid = new DataGrid(); detailsGrid.ID = "detailsGrid"; detailsGrid.Font.Name = this.Font.Name; detailsGrid.Font.Size = this.Font.Size; detailsGrid.Width = Unit.Percentage(100); detailsGrid.AllowPaging = true; detailsGrid.PageSize = 5; detailsGrid.PageIndexChanged += new DataGridPageChangedEventHandler( detailsGrid_PageIndexChanged); BindDetails(detailsGrid);

In particular, the child grid manages paging internally. It is given a built-in handler for the PageIndexChanged event, which automatically moves the grid to the next page as the user clicks on the pager. No further code is required on the programmer's end for this feature to work.

Any event that is generated within the embedded grid is not visible outside the outermost grid. Among other things, this means that no user code could ever be written to handle the clicking on the child grid's pager bar. Is there a way to work around this structural limitation? One possibility is that you fire a new event from within the internal handler.

If you don't like paging the child grid, you can use a scrollbar and wrap the grid into a scrollable panel. In HTML 4.0, the overflow CSS attribute transforms a bunch of HTML elements into scrollable areas if the contents exceed the fixed dimensions. To make the child grid scrollable, you simply need to wrap it in a Panel (corresponding to a <div> tag) and give the panel the overflow attribute (see Figure 9). Now the header of the grid scrolls with the rest of the control. This behavior is by design and requires quite a sophisticated trick, which I will not take the time to discuss in this column.

Figure 9 Making a Scrollable Child Grid

Panel outerPanel = null; if (ScrollChildren) { outerPanel = new Panel(); outerPanel.Height = Unit.Pixel(100); outerPanel.Style["overflow"] = "auto"; } // Build the grid if (ScrollChildren) { outerPanel.Controls.Add(detailsGrid); cellSubGrid.Controls.Add(outerPanel); } else cellSubGrid.Controls.Add(detailsGrid);

Conclusion

The ADO.NET DataSet object works like an in-memory database with a collection of tables and relations and allows you to create a hierarchical representation of data. You could render such data through a grid by using a combination of iterative and data-bound controls (such as DataList, Repeater, or Label) to easily simulate relationships. The downside of this approach is that you have to explicitly write code to perform paging. The DataGrid provides a lot of interesting features but doesn't supply any facility to render hierarchical, multi-table data. In the August 2003 Cutting Edge column, I discussed one solution based on a user control. In this column, I topped off the discussion by extending the DataGrid control itself through inheritance.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Programming ASP.NET and Applied XML Programming for .NET, both from Microsoft Press, he spends most of his time teaching classes on ADO.NET and ASP.NET and speaking at conferences. Get in touch with him at dinoe@wintellect.com.