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.

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 to scroll 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 a general technique you can use to scroll the contents of DataGrid controls. At the end of this article, you'll be able to enhance your Web pages with scrollable reports, while the header stays in place for easy reference.

 

Meet the Overflow Style Attribute

In the April 2003 issue of aspnetPRO, Jeff Prosise illustrated how to render the output of a DataGrid control in an area smaller than necessary ( see Scroll Call). The trick is based on a little-known Cascading Style Sheet (CSS) style, named overflow, that many HTML elements support. The overflow attribute has 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 attribute is set to either scroll or auto, the browser renders the output in an embedded window of sorts. This window is given the same size as the element. If the HTML output exceeds the designated area, scrollbars are added to the window to give users a chance to reach and see the extra content.

By default, the overflow attribute is set to visible, meaning that the content is not clipped and scrollbars are not added. Other feasible values are scroll, auto, and hidden. In the first case, the content is clipped and horizontal and vertical scrollbars are added - even if the content does not exceed the dimensions of the area. When the attribute is set to auto, scrollbars are added only when necessary. Finally, when the value is hidden, any exceeding content is clipped out and not displayed. The following code shows how to make a DataGrid scrollable, using the overflow style:

The idea is this: Wrap the grid in a

tag and give it a fixed height in pixels. Whenever the number of rows in the grid exceeds the fixed height of the container panel, a vertical scrollbar "magically" appears, letting you scroll down, as shown in Figure 1.


Figure 1. The overflow attribute enables scrolling capabilities on the HTML block to which it belongs. If the block (such as a

tag) contains a DataGrid, that content is displayed with scrollbars when necessary.

This solution's main drawback (for others, see the sidebar, "Know the Drawbacks") is that the scrollable area includes the grid as a whole. When you scroll the contents down, the header of the grid is clipped out, as shown in Figure 2.


Figure 2. The header of the grid scrolls with the rest of the component and leaves the user without the friendly feedback that the header text would provide.

The bug in the output may appear easy to fix at first, but - trust me - it's a tough one because it stems from architectural features. Let's review the fast facts of this solution. The overflow attribute applies to the

tag, meaning that anything within the
tag is scrolled, regardless of its expected role. And although HTML 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 DataGrid server control creates a HTML table, which doesn't include any of the above section tags. Finally, a scrollable
tag cannot be embedded within a table without altering the final structure of the table.

Does it sound like giving up is the only viable route? Well, not exactly. The trick is to manually divide the DataGrid into sections, then output each section as an independent HTML element.

 

Devise DataGrid Sections

