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 DataGrid
s: 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
- Download and extract the DLL zip file to the root of your web project:
bin/ScrollingGrid.dll
ScrollingGrid.js
- Add the
ScrollingGrid
control to your toolbox by browsing to bin/ScrollingGrid.dll. - Create a
ScrollingGrid
control on your page. - Drag a
DataGrid
into the ScrollingGrid
control. - 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
- 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).
- In your .aspx page:
- Register the
TagPrefix
:
<%@ Register TagPrefix=avg Assembly=ScrollingGrid
Namespace=AvgControls %>
- Within your web form, surround your
DataGrid
control with the ScrollingGrid
control as follows:
<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>
- If your page is not in the root of your web project, you will need to specify the
ScriptPath
property. Example, ScriptPath="../"
.
ScrollingGrid Notes
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
public class ScrollingGrid : System.Web.UI.WebControls.Panel
Members (Excluding Inherited)
| 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
|
| FooterWidthReduction | Get/set pixel width to reduce the footer by
|
| 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)
|
| OnInit(EventArgs) | Creates the controls before and after the child DataGrid control.
|
| Overflow | Content DIV overflow style setting. Can be: auto, scroll, hidden.
- Type:
String - Default:
scroll
|
| RenderBeginTag(HtmlTextWriter) | Output's start of control's container TABLE .
|
| RenderEndTag(HtmlTextWriter) | Output's end of control's container TABLE .
|
| ScriptPath | Get/set the location of ScrollingGrid.js
|
| ScrollingEnabled | Set to false to display the DataGrid as normal (i.e., without any scrolling or frozen header, etc.)
- Type:
Boolean - Default:
True
|
| SetStartScrollPosFromPostack() | Set starting scroll position of content DIV from postback.
|
| 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 DIV
s. 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):
var tblHdr = document.getElementById(scrollingGridID + "$tblHdr");
var tblDataGrid = document.getElementById(gridID);
var tblPager = document.getElementById(scrollingGridID + "$tblPager");
var tbodyEl = tblHdr.childNodes[firstChildElIndex(tblHdr, "TBODY")];
var trEl = tbodyEl.childNodes[firstChildElIndex(tbodyEl, "TR")];
var tbodyEl2 = tblDataGrid.childNodes[firstChildElIndex(tblDataGrid, "TBODY")];
var trEl2 = tbodyEl2.childNodes[firstChildElIndex(tbodyEl2, "TR")];
tbodyEl.removeChild(trEl);
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:
for (var i=0; i<widths.length; i++)
{
if (widths[i]+"" == "undefined")
continue;
var tdHdr = trEl.childNodes[i];
var tdContent = trEl2.childNodes[i];
var widthAdjustment = 0;
if (!document.all)
{
widthAdjustment = -2 * parseInt(tblGrid.getAttribute("cellpadding"));
}
if (tdHdr.offsetWidth != widths[i])
tdHdr.style.width = widths[i] + widthAdjustment;
if (tdContent.offsetWidth != widths[i])
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
:
function updateScroll(divObj, scrollingGridID)
{
if (document.getElementById(scrollingGridID + "$divHdr") != null)
document.getElementById(scrollingGridID + "$divHdr").scrollLeft =
divObj.scrollLeft;
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
DIV
s. 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
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.