DataGrid Magic
Dino Esposito reveals his best DataGrid tricks, showing us howto make two-row headers, how to build counter columns, how to insert horizontalcell padding, and how to add tool tips to grid cells.
October 30, 2009
Data Bound
LANGUAGES: C#
TECHNOLOGIES: DataGrid, Data Binding
DataGridMagic
Tricks Expose Many Undocumented Possibilities
By DinoEsposito
Eventhough you may be relatively new to ASP.NET programming and data binding, youprobably have figured out the importance and the power of the DataGrid control. In brief, it is anextremely versatile and highly configurable control that renders data in atabular, column-based format. By itself, the control provides a tremendousamount of programmable features, but that never seems to be enough to meetusers demands and requirements. Fortunately, though, after a year ofexperimentation, I have yet to find a feature that just cannot be implementedon top of a DataGrid Web control. Iwant to share with you a few tricks; a few which might even be called dirtytricks. The tricks concern the lookand feel of the DataGrid control andhow it presents information to users.
Beforegoing any further, though, let me clarify one key point that seems to be thesource of some confusion. The .NET Framework defines two flavors of DataGrid controls. They have the samename but belong to different namespaces. More importantly, they have nothingelse in common besides the name. The DataGridcontrol 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 fairlyindependent ways, although both try to offer a common set of capabilities, andboth follow a similar programming model. The Windows Forms DataGrid shows off a number of features that have not beenimplemented in the Web version. Likewise, you can do things with the Web Forms DataGrid control that aren t possibleor do not make sense with the desktop control. So, when reading through theMSDN documentation, check carefully the control you are reading about.
In thisarticle, I ll be discussing and implementing solutions for the following commondevelopment issues:
Howto build a two-row header in which the topmost row groups together moredetailed columns and results in a more descriptive and informative table.
Howto build counter columns (fake columns that simply number the items displayedin the grid, page by page).
Howto insert horizontal cell padding for the text shown in a column.
Howto add context-sensitive tool tips to the cells of the grid.
Theprogramming elements of the DataGridcontrol that will be touched are the ItemCreatedhook, the DataFormatStringattributes of the BoundColumn columnclass, and the pager bar.
One Header for Two Rows
The DataGrid control allows you to assign acaption to each column you bind to the control. The header text is specifiedusing the HeaderText property of thecolumn class. All column classes, from BoundColumnto TemplateColumn and from HyperLinkColumn to ButtonColumn, have a HeaderTextproperty. There are situations, though, in which the complexity and thequantity of the data to render is so high that you just want a second level ofheaders. The second header row is placed atop the columns captions. Each cellgroups together two or more of the underlying columns. The following HTML code(see the output in FIGURE 1) shows what I mean:
Group 1Group 2 Col #1 Col#2 Col #3 Col#4 Contentsof the table |
FIGURE 1: A simple HTML tablewith a two-row header.
Thisfeature is important when you have complex tables to show, such as forinvoices, sales reports, or statistics. Having a couple of header rows is anon-issue if you use plain HTML code or even ASP classic. Paradoxically, itbecomes 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 doesnot support the double-header feature through predefined attributes ordelegates. On the other hand, using the DataGridcontrol is almost mandatory because other list controls, such as the DataList or the Repeater, do not provide for pagination and sorting, and both arecrucial for serious Web reporting. However, if pagination and sorting are notcritical features for you, the DataListcontrol 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 2000Northwind database that clearly distinguishes between personal and job-relatedinformation. 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 DataGridcontrol. Thefirst three columns name, birth date, and country of origin will be groupedunder the Personal super-heading. The other two, title and hire date, fallunder the Job super-heading. The DataGrid control automatically providesfor one header row. The header also takes the graphical styles defined throughthe HeaderStyle property. The rub isthat the DataGrid control does notlet you hook into the creation of the header row. You could define an eventhandler for the ItemCreated event,but that would give you a chance to intervene only after the HTML code for theheader row has been generated. Alternatively, the control s programminginterface allows you to catch the TableRowobject that represents the header row, but you will not get a reliable parentcontrol from it: TableRow rowHeader = (TableRow) e.Item; Youwould need a Table object that is,a living instance of the ASP.NET control used to render the grid to add a newrow. I tried with the Parentproperty of the TableRow object, butit apparently always returns null. Anotherapproach I tried unsuccessfully was wrapping the DataGrid control with an outer asp:table control. With thatapproach, though, the result is two completely distinct tables. It is thenrather impossible to delimit the cells of the topmost table to encompass two ormore of the bottom columns. As weirdas it may seem, the key to working around this problem is the pager item. Ifyou carefully check the pager item through the DataGrid documentation, you cannot miss the fact that the pager canbe placed in three different positions. By default, the control renders itbelow all the grid s items. However, it could be rendered at the top of thegrid, also, and even at both the top and the bottom. I figured this out by merechance while tracing out the behavior of the ItemCreated event handler. You can control the position of thepager programmatically through the Positionproperty: grid.PagerStyle.Position = PagerPosition.TopAndBottom; Just asmany other ASP.NET controls do, the DataGridfirst prepares its output as a string, then fires the PreRender event, and finally dumps out the HTML code. The HTML codeis built according to the control s attributes and the actions accomplishedduring the ItemCreated hooks. Inoticed ItemCreated was called twicefor the pager item. The grid defines pager rows as the first row and last rowsof the resulting table. When it comes to the actual rendering, though, one orboth of these rows are dropped according to the pager position and visibilitysettings. What is the lesson here? If you set the pager position to TopAndBottom, the control will displaytwo identical and functional pagers: one at the top of the grid and one at thebottom (see FIGURE 3).
FIGURE 3: A DataGrid control with two pagers. Bothpager rows are an integral part of the grid s table and can be hooked up duringthe ItemCreated event. The onlycaveat is that you should distinguish between the first and second. If yourcode hooks up the first pager, you might want to clear all the child controlsand add cells as appropriate to form super-headings. If the pager interceptedis the second one, all you have to do is apply any customization to the linkbuttons you need. How doyou know which pager ItemCreated isdealing with? Do you remember the old-fashioned yet effective programming toolsnamed global variables? A plain old global Boolean variable, such as m_bFirstTime, easily could trackwhether or not the pager item is created for the first time. As the code inFIGURE 4 demonstrates, ItemCreateddetects if the pager item is created for the first time in the session and, ifso, removes all the controls in the first (and unique) cell of the row. Noticethat ItemCreated is always invokedtwice for pagers, irrespective of the value assigned to Position. The ex-pager cell (now the first super-heading cell) caninherit some of the styles (such as colors, font, and border) from the headerand override some of them. The method MergeStyleprovides for this. private boolm_bFirstTime = true; public voidItemCreated(Object sender, DataGridItemEventArgs e) { ListItemTypeelemType = 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(newLiteralControl("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(newLiteralControl("Job")); e.Item.Controls.Add(cell1); m_bFirstTime = false; } else { TableCell pager =(TableCell) e.Item.Controls[0]; // Loop through the pager buttonsskipping // over blanks // (Blanks are treated asLiteralControl(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 ItemCreatedevent handler that turns the pager into a header row. The ColumnSpan property of thesuper-heading cell must be set to the number of actual columns it is expectedto 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 becreated if you need to have more first-level headings. The sum of the valuesassigned to the ColumnSpan propertyof all cells must match the number of columns in the grid. When you are donewith it, do not forget to set the m_bFirstTimevariable to false. Likewise, don tforget to reset the global to truewhen you take the other route and process the second pager. If you omit thisstep, you ll have problems with the headers when moving through pages. FIGURE 5shows the DataGrid control with adouble header.
FIGURE 5: A two-row headercreated by reworking the topmost pager. A Pseudo CounterColumn Class Anotherapparently easy task that turns out to be rather tricky with DataGrid controls is having a columnthat simply numbers the displayed items page after page. I confess I neverthought 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 atemplate column and hook up the ItemCreatedevent. As an alternative, you could create and manage a global variable thattracks down the index currently rendered. Thetemplate column is necessary because it is the only way you have to write nondata-bound information in a grid s column. The ItemCreated event is necessary because it is the only way you haveto access the global index of the current item in the data source, withoutresorting to run-time calculations or homemade storage. This global index isreturned by the DataSetIndexproperty of the DataGridItem class.You can catch a running instance of this class in ItemCreated through the event data. Thetemplate of the column can be very straightforward, although you could make itcomplex at will: You canuse labels or other controls to do the job, but using literal controls iscertainly 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 = newLiteralControl(nValue.ToString()); row.Cells[0].Controls.Add(lc); } Noticethat this approach won t work if you are using custom pagination. With custompagination, the data set that is bound to the DataGrid control contains all the items for the current page, andonly those items. So moving through pages does not update the indexes. In thiscase, you must resort to a dynamic calculation. The ith item in page nhas the following 1-based index: (n-1) *PageSize + i + 1 Padding Cells Only Horizontally The DataGrid control supports cell paddingand cell spacing. Both properties are implemented through Cascading StyleSheets (CSS) styles. If you are familiar with CSS attributes, though, you knowthat you could set margins and padding individually for each side. The grid scell spacing and padding, instead, surround the cell text both horizontally andvertically. There really is nothing wrong with this except perhaps that if youspace out the text of two contiguous columns, you end up taking too muchvertical space. (In general, vertical space is a much more valuable resource ina Web page.) So what you really need is a way to set the CSS margin-leftand margin-right attributes. Notice that this cannot be done at the celllevel a feature the DataGridcontrol easily provides for through the ItemStyleand AlternatingItemStyle properties.To be effective, margins must be set for the HTML tag that contains the text inthe cell. Forperformance reasons, the DataGridcontrol renders the content of each cell through a literal control. When mappedto 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 certainlywould help. The code snippet below demonstrates how to use them for thispurpose: ((DataRowView)Container.DataItem)["field_name"] Althoughuseful, templated columns are not so lightweight. You should avoid themwhenever you can obtain the same results in other ways. This is certainly thecase if you need to control padding. The text that goes through a normal BoundColumn column class can be paddedhorizontally if you simply resort to the DataFormatStringproperty. DataFormatString="{0}"/> Theoriginal 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 SeveralASP.NET controls have a ToolTipproperty that defaults to the empty string. DataGridItem and TableCellcontrols are no exception. In a grid, tool tips could allow you to show extrainformation on a per-cell basis. To set a context-sensitive tool tip, you needto hook up the ItemCreated event,catch the cell you need, and then set the ToolTipproperty. Of course, ItemCreatedmust 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); Acontext-sensitive tool tip uses the data item to read row-specific information.The DataItem property serves thispurpose. Pay attention, though, to a peculiarity of the DataGrid control-rendering mechanism that can have unpleasanteffects on pageable grids. Whilethe 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 isrestored. Therefore, any attempt to access the DataItem property is destined to fail. Once the new page index hasbeen set, and the data source properly re-bound to the grid, you call the DataBind method to order theuser-interface refresh. At this time, the ItemCreatedevent fires again, but this time for all the items in the current page and withthe corresponding DataItem propertythat now is not null. FIGURE 6 shows context-sensitive tool tips for the Name column.
FIGURE 6: Tool tips in actionto expand a bit of information shown for a given column. Conclusion The DataGrid control is quite a complexcontrol. It is a mine of features and possibilities, both documented andundocumented, both explored and unexplored. In addressing a few tricks withpractical code, I hope I ve also shed some light on the control s internals. Asa final disclaimer, all that has been discussed and presented here is theoffspring of reverse-engineering, careful tracing, and experimentation. Nothingof the internals is really documented yet. If you happen to use any of thetricks described here, make sure you test them carefully when the .NETFramework ships. DinoEsposito is a trainer and consultant for Wintellect (http://www.wintellect.com) where hemanages the ADO.NET class. Dino writes the Cutting Edge column for MSDN Magazineand Diving Into Data Access for MSDN Voices. Author of Building Web Solutions with ASP.NET and ADO.NET (Microsoft Press), Dino is alsothe co-founder of http://www.VB2TheMax.com.Write to him at mailto:[email protected]. Tell us what you think! Please send any comments about thisarticle to [email protected] include the article title and author.
About the Author
You May Also Like