A simple partition for the DataGrid control requires the creation of header and body sections. (The footer and the pager bar would be other 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 the ShowHeader property set to false. It goes without saying that the body of the DataGrid will be wrapped in a scrollable

element. The HTML code below helps form the basis of the final schema:



The PlaceHolder control will be replaced by a programmatically created, single-row table acting as the header of the grid underneath. The grid, in turn, doesn't display the automatic header and appears made of items only. For a better graphical result, you can also group the PlaceHolder and the

tag into an all-encompassing table, as shown here: 

The contents of the grid are simply data-bound items. They're wrapped in an overflow

container and can be scrolled when necessary. The grid's header consists of an external table and, as such, occupies a stationary position, unaffected by the scrolling items.

Is it all really as simple as it sounds from this high-level analysis? Again, not exactly. To programmatically build the stationary header you need to create as many cells as there are columns in the bound data source. Next, you must ensure that the width of the columns in the header and body tables match. Header and body are two distinct tables with the same number of columns. This fact alone doesn't guarantee that the browser will make cells in both tables the same width. This happens by default when the rows belong to the same table, but not when the rows belong to distinct tables, as shown in Figure 3.


Figure 3. A stationary grid's header must be rendered as a table distinct from the body. However, the browser renders distinct tables independently, which provokes columns misalignment.

 

Implement the Grid's Header Section

It goes without saying that the problem documented in Figure 3 has an obvious workaround: Give the DataGrid columns and the header's cells the same explicit width. One way to obtain the same behavior programmatically is when the width of table columns isn't specified and the browser determines it dynamically by looking at the largest text to display and the font style in use. Why can't you do the same? Using a few GDI+ calls, you can measure the width of all the strings being displayed in a column and the width of each header. Next, once you have determined the ideal width for the column, you assign that to the Width property of the TableCell objects in the grid and in the table that represents the header.

The code necessary to implement such machinery is easy, but not that short. For this reason, I've built a user control to encapsulate the complexity and make the final scroll grid component fully reusable in any Web page.

The ScrollGrid user control (the scrollablegrid.ascx file) exposes a DataSource property and a DataBind method that you use to govern the binding mechanism and trigger the internal engine. The DataBind method has a simple implementation and just calls into the DataGrid's DataBind method. The DataSource's set accessor is the central console that controls the behavior of the scroll grid component:

public object DataSource {

 get {return theGrid.DataSource;}

 set {

 theGrid.ShowHeader = false;

 theGrid.AutoGenerateColumns = false;

 CalcColumnsWidth((DataTable) value);

 CreateHeader((DataTable) value);

 theGrid.DataSource = value;

 }

}

The CalcColumnsWidth method assumes that the data source object is a DataTable. (The code needs to be enhanced a little to support any feasible .NET data source.) The method loops through the columns in the DataTable object and for each row measures the size of the text based on the DataGrid's font. Width and height of each cell text is returned as a SizeF object:

void CalcColumnsWidth(DataTable data) {

 FontInfo fontInfo = theGrid.Font;

 foreach(DataColumn col in data.Columns) {

 float maxWidth = 0;

 foreach(DataRow row in data.Rows) {

 SizeF size = MeasureString(

 row[col.ColumnName].ToString, fontInfo);

if (maxWidth < size.Width)

 maxWidth = size.Width;

 }

 ColumnsWidth.Add(maxWidth);

 }

}

The SizeF class contains a pair of float values, which denote the width and the height of the region. The largest value 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 a font:

SizeF MeasureString(string text, FontInfo fontInfo) {

 SizeF size;

 float emSize;

 emSize = Convert.ToSingle(fontInfo.Size.Unit.Value);

 emSize = (emSize==0 ?12 :emSize);

 

 Font stringFont = new Font(fontInfo.Name, emSize);

 Bitmap bmp = new Bitmap(1000, 100);

 Graphics g = Graphics.FromImage(bmp);

 

 size = g.MeasureString(text, stringFont);

 g.Dispose;

 return size;

}

The MeasureString method of the GDI+ Graphics object returns the dimensions of the area necessary to write the specified text with the specified font. The method above extracts font information from the FontInfo object associated with the grid and creates a new Font object. You should notice that the Font object that must be passed to the GDI+ MeasureString method is different than the FontInfo class used to represent font information within the majority of server controls. In particular, the FontInfo object (returned by the Font property on all controls derived from WebControl) might lack information about the absolute size of the current font. This is the case when the control's font size is expressed using relative 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 the size of the font. The method above (which isn't particularly accurate) defaults to 12 if an explicit font size isn't specified. The Graphics object is the virtual device that controls the GDI+ rendering. You create a Graphics object based on a physical canvas window, printer, or memory. In the previous code, an in-memory canvas is used in the form of a Bitmap object.

 

Give Columns a Width

If the columns of the DataGrid are bound using the tag, you can indicate the width explicitly; if the columns are automatically generated (AutoGenerateColumns is set to true), the Columns collection is populated only at rendering time. In other words, there's no way for the user's code to plug in and override the Width property. Will this create problems for you? Maybe yes, maybe no.

For some reason, giving the header's columns the calculated size of the largest grid column only partially works. The size returned by the CalcColumnsWidth method is close to the real value determined by the browser, but it's not always identical. As you can guess, this brings up a little misalignment.

A better approach is to turn automatic 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 have the same width. The code snippet below shows how to create bound columns dynamically:

BoundColumn gridCol = new BoundColumn;

gridCol.DataField = col.ColumnName;

gridCol.ItemStyle.Width = Unit.Pixel(baseColumnWidth);

theGrid.Columns.Add(gridCol);

The value of the baseColumnWidth  variable (see the downloadable code) 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 to span over the scrollbar (16 pixels is the default size of Internet Explorer's scrollbar).

The result of this approach is shown in Figure 4 and produced by the following code:

 


Figure 4. The user control is made of a headerless DataGrid control wrapped in a scrollable

tag. The grid is surmounted by an independent table, whose cells depend on the content of the grid's data source.

 

What's Next?

Although the ScrollableGrid user control can be effectively used in many cases, it can't be considered the ultimate solution for the problem of scrolling the contents of a grid. Such a user control has several flaws, the biggest of which is that it's tailor-made for auto-generated grids.

On the other hand, if you indicate columns explicitly, you avoid having to write the code that calculates the width - but then you must handle the extra table necessary for the header. Since a similar control is likely reusable across pages and projects, this approach puts you on a bad track for code reusability. So much for this first relatively simple solution. In a future article, I'll explore ways to extend the DataGrid control itself to make it support scrolling in addition, not as an alternative, to the base set of features. Stay tuned!

The sample code in this article is available for download.

 

Know the Drawbacks

The overflow attribute is part of the CSS support built into the browser - specifically, Internet Explorer 4.0 and newer versions. Although it's effective with HTML tags and, indirectly with ASP.NET server controls, you shouldn't abuse it. Having a scrollable DataGrid (as opposed to a pageable DataGrid) may look like a good bargain. However, it comes with a couple "gotchas." First, the client must download all the records to view, which could be a very large result set. Second, implementing a scrollable section represents a hit to the browser. You should make a DataGrid scrollable to save some screen real estate ... but not as an alternative to paging.

Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish