Data Bound

LANGUAGES: C#

TECHNOLOGIES: DataGrid, Data Binding

 

DataGrid Magic

Tricks Expose Many Undocumented Possibilities

 

By Dino Esposito

 

Even though you may be relatively new to ASP.NET programming and data binding, you probably have figured out the importance and the power of the DataGrid control. In brief, it is an extremely versatile and highly configurable control that renders data in a tabular, column-based format. By itself, the control provides a tremendous amount of programmable features, but that never seems to be enough to meet users demands and requirements. Fortunately, though, after a year of experimentation, I have yet to find a feature that just cannot be implemented on top of a DataGrid Web control. I want to share with you a few tricks; a few which might even be called dirty tricks.    The tricks concern the look and feel of the DataGrid control and how it presents information to users.

 

Before going any further, though, let me clarify one key point that seems to be the source of some confusion. The .NET Framework defines two flavors of DataGrid controls. They have the same name but belong to different namespaces. More importantly, they have nothing else in common besides the name. The DataGrid control I m talking about in this article is the DataGrid Web control defined in the System.Web.UI.WebControl namespace. The other DataGrid control is the Windows Forms DataGrid control defined in the System.Windows.Forms namespace. They have been developed in fairly independent ways, although both try to offer a common set of capabilities, and both follow a similar programming model. The Windows Forms DataGrid shows off a number of features that have not been implemented in the Web version. Likewise, you can do things with the Web Forms DataGrid control that aren t possible or do not make sense with the desktop control. So, when reading through the MSDN documentation, check carefully the control you are reading about.

 

In this article, I ll be discussing and implementing solutions for the following common development issues:

  • How to build a two-row header in which the topmost row groups together more detailed columns and results in a more descriptive and informative table.
  • How to build counter columns (fake columns that simply number the items displayed in the grid, page by page).
  • How to insert horizontal cell padding for the text shown in a column.
  • How to add context-sensitive tool tips to the cells of the grid.

 

The programming elements of the DataGrid control that will be touched are the ItemCreated hook, the DataFormatString attributes of the BoundColumn column class, and the pager bar.

 

One Header for Two Rows

The DataGrid control allows you to assign a caption to each column you bind to the control. The header text is specified using the HeaderText property of the column class. All column classes, from BoundColumn to TemplateColumn and from HyperLinkColumn to ButtonColumn, have a HeaderText property. There are situations, though, in which the complexity and the quantity of the data to render is so high that you just want a second level of headers. The second header row is placed atop the columns captions. Each cell groups together two or more of the underlying columns. The following HTML code (see the output in FIGURE 1) shows what I mean:

 

<table>

<tr>

  <td colspan=2>Group 1</td> <td colspan=2>Group 2</td>

</tr>

<tr>

  <td>Col #1</td> <td>Col #2</td>

  <td>Col #3</td> <td>Col #4</td>

</tr>

<tr>

<td><i>Contents of the table</i></td>

</tr>

</table>

 


FIGURE 1: A simple HTML table with a two-row header.

 

This feature is important when you have complex tables to show, such as for invoices, sales reports, or statistics. Having a couple of header rows is a non-issue if you use plain HTML code or even ASP classic. Paradoxically, it becomes a tricky affair if you attempt to implement it as an ASP.NET solution. To render professional reports, the most reasonable approach is with the DataGrid control. Unfortunately, though, the DataGrid control does not support the double-header feature through predefined attributes or delegates. On the other hand, using the DataGrid control is almost mandatory because other list controls, such as the DataList or the Repeater, do not provide for pagination and sorting, and both are crucial for serious Web reporting. However, if pagination and sorting are not critical features for you, the DataList control is the easiest tool you can leverage to build complex headers.

 

Now, you ll see how to build a report of the employees in the SQL Server 2000 Northwind database that clearly distinguishes between personal and job-related information. FIGURE 2 shows how the columns of the sample DataGrid control have been declared.

 

