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:
Group 1 | Group 2 | ||
Col #1 | Col #2 | Col #3 | Col #4 |
Contents of the 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.
<%# "" + ((DataRowView)Container.DataItem)
["lastname"] + ", " +
((DataRowView)Container.DataItem)["firstname"] %>
DataFormatString="{0:d}" /> HeaderText="Country" /> DataFormatString="{0:d}" /> 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). 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 { 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. 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: 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: ((DataRowView)Container.DataItem)["field_name"] 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. DataFormatString="{0}"
/> The
original cell text is identified in the format string by the {0}
placeholder and is wrapped by a
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. 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:[email protected]. Tell us what you think! Please send any comments about this
article to [email protected].
Please include the article title and author.
FIGURE 3: A DataGrid control with two pagers.
FIGURE 5: A two-row header
created by reworking the topmost pager.
FIGURE 6: Tool tips in action
to expand a bit of information shown for a given column.