Freeze the Header, Scroll the Grid
Learn how to scroll only the body of a DataGrid, leaving the header in place, where users can see it.
October 30, 2009
Many ASP.NET developers know how to make the contents of aDataGrid control scrollable. However, the feedback I get through classes,conferences, and this magazine leads me to think fewer people know how toscroll the grid and keep the header and footer stationary. For more information on the DataGrid, see "Customize DataGrid Formatting" and "DataGrid Magic." In this article,I'll briefly recall the do's and don'ts of scrollable HTML blocks and discuss ageneral technique you can use to scroll the contents of DataGrid controls. Atthe end of this article, you'll be able to enhance your Web pages withscrollable reports, while the header stays in place for easy reference.
Meet the Overflow Style Attribute
In the April 2003 issue of aspnetPRO, Jeff Prosiseillustrated how to render the output of a DataGrid control in an area smallerthan necessary ( see ScrollCall). The trick is based on a little-known Cascading Style Sheet (CSS)style, named overflow, that many HTML elements support. The overflow attributehas no effect if the HTML element doesn't specify an explicit width and height.If a fixed area is specified for the element and the overflow style attributeis set to either scroll or auto, the browser renders the output in an embeddedwindow of sorts. This window is given the same size as the element. If the HTMLoutput exceeds the designated area, scrollbars are added to the window to giveusers a chance to reach and see the extra content.
By default, the overflow attributeis set to visible, meaning that the content is not clipped and scrollbars arenot added. Other feasible values are scroll, auto, and hidden. In the firstcase, the content is clipped and horizontal and vertical scrollbars are added -even if the content does not exceed the dimensions of the area. When theattribute is set to auto, scrollbars are added only when necessary. Finally,when the value is hidden, any exceeding content is clipped out and notdisplayed. The following code shows how to make a DataGrid scrollable, usingthe overflow style:
The idea is this: Wrap the grid ina
tag and give it a fixed height in pixels. Whenever the number ofrows in the grid exceeds the fixed height of the container panel, a verticalscrollbar "magically" appears, letting you scroll down, as shown in Figure 1.
Figure 1. The overflow attributeenables scrolling capabilities on the HTML block to which it belongs. If theblock (such as a
tag) contains a DataGrid, that content isdisplayed with scrollbars when necessary.
This solution's main drawback (forothers, see the sidebar, "Know the Drawbacks") is that the scrollable areaincludes the grid as a whole. When you scroll the contents down, the header ofthe grid is clipped out, as shown in Figure 2.
Figure 2. The header of the gridscrolls with the rest of the component and leaves the user without the friendlyfeedback that the header text would provide.
The bug in the output may appeareasy to fix at first, but - trust me - it's a tough one because it stems fromarchitectural features. Let's review the fast facts of this solution. Theoverflow attribute applies to the
tag, meaning that anything withinthe tag is scrolled, regardless of its expected role. And althoughHTML 4.0 promotes the idea of dividing the
tag into sections(THEAD, TBODY, TFOOT), the overflow attribute cannot be applied to such tags.As a result, a table cannot be scrolled partially. In any case, the DataGridserver control creates a HTML table, which doesn't include any of the abovesection tags. Finally, a scrollable tag cannot be embedded within atable without altering the final structure of the table. Does it sound like giving up is theonly viable route? Well, not exactly. The trick is to manually divide theDataGrid into sections, then output each section as an independent HTMLelement. A simple partition for the DataGrid control requires thecreation of header and body sections. (The footer and the pager bar would beother reasonable sections you might want to create, in a real-world solution.)The header will be output as a programmatically created, standalone HTML table;the body will be created by the default output of the DataGrid control with theShowHeader property set to false. It goes without saying that the body of theDataGrid will be wrapped in a scrollable element. The HTML codebelow helps form the basis of the final schema: The PlaceHolder control will bereplaced by a programmatically created, single-row table acting as the headerof the grid underneath. The grid, in turn, doesn't display the automatic headerand appears made of items only. For a better graphical result, you can alsogroup the PlaceHolder and the tag into an all-encompassing table,as shown here: The contents of the grid are simplydata-bound items. They're wrapped in an overflow container and canbe scrolled when necessary. The grid's header consists of an external tableand, as such, occupies a stationary position, unaffected by the scrollingitems. Is it all really as simple as itsounds from this high-level analysis? Again, not exactly. To programmaticallybuild the stationary header you need to create as many cells as there arecolumns in the bound data source. Next, you must ensure that the width of thecolumns in the header and body tables match. Header and body are two distincttables with the same number of columns. This fact alone doesn't guarantee thatthe browser will make cells in both tables the same width. This happens bydefault when the rows belong to the same table, but not when the rows belong todistinct tables, as shown in Figure 3. Figure 3. A stationary grid's headermust be rendered as a table distinct from the body. However, the browserrenders distinct tables independently, which provokes columns misalignment. It goes without saying that the problem documented inFigure 3 has an obvious workaround: Give the DataGrid columns and the header'scells the same explicit width. One way to obtain the same behaviorprogrammatically is when the width of table columns isn't specified and thebrowser determines it dynamically by looking at the largest text to display andthe font style in use. Why can't you do the same? Using a few GDI+ calls, youcan measure the width of all the strings being displayed in a column and thewidth of each header. Next, once you have determined the ideal width for thecolumn, you assign that to the Width property of the TableCell objects in thegrid and in the table that represents the header. The code necessary to implementsuch machinery is easy, but not that short. For this reason, I've built a usercontrol to encapsulate the complexity and make the final scroll grid componentfully reusable in any Web page. The ScrollGrid user control (thescrollablegrid.ascx file) exposes a DataSource property and a DataBind methodthat you use to govern the binding mechanism and trigger the internal engine.The DataBind method has a simple implementation and just calls into theDataGrid's DataBind method. The DataSource's set accessor is the centralconsole that controls the behavior of the scroll grid component: The CalcColumnsWidth method assumesthat the data source object is a DataTable. (The code needs to be enhanced alittle to support any feasible .NET data source.) The method loops through thecolumns in the DataTable object and for each row measures the size of the textbased on the DataGrid's font. Width and height of each cell text is returned asa SizeF object: The SizeF class contains a pair offloat values, which denote the width and the height of the region. The largestvalue for the column is packed into an array - the ColumnsWidth private member.The code snippet below illustrates how to obtain the size of a text given afont: The MeasureString method of theGDI+ Graphics object returns the dimensions of the area necessary to write thespecified text with the specified font. The method above extracts fontinformation from the FontInfo object associated with the grid and creates a newFont object. You should notice that the Font object that must be passed to theGDI+ MeasureString method is different than the FontInfo class used torepresent font information within the majority of server controls. Inparticular, the FontInfo object (returned by the Font property on all controlsderived from WebControl) might lack information about the absolute size of thecurrent font. This is the case when the control's font size is expressed usingrelative measures (such as Smaller, X-Small, Medium, Large). When this happens,in fact, the effective font size is calculated based on the browser's settings.To compute the width of a string of text, you need a number that represents thesize of the font. The method above (which isn't particularly accurate) defaultsto 12 if an explicit font size isn't specified. The Graphics object is thevirtual device that controls the GDI+ rendering. You create a Graphics objectbased on a physical canvas window, printer, or memory. In the previous code, anin-memory canvas is used in the form of a Bitmap object. If the columns of the DataGrid are bound using the tag, you can indicate the width explicitly; if the columns areautomatically generated (AutoGenerateColumns is set to true), the Columnscollection is populated only at rendering time. In other words, there's no wayfor the user's code to plug in and override the Width property. Will thiscreate problems for you? Maybe yes, maybe no. For some reason, giving the header'scolumns the calculated size of the largest grid column only partially works.The size returned by the CalcColumnsWidth method is close to the real valuedetermined by the browser, but it's not always identical. As you can guess,this brings up a little misalignment. A better approach is to turnautomatic column generation off and create any needed columns programmatically.This way, you can be certain that both the header's and the grid's columns havethe same width. The code snippet below shows how to create bound columnsdynamically: The value of thebaseColumnWidth variable (see the downloadablecode) is the size of the header text or the longest text in the column,whichever is greater. The last column in the grid gets an extra 16 pixels tospan over the scrollbar (16 pixels is the default size of Internet Explorer'sscrollbar). The result of this approach isshown in Figure 4 and produced by the following code: Figure 4. The user control is madeof a headerless DataGrid control wrapped in a scrollable tag. Thegrid is surmounted by an independent table, whose cells depend on the contentof the grid's data source. Although the ScrollableGrid user control can beeffectively used in many cases, it can't be considered the ultimate solutionfor the problem of scrolling the contents of a grid. Such a user control hasseveral flaws, the biggest of which is that it's tailor-made for auto-generatedgrids. On the other hand, if you indicatecolumns explicitly, you avoid having to write the code that calculates thewidth - but then you must handle the extra table necessary for the header.Since a similar control is likely reusable across pages and projects, thisapproach puts you on a bad track for code reusability. So much for this firstrelatively simple solution. In a future article, I'll explore ways to extendthe DataGrid control itself to make it support scrolling in addition, not as analternative, to the base set of features. Stay tuned! The sample code in this article is available for download. The overflow attribute is part of the CSS support builtinto the browser - specifically, Internet Explorer 4.0 and newer versions.Although it's effective with HTML tags and, indirectly with ASP.NET servercontrols, you shouldn't abuse it. Having a scrollable DataGrid (as opposed to apageable DataGrid) may look like a good bargain. However, it comes with acouple "gotchas." First, the client must download all the records to view,which could be a very large result set. Second, implementing a scrollablesection represents a hit to the browser. You should make a DataGrid scrollableto save some screen real estate ... but not as an alternative to paging. Tell us what you think! Please send any comments aboutthis article to [email protected] include the article title and author. |
About the Author
You May Also Like