MSDN Magazine > Issues and Downloads > 2002 > March >  Cutting Edge: ASP.NET Data Shaping
From the March 2002 issue of MSDN Magazine
MSDN Magazine
ASP.NET Data Shaping
Dino Esposito
Download the code for this article: Cutting0203.exe (47KB)
L ast month Johnny Papa and I each covered tools and tech-niques related to the presentation and manipulation of hierarchically shaped data. It was not a coincidence that we both discussed it from within the context of Windows® Forms applications. The Windows Forms DataGrid control, in particular, has several pretty cool features that make it suitable as the main component of the data entry module of your applications.
      In his column, Johnny demonstrated how a well-built middle tier between the DataGrid control and the physical data source is just about all you need to implement viewing and editing of data, even if the data is taken from several tables and assumes a hierarchical form. In contrast, I discussed how this sort of magic is performed (see February's Cutting Edge). It happens as the result of the data-binding implementation that is available in Windows Forms itself, through the form's BindingContext collection.
      Is it possible to port this model to Web Forms and get automatic synchronization between two DataGrid server controls? This is the question I will try to answer in this month's column.

Master/Detail on the Web

      Functionally speaking, the answer to my question can only be a resounding yes. From a technical point of view, though, you need a more thoughtful approach because of the stateless nature of the Web's underlying protocol—HTTP. Each time you select a new row in a list-bound control (DataGrid, DataList, or Repeater), a new round-trip occurs. The form is posted back to the Web server where the state of the form is restored, loading information back from the page's ViewState bag. As long as list-bound controls are involved—a common requirement for many applications manipulating hierarchical data—the data source is not automatically cached, nor is it persisted to the client as a part of the ViewState bag. So each time the page is posted back, the data has to be reloaded or recovered if cached somewhere.
      As I discussed in the November and December 2001 installments of Cutting Edge, this is an application-specific choice. As a result, neither list-bound controls nor the Page component itself can make any reasonable assumption about the data, neither for the actual object bound (DataTable, DataView, DataReader, array, collection), nor for the size of the data itself. In a Windows Forms scenario the application works on the client, and caching even a lengthy DataSet is normally a good option.
      Once the controls have been set up, you need no further round-trip or data access operations to extract the set of child rows bound to the currently selected parent row. On the Web, applications can download and configure the whole DataSet once and cache it. And if that's not sufficiently complex, applications can do this in a number of different ways. An application might decide to extract only the necessary parent and child rows during each step. On the Web, it's always an effective approach to make sure that each involved control does only what it is expected to do, and only when it cannot be delayed any further. Keep in mind that on the Web, more effective normally means more scalable.
      Web Forms have no direct counterpart to the BindingContext collection and do not provide ready-to-use tools to set up synchronization between two list-bound controls in a declarative fashion, as you have with Windows Forms. That said, don't forget that Web Forms feature an interesting and extremely flexible programming tool—the processor of data-binding expressions. By making extensive—and even a bit creative—use of data-binding expressions, the result can be Web pages in which two list-bound controls cooperate to provide synchronized views of hierarchical data. More importantly, to accomplish this you don't have to customize the boilerplate code.

Coding a Master/Detail Schema

      In past installments of Cutting Edge I thoroughly covered the DataGrid and other list-bound ASP.NET controls. In particular, in the May 2001 column I tackled the issue of selecting items in a DataGrid and a DataList control. If you have hierarchical data to display as parent and child rows, using two distinct list-bound controls connected to each other through code is definitely a good option. Moreover, it will likely be the first idea to come to mind. Although it's a bit boring if you have to code it repeatedly in different pages of your applications, using explicit code to connect bound controls in a master/detail fashion is the most flexible approach. Let's quickly review the way this option works.
      You start by placing two list-bound controls in the page. You can choose any pair that matches your page requirements from the set given by the DataGrid, DataList, and Repeater controls. Next, you must explicitly define the role of each. In other words, you determine which one will be acting as the parent (and subsequently showing rows from the parent table) and which one will be the child. DataGrid and DataList controls (but not the Repeater control) provide built-in support for the selection of a displayed row. So you should restrict the choice for the role of the parent table to these two controls.
      The extremely lazy programming interface of the Repeater control makes it particularly suitable for rendering the child table. Using the Repeater control makes it easy to apply a custom layout to each child row. This choice, though, may pose problems if you expect to have a large number of child rows, because the Repeater control, like the DataList, does not support pagination.
      Figure 1 shows the simplest way to render a master/detail relation in ASP.NET pages using two DataGrid controls. As you can see from the figure, the Web page contains two DataGrid server controls. The actual master grid is bound to the content of the Employees table in the SQL Server™ 2000 sample Northwind database.

Figure 1 Master/Detail in ASP
Figure 1 Master/Detail in ASP

      The details grid (shown in Figure 1, right below the master grid) is bound to a DataTable that is generated by an ad hoc procedure. Both grids are synchronized on the value of the employeeid field, which is read from the currently selected row of the master grid. The details grid shows the list of the territories covered by the selected employee, and will look like the following code:
<form runat="server">
    <asp:DataGrid id="masterGrid" runat="server"
        OnSelectedIndexChanged="SelectedIndexChanged" ... />
    <hr>
    <asp:DataGrid id="detailGrid" runat="server" ... />
    •••
</form>
      Figure 2 shows the source code necessary to refresh the child grid when the selection in the parent grid changes. You can use an ordinary SQL command to populate the parent grid. The command executes through a data adapter and returns a DataSet with just one table. The table is bound to the parent grid in the page.
masterGrid.DataSource = ds.Tables[0];
masterGrid.DataBind();
      To set up a visual dependency between the two grids you must intercept the event that's fired when a new row in the parent grid is selected. DataGrid controls support selection through an ad hoc style property called SelectionItemStyle and a couple of item properties called SelectedItem and SelectedIndex. SelectionItemStyle lets you define the look of the selected row; SelectedItem returns a DataGridItem object that represents the item currently being selected from the grid. The zero-based ordinal position of this item in the current grid page is what SelectedIndex provides.
      Selection is activated on a DataGrid's column only if it's a living instance of the ButtonColumn class. These button columns are bound columns that execute an action when clicked. All button columns fire the ItemCommand event to give you a chance to intercept mouse clicks; they can be distinguished from one another by the value of their CommandName property. A selection-enabled column is just a special kind of button column with a command name of "select." In this case, two events are fired: the generic ItemCommand event, which would fire for any command button, and SelectedIndexChanged, which fires only for selectable button columns. ItemCommand reaches your application first.
      To refresh what you identify as the page-specific details grid, you should run a custom procedure that retrieves the data from the database and then displays the detail data.
void SelectedIndexChanged(Object sender, EventArgs e)
{
BindChildData();
}
      Within the body of this selection changed event, you typically retrieve the primary key of the currently selected row and use that value to run a SQL query to get child rows. In the sample application shown in Figure 1, the child rows contain information about the territories covered by each employee. You retrieve the primary key information with the following code:
int nEmpID = (int) masterGrid.DataKeys[masterGrid.SelectedIndex];
The DataKeys array is automatically filled by the DataGrid control with the values of the column whose name is stored in DataKeyField. The DataKeys array contains as many items as the number of currently displayed grid items.
      If you just want to use custom layout for items, you can also use a Repeater control to enumerate the child items.
<asp:Repeater id="detailGrid" runat="server">
  <headertemplate>
  <b><hr>
  <%#
  ((LinkButton)masterGrid.SelectedItem.Cells[0].Controls[0]).Text
  %>
  <hr></b>
  </headertemplate>

  <itemtemplate>
  <%#
  DataBinder.Eval(Container.DataItem, "territorydescription")
  %>
  <br>
  </itemtemplate>
</asp:Repeater>
In this example, I use a couple of data-binding expressions to link the header and the body of the Repeater control with the data source. In this case, I use these expressions because of the extremely simple, templated nature of the list-bound control. However, as you'll see in a moment, binding expressions are also the key to automating the synchronization between parent and child grids. Figure 3 shows the output of a master/detail application using the Repeater control.

Figure 3 Master/Detail with Repeater Control
Figure 3 Master/Detail with Repeater Control

      A data-binding expression is any text that appears within the <%#�%> delimiters. This text is dynamically evaluated when the control's DataBind method is called. You can include data-binding expressions as the value of an attribute pair in the opening tag of a server control or anywhere else in the page. Expressions can involve a page property yet not necessarily involve a server control's property. The <%#�%> expression can be placed anywhere in the page and is treated in much the same way as the old-style code blocks in ASP.
      You cannot programmatically assign a data-binding expression to a property. If you try the following code in C#, you'll get a compiler error stating that too many characters have been found in the character literal.
theLabel.Text='<%# "Hello, world" %>';
This error occurs because the C# compiler considers everything that follows a single quote to be a literal. The following code keeps the C# compiler happy
theLabel.Text="<%# \"Hello, world\" %>";
but the caption of the label now equals the literal string and not a data-binding directive.
<%# "Hello, world" %>
So any data-binding expression can only appear in the body of the ASP.NET page, preferably as the value of control's attribute.
      In ASP.NET, data-binding processing is split into two main steps. When the layout of the page is parsed, any <%#�%> binding object is identified and traced. Since the binding objects are early-bound, you can't programmatically add a binding object and, if you try, you can only assign the expression to the control's property as a literal string.
      The second step occurs when the ASP.NET runtime scans the Web Forms page code and renders the output to the browser. Upon finding a binding object, the runtime replaces it with the dynamically calculated data value. In this manner, a data value can be assigned to almost any property exposed by either the page or the controls it contains.

Declarative Master/Detail

      What was wrong with the code used to produce the output of Figure 1 and Figure 3? This kind of code requires that you always write a handler for the OnSelectedIndexChanged event and subsequently the SQL command that retrieves the subset of child rows. Although the code is rather vanilla, writing it over and over can get boring. Let's see how to make it powerful and more automatic.
      In the OnSelectedIndexChanged event, you have to fetch the subset of rows that form the child view. The code in the BindChildData procedure of Figure 2 can be made more parametric by passing the connection string, the command text, and the ID of the parent grid control. It could also be changed to return the DataTable object just fetched.
DataTable BindChildData(String strCnn, String strCmd,
                        String strParentID)
      The return value of this method can be declaratively attached to the DataSource of the child grid using a data-binding expression.
<asp:DataGrid id="detailGrid" runat="server"
•••
  datasource='<%# BindChildData(
    "DATABASE=northwind;SERVER=localhost;UID=sa;",
    "SELECT et.territoryid, td.territorydescription FROM " +
    "employeeterritories AS et INNER JOIN territories AS td " +
    "ON et.territoryid=td.territoryid", "masterGrid") %>'