<Columns>

   <asp:TemplateColumn HeaderText="Name">

      <itemtemplate>

          <%# "<b>" + ((DataRowView)Container.DataItem)

            ["lastname"] + "</b>, " +

          ((DataRowView)Container.DataItem)["firstname"]   %>

      </itemtemplate>

   </asp:TemplateColumn>

   <asp:BoundColumn DataField="birthdate" HeaderText="Born"

    DataFormatString="{0:d}" />

   <asp:BoundColumn DataField="country"

    HeaderText="Country" />

   <asp:BoundColumn DataField="title" HeaderText="Title" />

   <asp:BoundColumn DataField="hiredate" HeaderText="Hired"

    DataFormatString="{0:d}" />

</Columns>

FIGURE 2: The columns of the sample DataGrid control.

 

The first three columns name, birth date, and country of origin will be grouped under the Personal super-heading. The other two, title and hire date, fall under the Job super-heading.

 

The DataGrid control automatically provides for one header row. The header also takes the graphical styles defined through the HeaderStyle property. The rub is that the DataGrid control does not let you hook into the creation of the header row. You could define an event handler for the ItemCreated event, but that would give you a chance to intervene only after the HTML code for the header row has been generated. Alternatively, the control s programming interface allows you to catch the TableRow object that represents the header row, but you will not get a reliable parent control from it:

 

TableRow rowHeader = (TableRow) e.Item;

 

You would need a Table object that is, a living instance of the ASP.NET control used to render the grid to add a new row. I tried with the Parent property of the TableRow object, but it apparently always returns null.

 

Another approach I tried unsuccessfully was wrapping the DataGrid control with an outer asp:table control. With that approach, though, the result is two completely distinct tables. It is then rather impossible to delimit the cells of the topmost table to encompass two or more of the bottom columns.

 

As weird as it may seem, the key to working around this problem is the pager item. If you carefully check the pager item through the DataGrid documentation, you cannot miss the fact that the pager can be placed in three different positions. By default, the control renders it below all the grid s items. However, it could be rendered at the top of the grid, also, and even at both the top and the bottom. I figured this out by mere chance while tracing out the behavior of the ItemCreated event handler. You can control the position of the pager programmatically through the Position property:

 

grid.PagerStyle.Position = PagerPosition.TopAndBottom;

 

Just as many other ASP.NET controls do, the DataGrid first prepares its output as a string, then fires the PreRender event, and finally dumps out the HTML code. The HTML code is built according to the control s attributes and the actions accomplished during the ItemCreated hooks. I noticed ItemCreated was called twice for the pager item. The grid defines pager rows as the first row and last rows of the resulting table. When it comes to the actual rendering, though, one or both of these rows are dropped according to the pager position and visibility settings. What is the lesson here? If you set the pager position to TopAndBottom, the control will display two identical and functional pagers: one at the top of the grid and one at the bottom (see FIGURE 3).

 


FIGURE 3: A DataGrid control with two pagers.

 

Both pager rows are an integral part of the grid s table and can be hooked up during the ItemCreated event. The only caveat is that you should distinguish between the first and second. If your code hooks up the first pager, you might want to clear all the child controls and add cells as appropriate to form super-headings. If the pager intercepted is the second one, all you have to do is apply any customization to the link buttons you need.

 

How do you know which pager ItemCreated is dealing with? Do you remember the old-fashioned yet effective programming tools named global variables? A plain old global Boolean variable, such as m_bFirstTime, easily could track whether or not the pager item is created for the first time. As the code in FIGURE 4 demonstrates, ItemCreated detects if the pager item is created for the first time in the session and, if so, removes all the controls in the first (and unique) cell of the row. Notice that ItemCreated is always invoked twice for pagers, irrespective of the value assigned to Position. The ex-pager cell (now the first super-heading cell) can inherit some of the styles (such as colors, font, and border) from the header and override some of them. The method MergeStyle provides for this.

 

private bool m_bFirstTime = true;

public void ItemCreated(Object sender,

 DataGridItemEventArgs e)

