Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

ScrollingGrid: A Cross-Browser Freeze-Header Two-Way Scrolling DataGrid

4.75/5 (56 votes)
28 Aug 2006CPOL10 min read 20   11.4K  
A cross-browser container control for a DataGrid to freeze the header row and sync the header when the DataGrid is scrolled horizontally

Screenshot - Firefox

Test the online demo of ScrollingGrid: Online Demo^ (supports Internet Explorer, Firefox 1.0+, Netscape 7+).

Introduction

This control provides a cross-browser solution to a common problem with large DataGrids: being able to scroll data while keeping the header row frozen above the data. This control allows two-way scrolling - i.e., the header slides left and right as you scroll horizontally.

There is an IE-only solution that has been around for some time, which makes the header row behave like a layer. But one minor issue with it is that dropdown lists float above the header row when scrolling. A major issue with it is that it's IE-only :-).

Advantages Over Similar Controls

  • Cross-browser compatible: Mozilla Firefox 1+, Internet Explorer 5+, Netscape 7+
  • <select> elements do not float above the header. This is a common complaint with other solutions.
  • Last scroll position is submitted on postback, which you can then use to set the start scroll position.
  • You still use the ASP.NET DataGrid control, so you don't lose Intellisense in Visual Studio's HTML editor (for grid columns, etc.).

Disadvantages

  • It only freezes header and bottom-pager rows. It does not freeze columns (similar IE-only controls can).