>
      This changes the whole sample application, as shown in Figure 4. When you declare the DataGrid control in the ASPX page, you just use the generic BindChildData procedure and qualify it with three page-specific arguments. The BindChildData code can be used over and over again as long as you have two DataGrid controls to synchronize. Note that with this approach you still have to define an event handler for the SelectedIndexChanged event on the master data grid. The code for this particular handler is pretty straightforward, though, as it is limited to rebinding and refreshing the details grid. BindChildData is a standard piece of code and can be buried in a number of places, including a custom method on a custom page-derived class or in a custom DataGrid control.
      In the code shown in Figure 4, I also made some assumptions that do not significantly affect the general applicability of the solution. In particular, I assume that the parent DataGrid has the DataKeyField set—which is definitely likely to happen in real-world scenarios. Second, I assume that the type of the primary key set in DataKeyField can be handled as a variable-length data type in SQL Server. Since the data key field represents a column with unique values, it normally contains a number or a string. In both cases, the following code would work just fine:
SqlParameter p = new SqlParameter("@p1", SqlDbType.VarChar);
p.Value = nP1;
cmd.Parameters.Add(p);
      ASP.NET synchronization can never be as seamless as the synchronization for Windows Forms applications. There is no reliable way for a list-bound control to silently determine its parent. Just for this one comparison, the BindingContext collection of Windows Forms data binding provides the DataSet objects that each bound control points to. Moreover, the DataSet objects are downloaded only once. In a Web scenario, whenever a user changes the selection on one grid, the back-end environment is quickly rebuilt and the smallest amount of information possible is kept in memory.
      If you want to implement master/detail synchronization over the Web while reusing as much code as possible, there are two approaches, both of which require you to write a customized version of the DataGrid control. The first approach requires that you download only the child rows that you need continually. In order to do so, you need to keep track of the connection string, the SQL command to execute, and the parent grid. The parent grid is needed to get the currently selected item and the relative primary key value. This approach is based on the code in Figure 4, taken one step further.
      The alternate approach is closer to what happens with Windows Forms automatic data binding. The parent grid contains all the data and sets up an ADO.NET relationship between elements. In order to make this approach more effective, the full DataSet is cached across multiple page requests. The name of the parent grid and the name of the relation are passed to the detail grid, which then retrieves the parent data source and creates a child view based on the specified relation. These two approaches can be built into the same server control. Note that you can see the code for HDataGrid in Figure 5.