{

  ListItemType elemType = e.Item.ItemType;

  if (elemType == ListItemType.Pager)

  {

    if (m_bFirstTime)

    {

      // Personal header

      TableCell cell0 = (TableCell) e.Item.Controls[0];

      cell0.Controls.Clear();

      cell0.MergeStyle(grid.HeaderStyle);

      cell0.BackColor = Color.Navy;

      cell0.ForeColor = Color.Yellow;

      cell0.ColumnSpan = 3;

      cell0.HorizontalAlign = HorizontalAlign.Center;

      cell0.Controls.Add(new LiteralControl("Personal"));

 

      // Job header

      TableCell cell1 = new TableCell();

      cell1.MergeStyle(grid.HeaderStyle);

      cell1.BackColor = Color.Navy;

      cell1.ForeColor = Color.Yellow;

      cell1.ColumnSpan = 2;

      cell1.HorizontalAlign = HorizontalAlign.Center;

      cell1.Controls.Add(new LiteralControl("Job"));

      e.Item.Controls.Add(cell1);

      m_bFirstTime = false;

    }

    else

    {

      TableCell pager = (TableCell) e.Item.Controls[0];

 

      // Loop through the pager buttons skipping

      // over blanks

      // (Blanks are treated as LiteralControl(s)

      for (int i=0; i<pager.Controls.Count; i+=2)

      {

        Object o = pager.Controls[i];

        if (o is LinkButton)

         {

          LinkButton h = (LinkButton) o;

          h.Text = "[ " + h.Text + " ]";

        }

      else

        {

          Label l = (Label) o;

          l.Text = "Page " + l.Text;

        }

      }

      m_bFirstTime = true;

    }

  }

}

FIGURE 4: The ItemCreated event handler that turns the pager into a header row.

 

The ColumnSpan property of the super-heading cell must be set to the number of actual columns it is expected to group together. Finally, the cell is given text through a literal control. When you intercept the pager, it has only one cell. Thus, new cells have to be created if you need to have more first-level headings. The sum of the values assigned to the ColumnSpan property of all cells must match the number of columns in the grid. When you are done with it, do not forget to set the m_bFirstTime variable to false. Likewise, don t forget to reset the global to true when you take the other route and process the second pager. If you omit this step, you ll have problems with the headers when moving through pages. FIGURE 5 shows the DataGrid control with a double header.

 


FIGURE 5: A two-row header created by reworking the topmost pager.

 

A Pseudo CounterColumn Class

Another apparently easy task that turns out to be rather tricky with DataGrid controls is having a column that simply numbers the displayed items page after page. I confess I never thought that one day someone would ask me to implement just this feature. But, when it happened, I realized that if you want to code it, you need to have a template column and hook up the ItemCreated event. As an alternative, you could create and manage a global variable that tracks down the index currently rendered.

 

The template column is necessary because it is the only way you have to write non data-bound information in a grid s column. The ItemCreated event is necessary because it is the only way you have to access the global index of the current item in the data source, without resorting to run-time calculations or homemade storage. This global index is returned by the DataSetIndex property of the DataGridItem class. You can catch a running instance of this class in ItemCreated through the event data.

 

The template of the column can be very straightforward, although you could make it complex at will:

 

<asp:TemplateColumn HeaderText="">

  <itemtemplate></itemtemplate>

</asp:TemplateColumn>

 

You can use labels or other controls to do the job, but using literal controls is certainly the fastest way you can get to it. The ItemCreated handler above needs to be modified as follows:

 

if (elemType == ListItemType.Item ||

 elemType == ListItemType.AlternatingItem)

{

 DataGridItem row = (DataGridItem) e.Item;

 int nValue = 1 + row.DataSetIndex;

 LiteralControl lc = new LiteralControl(nValue.ToString());

 row.Cells[0].Controls.Add(lc);

}

 

Notice that this approach won t work if you are using custom pagination. With custom pagination, the data set that is bound to the DataGrid control contains all the items for the current page, and only those items. So moving through pages does not update the indexes. In this case, you must resort to a dynamic calculation. The ith item in page n has the following 1-based index:

 

(n-1) * PageSize + i + 1

 

Padding Cells Only Horizontally

