Freeze ASP.NET GridView Headers by Creating Client-Side Extenders
Lately I've been working on a pet project where I needed to freeze a GridView header so that it didn't move while the grid records were scrolled by an end user. After searching the Web I came across a lot of "pure" CSS ways to freeze a header. After researching them more they tend to rely on a lot of CSS hacks to do the job and require HTML to be structured in a custom way. I also found a lot of custom GridView classes (another one here), and more. Some of the solutions were really good while others seemed a bit over the top and required a lot of custom C# or VB.NET code just to do something as simple as freezing a header. Freezing a header requires CSS and potentially JavaScript depending upon what needs done. Regardless if CSS or JavaScript are used, these are client-side technologies of course. I started to write my own derivative of the GridView class but quickly realized that it made more sense to use the standard GridView control and simply extend it from the client-side. An example of what I was after is shown next:
In the past I've always made a scrollable GridView by wrapping a <div> tag around the control, adding a table with header rows inside the <div> (but above the GridView control code), and setting the CSS overflow property on the wrapper div to "auto". Scott Mitchell has a nice article on doing this with a DataGrid and you can see a sample of doing something like that at my XML for ASP.NET Developers Website if you're interested. This technique works well but makes it a bit hard to line up the header and child rows perfectly (although it can be done).
I decided to build my own client-side GridView "extender" code to accomplish the task since I didn't want to worry about yet another GridView assembly being in my toolbox just to freeze a header. It appears to work great in IE6+ and Firefox 2 (haven't tried FireFox 1 – 1.5) although I'm sure there are some ways it could be improved and I can't say I've done extensive testing. The code basically grabs the first header row from the GridView using JavaScript and moves it inside of a THEAD tag so that CSS can easily be applied to all of the TH tags and things work well in FireFox. It then applies a few styles to the appropriate items within the GridView HTML that is generated.
The JavaScript and CSS code is shown below. On online version can be viewed here.
.WrapperDiv {
width:800px;height:400px;border: 1px solid black;
}
.WrapperDiv TH {
position:relative;
}
.WrapperDiv TR
{
/* Needed for IE */
height:0px;
}
</style>
<script>
function onLoad()
{
FreezeGridViewHeader('GridView1','WrapperDiv');
}
function FreezeGridViewHeader(gridID,wrapperDivCssClass)
{
/// <summary>
/// Used to create a fixed GridView header and allow scrolling
/// </summary>
/// <param name="gridID" type="String">
/// Client-side ID of the GridView control
/// </param>
/// <param name="wrapperDivCssClass" type="String">
/// CSS class to be applied to the GridView's wrapper div element.
/// Class MUST specify the CSS height and width properties.
/// Example: width:800px;height:400px;border:1px solid black;
/// </param>
var grid = document.getElementById(gridID);
if (grid != 'undefined')
{
grid.style.visibility = 'hidden';
var div = null;
if (grid.parentNode != 'undefined')
{
//Find wrapper div output by GridView
div = grid.parentNode;
if (div.tagName == "DIV")
{
div.className = wrapperDivCssClass;
div.style.overflow = "auto";
}
}
//Find DOM TBODY element and remove first TR tag from
//it and add to a THEAD element instead so CSS styles
//can be applied properly in both IE and FireFox
var tags = grid.getElementsByTagName('TBODY');
if (tags != 'undefined')
{
var tbody = tags[0];
var trs = tbody.getElementsByTagName('TR');
var headerHeight = 8;
if (trs != 'undefined')
{
headerHeight += trs[0].offsetHeight;
var headTR = tbody.removeChild(trs[0]);
var head = document.createElement('THEAD');
head.appendChild(headTR);
grid.insertBefore(head, grid.firstChild);
}
//Needed for Firefox
tbody.style.height =
(div.offsetHeight - headerHeight) + 'px';
tbody.style.overflowX = "hidden";
tbody.overflow = 'auto';
tbody.overflowX = 'hidden';
}
grid.style.visibility = 'visible';
}
}
</script>
This solution works well but it means I'd have to reference a CSS file and JavaScript file for each GridView control that needs a frozen header. I decided to build an ASP.NET AJAX Extender control based upon the ASP.NET AJAX Toolkit which turned out to be fairly straightforward. This means that I have to reference an assembly in my project (which I complained about above), but I can still use the stock GridView control. I'll talk more about the extender control (called GridViewHeaderExtender) in a future post. Those interested in seeing the code can download it here. The extender seems to work great with IE6+ and FireFox 2 but doesn't work with Safari on Windows (very few of the frozen header solutions I found online worked with Safari).
Update: Matt Berseth recently blogged about freezing GridView headers that are rendered using ControlAdapters. Read more about his technique here.