HDataGrid Does SQL

      A control that automatically fetches the data to display from the physical data source is strictly dependent on the data source itself. In ADO.NET, database-aware classes have names and implementations specific to the underlying source. Thus, the SqlConnection class is distinct from the OleDbConnection class; you cannot use them interchangeably. You could work with connection objects in a generic, database-agnostic way, but the actions you can take have to be limited to the methods and properties that connection classes inherit from the IDbConnection interface. Under no circumstances can you create an instance of the OleDbConnection class to use as if it were a SqlConnection.
      The same considerations apply to ADO.NET command objects, but not to data adapters. So you could program against commands using the IDbCommand interface regardless of the actual type of command. By contrast, data adapters can only be used through their base (and provider-specific) class—SqlDataAdapter, OleDbDataAdapter, OdbcDataAdapter, and the like. What does this mean to custom DataGrid controls? If the DataGrid is expected to automatically synchronize with a parent control and retrieve the rows itself, then it must be intimately tied to a particular Microsoft .NET data provider (such as SQL Server). This means that the DataGrid will get data using SqlDataAdapter or SqlCommand. Subsequently, these two classes cannot be filled with data coming from Microsoft Access or any other OLE DB or ODBC provider.
      The HDataGrid control has three new properties called ConnectionString, SqlCommandText, and ParentGrid. These three properties are strings and map to the arguments of the BindChildData procedure discussed earlier. All these properties are automatically persisted into the control's ViewState bag.