The DataGrid control supports cell padding and cell spacing. Both properties are implemented through Cascading Style Sheets (CSS) styles. If you are familiar with CSS attributes, though, you know that you could set margins and padding individually for each side. The grid s cell spacing and padding, instead, surround the cell text both horizontally and vertically. There really is nothing wrong with this except perhaps that if you space out the text of two contiguous columns, you end up taking too much vertical space. (In general, vertical space is a much more valuable resource in a Web page.) So what you really need is a way to set the CSS margin-left and margin-right attributes. Notice that this cannot be done at the cell level a feature the DataGrid control easily provides for through the ItemStyle and AlternatingItemStyle properties. To be effective, margins must be set for the HTML tag that contains the text in the cell.

 

For performance reasons, the DataGrid control renders the content of each cell through a literal control. When mapped to HTML, an ASP.NET literal control is plain, untagged text. In light of this, it seems templated columns are, once again, the only way to go. They certainly would help. The code snippet below demonstrates how to use them for this purpose:

 

<asp:TemplateColumn HeaderText="Caption">

     <itemtemplate>

  <span style="margin-left:3;margin-right:3">

((DataRowView)Container.DataItem)["field_name"]

     </itemtemplate>

</asp:TemplateColumn>

 

Although useful, templated columns are not so lightweight. You should avoid them whenever you can obtain the same results in other ways. This is certainly the case if you need to control padding. The text that goes through a normal BoundColumn column class can be padded horizontally if you simply resort to the DataFormatString property.

 

<asp:BoundColumn DataField="title" HeaderText="Title"

  DataFormatString="<span>{0}" />

 

The original cell text is identified in the format string by the {0} placeholder and is wrapped by a <span> tag. The tag contains the margin settings to pad the cell horizontally.

 

Context-sensitive Tool Tips

Several ASP.NET controls have a ToolTip property that defaults to the empty string. DataGridItem and TableCell controls are no exception. In a grid, tool tips could allow you to show extra information on a per-cell basis. To set a context-sensitive tool tip, you need to hook up the ItemCreated event, catch the cell you need, and then set the ToolTip property. Of course, ItemCreated must be hooked up only when the element type is Item or AlternatingItem:

 

TableCell cell = (TableCell) e.Item.Cells[1];

DataRowView drv = (DataRowView) e.Item.DataItem;

if (drv != null)

   cell.ToolTip = PrepareToolTipText(drv);

 

A context-sensitive tool tip uses the data item to read row-specific information. The DataItem property serves this purpose. Pay attention, though, to a peculiarity of the DataGrid control-rendering mechanism that can have unpleasant effects on pageable grids.

 

While the control restores its state after a postback event, ItemCreated is repeatedly invoked for the items in the last page. This happens before the new page index is set and before the data source is restored. Therefore, any attempt to access the DataItem property is destined to fail. Once the new page index has been set, and the data source properly re-bound to the grid, you call the DataBind method to order the user-interface refresh. At this time, the ItemCreated event fires again, but this time for all the items in the current page and with the corresponding DataItem property that now is not null. FIGURE 6 shows context-sensitive tool tips for the Name column.

 


FIGURE 6: Tool tips in action to expand a bit of information shown for a given column.

 

Conclusion

The DataGrid control is quite a complex control. It is a mine of features and possibilities, both documented and undocumented, both explored and unexplored. In addressing a few tricks with practical code, I hope I ve also shed some light on the control s internals. As a final disclaimer, all that has been discussed and presented here is the offspring of reverse-engineering, careful tracing, and experimentation. Nothing of the internals is really documented yet. If you happen to use any of the tricks described here, make sure you test them carefully when the .NET Framework ships.

 

Dino Esposito is a trainer and consultant for Wintellect (http://www.wintellect.com) where he manages the ADO.NET class. Dino writes the Cutting Edge column for MSDN Magazine and Diving Into Data Access for MSDN Voices. Author of Building Web Solutions with ASP.NET and ADO.NET (Microsoft Press), Dino is also the co-founder of http://www.VB2TheMax.com. Write to him at mailto:dinoe@wintellect.com.

 

Tell us what you think! Please send any comments about this article to editors@devproconnections.com. Please include the article title and author.