Other Points

  • Simple implementation
  • Supports multiple scrolling grids on the same page
  • Doesn't work with Opera browser (yet)
  • If JavaScript is disabled, the entire original DataGrid will scroll within the main DIV (i.e., the header doesn't freeze).

Using the ScrollingGrid

Visual Studio Designer

  1. Download and extract the DLL zip file to the root of your web project:
    bin/ScrollingGrid.dll
    ScrollingGrid.js
  2. Add the ScrollingGrid control to your toolbox by browsing to bin/ScrollingGrid.dll.
  3. Create a ScrollingGrid control on your page.
  4. Drag a DataGrid into the ScrollingGrid control.
  5. If your page is not in the root of your web project, you will need to specify the ScriptPath property. Example, ScriptPath="../".

Visual Studio HTML View

  1. Download and extract the DLL zip file to the root of your web project:
    bin/ScrollingGrid.dll
    ScrollingGrid.js

    Now add a reference to ScrollingGrid.dll in your web project. Alternatively, you can add ScrollingGrid.cs to your project (included in the source ZIP), instead of using ScrollingGrid.dll (which you should delete in this case).

  2. In your .aspx page:
    1. Register the TagPrefix:
      ASP.NET
      <%@ Register TagPrefix=avg Assembly=ScrollingGrid 
                                 Namespace=AvgControls %>
    2. Within your web form, surround your DataGrid control with the ScrollingGrid control as follows:
      HTML
      <form runat=server>
      
        <avg:ScrollingGrid runat=server ID=sg1 
             Width=450 Height=240 CssClass=sgTbl>
      
          <asp:DataGrid runat=server ID=Grid2 CellPadding=5 CellSpacing=1
            AutoGenerateColumns=True AllowSorting=True 
            AllowPaging=True PageSize=35
            OnPageIndexChanged=Grid2_PageIndexChanged 
            AllowCustomPaging=True>
              <HeaderStyle BackColor=red ForeColor=white Font-Bold=True />
              <ItemStyle BackColor=#fefefe />
              <AlternatingItemStyle BackColor=#eeeeee />
              <PagerStyle BackColor=silver ForeColor=White 
                          Mode=NumericPages />
          </asp:DataGrid>
      
        </avg:ScrollingGrid>
      
      </form>
    3. If your page is not in the root of your web project, you will need to specify the ScriptPath property. Example, ScriptPath="../".

ScrollingGrid Notes

  • The Width, Height, ScriptPath, and CssClass properties are all optional and have default values.
  • The Width value may be pixels or percentage.
  • The Height value must be pixels (not percentage), and corresponds to the height of the DIV that contains the data rows. Since the header and pager are moved outside this DIV, your total height will be slightly larger.
  • The ScrollingGrid expects only the DataGrid as a child control.
  • If your DataGrid contains a bottom pager, it will be automatically frozen underneath the content rows.
  • If your ScrollingGrid is not contained within a web form, you should reference the JavaScript file in your HEAD tag:
    HTML
    <script language=JavaScript src="ScrollingGrid.js"></script>
  • Some properties cannot be changed at runtime because the ScrollingGrid creates the control structure in the OnInit method (seemed to be the only way the DataGrid postback functionality could be preserved). Example, the DataGrid's ShowHeader and the ScrollingGrid's ScrollingEnabled property
  • Images in your DataGrid may get clipped in Internet Explorer if you don't set their width attribute.
  • If you use the Visual Studio .NET designer, you can add this control to your toolbox. Then, just drag it to the page, and drag your DataGrid into the ScrollingGrid.

DataGrid Notes

There are a couple of points to be aware of when it comes to your DataGrid control:

  • If you do not set the CellPadding attribute of your DataGrid, the ScrollingGrid control will assign a value of 2. The default value of -1 causes problems in Firefox.
  • The ScrollingGrid control automatically assigns GridLines=None on your DataGrid control; otherwise, Firefox will ignore your CellSpacing value. This is a common problem with the DataGrid in Firefox.
  • The ScrollingGrid control automatically sets your DataGrid's BorderWidth=0 property; otherwise, Firefox doesn't match up the columns accurately. If you need to display borders on your DataGrid table, I recommend setting the CellSpacing property to the width of the border (and must also have GridLines=None). Then, set your ScrollingGrid's BackColor property (which will show through as the border color).

ScrollingGrid Class

Summary

Cross-browser container control for a DataGrid to freeze its header and bottom pager while scrolling both horizontally and vertically.

Syntax

C#
public class ScrollingGrid : System.Web.UI.WebControls.Panel

Members (Excluding Inherited)

Image 2

FirefoxBorderWorkaround

Set to false if GridLines and BorderWidth properties on the DataGrid should not be set for optimal results in Firefox.

  • Type: Boolean
  • Default: True

Image 3

FooterWidthReduction

Get/set pixel width to reduce the footer by

  • Type: Int32
  • Default: 0

Image 4

HeaderWidthReduction

Get/set pixel width to reduce the header by, e.g., 17 = scrollbar width (if you don't want the header to extend across the top of the scrollbar)

  • Type: Int32
  • Default: 0

Image 5

OnInit(EventArgs)

Creates the controls before and after the child DataGrid control.

  • Return Type: void

Image 6

Overflow

Content DIV overflow style setting. Can be: auto, scroll, hidden.

  • Type: String
  • Default: scroll

Image 7

RenderBeginTag(HtmlTextWriter)

Output's start of control's container TABLE.

  • Return Type: void

Image 8

RenderEndTag(HtmlTextWriter)

Output's end of control's container TABLE.

  • Return Type: void

Image 9

ScriptPath

Get/set the location of ScrollingGrid.js

  • Type: String
  • Default:

Image 10

ScrollingEnabled

Set to false to display the DataGrid as normal (i.e., without any scrolling or frozen header, etc.)

  • Type: Boolean
  • Default: True

Image 11

SetStartScrollPosFromPostack()

Set starting scroll position of content DIV from postback.

  • Return Type: void

Image 12

StartScrollPos

Get/set the start scroll position of the content DIV.

  • Type: Point
  • Default: new Point(0, 0)

Inherited Properties

Only these inherited properties have any effect on the HTML output:

  • BackColor
  • CssClass
  • Height
  • Width

How It Works

I had the initial idea after being presented with a question at a job interview, a few years ago. The idea being that the header could actually be an entirely separate table, with the column widths matching exactly with the content table. Both tables would be in their own DIVs. The header DIV would hide the overflow. The content DIV would scroll. When the user scrolls the content DIV across, the header DIV is automatically scrolled to the same horizontal scroll-value. And when the user scrolls the content DIV down, the header remains in view.

However, manually setting column widths is not practical. And making a new grid control would have limited appeal, since most developers are used to the functionality of the DataGrid control. It was about a year ago that I started working on the idea of rendering it around the DataGrid control. But one problem is that the DataGrid doesn't give you a way to get the header HTML only. But accessing the header row in the browser's DOM is simple enough. So as the page is loading in the browser, the script simply reassigns the header TR to a new table.

But moving the header row does not keep the original column widths. So, they then need to be dynamically matched up. Once that is done, it pretty much looks like the original table, with scrollbars for the data rows, and a "frozen" header row that slides left and right as the data is scrolled.

As for the ASP.NET control, the ScrollingGrid class inherits from the Panel control in order to be VS.NET designer-friendly. However, the HTML output is completely custom, so most of the Panel's inherited properties have no effect. The ScrollingGrid expects a DataGrid child-control, and adds its HTML around the DataGrid. The main reason I did not inherit from the DataGrid class is that you would lose VS.NET Intellisense when coding the DataGrid in HTML mode (quite frustrating if you code ASP.NET pages in HTML mode).

The Code

Most of this control's functionality is in the JavaScript initialisation of the control in the browser. The DataGrid's header row and bottom pager row are moved to their respective place-holder tables. Then, the header and content column widths are sync'd by increasing the width of the narrowest column.

Column widths are often influenced by the total width of the table. In Internet Explorer, you can change this behaviour by setting tableEl.style.tableLayout = "fixed". However, in Firefox, this doesn't seem to have any effect, so instead, you need to make sure the table has plenty of room to expand. This is accomplished by setting the width of the outer place-holder table extremely wide (i.e., 10000).

Here is an excerpt from the initScrollingGrid() JavaScript function (to move the header TR element to the place-holder table):

JavaScript
var tblHdr = document.getElementById(scrollingGridID + "$tblHdr");
var tblDataGrid = document.getElementById(gridID);
var tblPager = document.getElementById(scrollingGridID + "$tblPager");

// get header table's first row
var tbodyEl = tblHdr.childNodes[firstChildElIndex(tblHdr, "TBODY")];
var trEl = tbodyEl.childNodes[firstChildElIndex(tbodyEl, "TR")];

// get datagrid table's first row
var tbodyEl2 = tblDataGrid.childNodes[firstChildElIndex(tblDataGrid, "TBODY")];
var trEl2 = tbodyEl2.childNodes[firstChildElIndex(tbodyEl2, "TR")];

// delete empty TR on placeholder table
tbodyEl.removeChild(trEl);

// move the header row from datagrid table to our placeholder table
tbodyEl.appendChild(trEl2);

The firstChildElIndex function is a necessary step for Firefox, in response to an annoying behaviour - namely that white-space results in a "#text" childNode in the DOM tree. So in some cases, TBODY is the first childNode of TABLE, and in other cases, the second childNode (depending on whether there is white-space between <table> and <tr>).

Here is an excerpt from the SetWidths JavaScript function:

JavaScript
for (var i=0; i<widths.length; i++)
{
    if (widths[i]+"" == "undefined")
        continue;

    // TD element for the header row
    var tdHdr = trEl.childNodes[i];

    // TD element for the content row
    var tdContent = trEl2.childNodes[i];

    var widthAdjustment = 0;
    if (!document.all)
    {
        // FF: subtract cellpadding
        widthAdjustment = -2 * parseInt(tblGrid.getAttribute("cellpadding"));
    }

    // Update either the header cell or content cell
    // (not both, otherwise FF stuffs up)
    if (tdHdr.offsetWidth != widths[i])
        // update header column width
        tdHdr.style.width = widths[i] + widthAdjustment;
    if (tdContent.offsetWidth != widths[i])
        // update content column width
        tdContent.style.width = widths[i] + widthAdjustment;
}

The widths array is populated in a previous loop, and contains the correct width for each column. So here, the appropriate table cells are adjusted to their new width.

To sync the header with the content, this simple JavaScript function handles scroll events on the content DIV:

JavaScript
// content scroll event handler (matches the header row
// with the horizontal scroll position of content)
function updateScroll(divObj, scrollingGridID)
{
    if (document.getElementById(scrollingGridID + "$divHdr") != null)
        document.getElementById(scrollingGridID + "$divHdr").scrollLeft = 
                                                       divObj.scrollLeft;

    // save scroll position to hidden input
    document.getElementById(scrollingGridID + "$hdnScrollPos").value = 
                            divObj.scrollLeft + "-" + divObj.scrollTop;
}

Even though the header DIV does not display a scrollbar, its scrollLeft property still shifts the position of its content.

Issues

  • In Firefox (versions prior to 1.5), if you drag to select text in the content table and cause the content DIV to scroll, the scroll event handler does not fire and so the header doesn't get sync'd.
  • In Firefox (versions prior to 1.5), the mouse wheel doesn't work with DIVs. This is a browser behaviour.
  • To disable the scrolling behaviour, the ScrollingEnabled property of the ScrollingGrid has to be specified in the control's server tag (not code-behind). Setting this at runtime doesn't work. This is a side-effect of setting all the controls in the OnInit method (which is necessary to avoid issues with the DataGrid's postback events). I'll have to experiment with a few other ideas to work around this limitation.

Points of Interest

  • The rendering behaviour of IE vs. FF is very different. Developing the variable width functionality was much easier with Firefox which renders tables as you would expect when specifying percentage widths. However, IE reacted completely differently. Because the DIVs within the outer table contained a lot of content (even though the overflow was clipped), IE was making the outer table really wide to accommodate the content. The solution was to use table-layout:fixed which tells IE to listen to the specified table widths (and clip any wide content). I then use the script to update the DIV widths to the same width as the TD.
  • Large amounts of data don't seem to present any problems. The header-row doesn't sync quite as instantly when there are thousands of rows. And the browser takes up a lot of memory and CPU. But from what I could tell, no more so than using a DataGrid on its own.
  • Developing the DHTML to work with Firefox + IE was a challenge, primarily because the Firefox DOM creates #text nodes even when there is only whitespace between HTML tags.
  • If you want to clip the text in a column (and not have it wrap), you need to create a TemplateColumn and do some fancy stuff with CSS and a DIV. Firefox scores extra points here because you can actually highlight the text and it will slide across within the clipped area, whereas IE just clips it. Here is the TemplateColumn to achieve this:
    ASP.NET
    <asp:TemplateColumn Visible=True HeaderText=ShipName>
      <ItemTemplate>
        <div style="overflow:hidden; text-overflow:clip; width:70px;">
           <nobr><%# DataBinder.Eval(Container.DataItem, "ShipName") %></nobr>
        </div>
      </ItemTemplate>
    </asp:TemplateColumn>

Conclusion

Developing cross-browser DHTML can be a real challenge, but in my opinion, Firefox is a great browser, and its continued popularity warrants the extra effort. It's definitely been a good lesson in browser rendering behaviours as well as developing custom controls. Feel free to leave feedback below if you find this control useful.

Updates

August 2006 - Major improvements to the control. Reworked this article.

  • Now supports variable width (i.e., percentage).
  • Submits the last scroll position on postback, which can optionally be used to set the start scroll position (using the new StartScrollPos property).
  • Utility function to scale the height when the browser is resized.
  • Added a property to specify the path to ScrollingGrid.js.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)