public String ConnectionString
{
    get {return (String) ViewState["ConnectionString"];}
    set {ViewState["ConnectionString"] = value;}
}
In addition, the control overrides the DataBind method, namely the method that is invoked when the user needs to refresh the user interface of the control.
public override void DataBind()
{
    if (DataSource != null)
    {
        base.DataBind();
        return;
    }
        •••
}
      If the DataSource property is set to a non-null value when DataBind is called, then the control works as expected, calling into the base implementation of the method. Otherwise, if the trio of new properties does not contain null or empty values, the contents of the DataSource property are determined on-the-fly by executing the specified command text over the given connection. Notice that the command text you specify should not include any WHERE clause that restricts the result set to the child rows of the currently selected row in the parent grid. As shown in BindChildData, the WHERE clause is automatically added using the DataKeyField property of the parent grid. To retrieve the page's living instance of the parent grid you use the value of the ParentGrid property in conjunction with the Page's FindControl method.
if (SqlCommandText != String.Empty &&
      ConnectionString != String.Empty &&
    ParentGrid != String.Empty)
{
      DataGrid grid = (DataGrid) Page.FindControl(ParentGrid);
      •••
}
The parent DataGrid will have the DataKeyField property set to a column with unique values. This information is used to unequivocally identify the parent row.
      You register a custom control with an ASP.NET page using the following directive.
<%@ Register TagPrefix="expo" Namespace="BWSLib"
 Assembly="HDataGrid" %>
The value of TagPrefix is up to you. The Namespace property must be filled with the namespace you used to wrap the control's class in the source code.
      In the body of the ASP.NET page, the HDataGrid is used exactly like an ordinary DataGrid control, with the sole exception of the three extra properties (see Figure 6). If you don't set all of them, the HDataGrid control will work as the DataGrid control usually does.

HDataGrid Uses Views

      In Windows Forms, the synchronization between two or more DataGrid controls is established through a DataRelation object. The child grid retrieves the rows to display based on the currently selected row in the parent grid and the relation set on the displayed table. In the HDataGrid control, you can also determine the data source by combining the ParentGrid and DataRelation properties. Assuming that in the DataSet bound to the parent grid a relation such as the following is defined,
DataColumn c1 = ds.Tables["Employees"].Columns["employeeid"];
DataColumn c2 = ds.Tables["Territories"].Columns["employeeid"];
DataRelation rel = new DataRelation("EmployeesToTerritories",
                                     c1, c2);
ds.Relations.Add(rel);
then you can bind the child grid like this:
<expo:HDataGrid id="detailGrid" runat="server"
    datarelation="EmployeesToTerritories"
    parentgrid="masterGrid"
    autogeneratecolumns="false"
    font-size="x-small" font-names="verdana">
    •••
    <Columns>
      <asp:BoundColumn DataField="territoryid"
HeaderText="ID" />
      <asp:BoundColumn DataField="territorydescription"
HeaderText="Territory" />
    </Columns>
</expo:HDataGrid>
It goes without saying that both the Employees and Territories tables and all their rows have to be present in the DataSet. This is the key reason to cache the DataSet somewhere instead of downloading it each and every time.
      In the body of the HDataGrid's DataBind method, when both the parent grid and the relation name have been specified, the control retrieves the DataView object that forms the data source of the parent grid. In the sample code available with this column (see the link at the top of this article), I'm assuming that the data source of the grid is a DataView object. If you plan to make the code more generic and also accept DataTable objects, then you might first want to make the following code a little bit more type-independent.
if (DataRelation != String.Empty &&
    ParentGrid != String.Empty)
{
    DataGrid grid = (DataGrid) Page.FindControl(ParentGrid);
    DataView dvParent = (DataView) grid.DataSource;
        DataRowView drv = dvParent[grid.SelectedIndex];
    DataView dvChild = drv.CreateChildView(DataRelation);
    DataSource = dvChild;
}
In particular, you can obtain the parent view as shown here:
Object o = grid.DataSource;
DataView dvParent = null;
if (o is DataTable)
    dvParent = ((DataTable)o).DefaultView;
else
    dvParent = (DataView) o;
      Getting the DataView object to be the data source of the parent grid is only the first step. Next you must select a view on the currently selected row:
DataRowView drv = dvParent[grid.SelectedIndex];
Finally, you can extract a child view from a DataRowView object, based on a data relation object.
DataSource = drv.CreateChildView(DataRelation);
      Notice that this approach is not dependent on the particular data provider you use. More exactly, the details about the data provider are hardcoded in the hosting ASP.NET page and do not affect the implementation of the HDataGrid control. On the other hand, this approach makes sense only if you have enough memory and therefore can afford to cache data. A strict prerequisite of this solution, in fact, is that all the data is kept in memory and its relationships are all stored in the same data container object. In this month's sample source code, you will find both approaches built into the same HDataGrid control. If you want your own version of the control to support only one of the approaches mentioned here, all you have to do is drop the unneeded code from the DataBind method.

Coming Next Month

      Hierarchical data is very common in real-world projects, so developers need as many tools as they can get their hands on to render such data efficiently and declaratively. In this column, I discussed the techniques you can employ to automate the process of displaying hierarchically shaped data, at the same time making the code reusable. If you're creating Windows Forms applications instead of Web applications, you get better support from the .NET Framework, as I examined in the February issue. Next month I'll tackle another programming technique: two-way data binding.

Send questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Building Web Solutions with ASP.NET and ADO.NET (Microsoft Press), he now spends most of his time teaching classes on ASP.NET and ADO.NET for Wintellect (http://www.wintellect.com). Get in touch with Dino at dinoe@wintellect.com.

Page view tracker