Real World GridView: Excel-like Frozen Headers for ASP.NET 2.0

Many of us are familiar with frozen cells in Excel, but it is typically quite difficult to implement something like that HTML.  In this “Real World GridViews”, we investigate adding this functionality to GridView to make frozen headers easy to reuse across pages.   Though I am only going to go over freezing headers here, once you get the idea, it is a short step to freeze columns (right or left) and footers. 



This is part 3 of “Real World GridViews”, and if you missed part 1 or 2 then you have some reading to do J Here is a link to Part 2:


I’m starting with a refresher course before we delve into the details.  Since a web control’s primary purpose is to spit out HTML, it’s pretty darn important to understand exactly what HTML a web control spits out, in relation to what HTML you want it to render.  Really GridView just renders a HTML table, surrounded by a DIV, but it is important to understand that. 



    <table cellspacing="0" rules="all" border="1" id="SampleGrid" style="border-collapse:collapse;">


            <th scope="col">Header</th>







Notice that all the properties you set on the GridView (id, borders, etc) end up getting set on the TABLE element.  That is going to be of particular interest to us later in this article.  Now we are ready to get started!!


This work is based on the brilliant algorithm of Brett Merkey, and if you want to really understand how it works, visit his web site (  To summarize his work, he has devised a way to use CSS expressions to create the illusion of “relative fixed positioning”.  His algorithm is fixing the position of a TH or TD relative to the surrounding DIV.  In case you’ve never heard of a CSS expression, it is just some JavaScript in the CSS style which reevaluates whenever the page changes (i.e. someone scrolls, or a dependant element changes).  IE does some black-magic to figure out if a style bound to an expression needs to be reevaluated, and seems to err on the side of forcing reevaluation if it’s not sure if something changed. That said, be warned that if you have a lot of elements with CSS expressions applied to them (over a thousand on my machine), CSS expression can use up a LOT of client side CPU, so be judicious in their use.  Secondly, IE is the only browser which supports CSS expressions, so this is an IE only solution.


I have made some performance optimizations to Brett’s original code, but conceptually it’s the same thing.  Brett uses getElementById which can get VERY slow with large tables, though he does include a note about using “parentNode.parentNode.parentNode.parentNode.scrollTop” which is a little better.  After some extensive testing, I’ve found an even faster way: “this.offsetParent.scrollTop”.  The key to the outer div being the offset parent is that it’s positioning is set to “relative”, which seems odd because that is the default, but it has to be set! 


We are going to make this control completely self contained, so you will not have to deploy a separate CSS with it.  The first thing we need to do is register the styles.  In ASP.NET 1.1, this would have been difficult, but fortunately ASP.NET 2.0 has made this significantly easier.  First we are going to create a class for our style which inherits from “Style”.


private class FrozenTopStyle : Style


    protected override void FillStyleAttributes(CssStyleCollection attributes, IUrlResolutionService urlResolver)


        base.FillStyleAttributes(attributes, urlResolver);


        attributes[HtmlTextWriterStyle.Top] = "expression(this.offsetParent.scrollTop)";

        attributes[HtmlTextWriterStyle.Position] = "relative";

        attributes[HtmlTextWriterStyle.ZIndex] = "2";




And now that we’ve done that, we can register it with the page.  I’ve added a simple check to make sure that we only register the class once when we have multiple grids on the same page.


protected override void OnPreRender(EventArgs e)




    if (this.FreezeHeader && !this.Page.Items.Contains(FrozenGridView.FrozenTopCssClass))


        this.Page.Items[FrozenGridView.FrozenTopCssClass] = "Registered";

        this.Page.Header.StyleSheet.CreateStyleRule(new FrozenTopStyle(), null, "." + FrozenGridView.FrozenTopCssClass);




 Now that we have our styles registered, it’s time to apply those styles to our header.  As you’ve seen in the other articles, we do this after data binding.  The FreezeCells() function just loops through the cells in a header and applies the CSS class to each cell.  It’s interesting to note that this code combines the existing style and our style.  Many people don’t realize that you can assign more than one CSS class to an element just by separating them by a space (i.e. class=“style1 style2” applies both style1 and style2).


private void FreezeCells()


    if (this.FreezeHeader)


        foreach (DataControlFieldHeaderCell th in this.HeaderRow.Cells)


            th.CssClass = FrozenGridView.FrozenTopCssClass + " " + th.CssClass;





This is where we run into a challenge.  We have applied the styles to all the header cells, and now we need to apply some styles to the DIV surrounding the GridView’s table.  Unfortunately GridView does not supply us with a way to easily modify the surrounding DIV, and in some cases it doesn’t even display the DIV!!  Because of this, we are forced to override Render().  Using my favorite tool, .NET Reflector, I was able to figure out what GridView’s implementation of Render did, and just add the stuff I needed. 


Naturally, you are asking, “what do we need to add”?  Well, we want this thing to scroll, so we need to set the overflow style to something appropriate.  We actually let the page tell us what type of scrolling it wants with our Scrolling property, so we need to translate a ScrollBars variable into the appropriate CSS style.  We’ve created two private properties which help us out.  Here we’ll just look at OverflowX, because OverflowY is very similar:


private string OverflowX




        if (this.Scrolling == ScrollBars.Horizontal || this.Scrolling == ScrollBars.Both)


            return "scroll";


        else if (this.Scrolling == ScrollBars.Auto)


            return "auto";




            return "visible";





Secondly, scrolling only happens when the DIV is smaller than the TABLE, so we need to be able to set the Height and Width of the DIV separately from the dimensions of the TABLE.  As we saw in the refresher course at the beginning of this article, GridView’s implementation only decorates the TABLE tag, and does nothing to the DIV.  In order to get the behavior we want, we are going to have to change that.  I’ve overridden the GridView’s default implementation of the Height and Width to set the DIV’s dimensions and added ScrollHeight and ScrollWidth to set the dimensions of the TABLE.


public Unit ScrollHeight




        return base.Height;




        base.Height = value;




public override Unit Height




        object val = this.ViewState["DivHeight"];

        if (null == val)


            return Unit.Empty;



        return (Unit)val;




        this.ViewState["DivHeight"] = value;




Finally we need to write all these styles to the DIV.  We do that in our override of the Render() function.


writer.AddAttribute(HtmlTextWriterAttribute.Id, String.Format(CultureInfo.InvariantCulture, "__gv{0}__div", clientID), true);

writer.AddStyleAttribute(HtmlTextWriterStyle.OverflowX, this.OverflowX);

writer.AddStyleAttribute(HtmlTextWriterStyle.OverflowY, this.OverflowY);

if (!this.Width.IsEmpty)


    writer.AddStyleAttribute(HtmlTextWriterStyle.Width, this.Width.ToString(CultureInfo.InvariantCulture));


if (!this.Height.IsEmpty)


    writer.AddStyleAttribute(HtmlTextWriterStyle.Height, this.Height.ToString(CultureInfo.InvariantCulture));


writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "relative");

writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "Black");

writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "3");

writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "solid");



That’s it! We now have a GridView which will provide frozen column headers and scrolling just by setting a few properties from the page!!  You can see from the series of articles that most of the time GridView is kind to inheritors, however there are those few cases (like our render function) where simply extending the base implementation just won’t cut it.  When you find yourself in one of these situations, don’t panic, make sure you have .NET Reflector in your toolbox.


I’ve posted the code for all three articles @ .  You can actually see what I am working on next if you look around!!

Comments (58)

  1. Myo Zaw says:

    Thanks a lot. This help me out a lot.

  2. Cassini v2 / Visual Studio 2005 Web Server

    Source [Via: gduthie ]

    Debugging AJAX Apps, Part 3 – IE…

  3. smartkeyan says:

    "this.FreezeHeader && !this.Page.Items.Contains(FrozenGridView.FrozenTopCssClass)"

    i get an error at freezeheader & fronzentopssclass. What are these??

  4. foobar says:

    I’ve no idea what you mean by "more than one header."  I’ve no idea where in a program I should stick the little piece of TwoHeadedGridView code you demonstrated in Part 2.

    This is what I had to go through only to get nothing useful from gotdotnet:

    1. Create a phony e-mail account.

    2. Make multiple attempts to sign up for the Passport thing.

    3. Make multiple attempts to download stuff while getting numerous errors on the gotdotnet server.

    4. Whip up a new virtual machine — I’m not going to install any .msi file I know nothing about on a real machine.

    5. Install your .msi file.

    The end result: no source code, no functioning project solution.  With all due respect, this is not funny, this is sick.

    If you don’t intend to release source files, please, let the lowly mortals know.

    If you do intend to release source files, please, create a little bloody folder on your server, stick the 10-or-so KB files in it, provide a link.


  5. Andy Neads says:

    Matt, thanks for these articles, they are fantastically helpful.  I have one problem at the moment with the scrolling grid:  I would like to just use horizontal scrolling along with the in-built paging option rather than vertical scrolling as I need to freeze 6 columns and the number of possible rows can put too much strain on the user’s browser.  However, when I try this I get a null reference exception from GridView BuildCallbackReference when processing the paging at data binding time (I am binding to a data table).  Any thoughts on how I might resolve this?

  6. Mike Liddekee says:

    I seem to be having problems with the code i downloaded from the gotdotnet site.  i created the new control, compiled, and then added to my website.  i’m able to get the grid and show but the problem is the scrolling portion of the grid seems to push the text up into the header area.

  7. Nirmal says:

    In the given scenario when you maximize screen header diappears.

    width  empty

    Height empty

    FreezeHeader set to true

    and scrolling set Both

  8. pac says:

    Will This Work with Dropdown lists in Gridview?

    as Dropdown always comes over the freezed column?

  9. Well, how can i write it in VB ? Please help me.

  10. kentr says:

    In trying to implement this code, I am getting the following build error:

    Error 1 ‘Forms_Admin_Documents.Height’: no suitable method found to override C:InetpubwwwrootPhysicianOrdersFormsAdminDocuments.aspx.cs 96 26 C:…PhysicianOrders

    I see the height method as part of the page.  What am I missing??



  11. mehul says:

    Thanks for the excellent articles and code! I ran into one problem using the DropDowns.aspx. I changed the BulkEditGridView control to allow Insert. The new row did get created but the state dropdown was not populated.

    The only change I made to your code was:

    <rwg:BulkEditGridView ………

                   InsertRowCount="1" EnableInsert="true" ShowFooter="false">

    Thanks again.

  12. Ron Trull says:

    Will this work with DropDownLists in Gridview, as DropDownLists run on top of the frozen headers as soon as you scroll?

  13. mparter says:

    I’m having difficulties with the VB version you posted on GotDotNet. It works brilliantly except that it overwrites any styles I’ve got declaratively setup in my GridView.


    <asp:BoundField DataField="EnrolmentID" HeaderText="Enrolment ID" HeaderStyle-Wrap="false" HeaderStyle-Width="90" ItemStyle-HorizontalAlign="Center" HeaderStyle-CssClass="gridEndCol" />

    The gridEndCol CSS class isn’t in the HTML served to the browser.

  14. Shane Dovers says:

    I was getting an object instance error when the result set was empty, and The EmptyData Template would not display.  To work around this error, here is the code I used:

    Private Sub FreezeCells()

    If Me.FreezeHeader Then

    If (Not Me.HeaderRow Is Nothing) Then

    For Each th As DataControlFieldHeaderCell In Me.HeaderRow.Cells

    th.CssClass = FrozenGridView.FrozenTopCssClass & " " & th.CssClass

    Next th

    End If

    End If

    End Sub

    The issue was that Me.Freezheader was TRUE, but there was no object Me.HeaderRow, and since there was a reference to this object, the error was being generated.

    Now the EmptyData Template is being displayed in there are no records returned.

  15. bdc604 says:

    Usually you have a GridView working with a FormView to show details of the selected record.

    The unanswered question is how to maintain scroll position of the GridView after postback functions, problematic if you have more than one view inside a multiview linked to the GridView. SmartNavigation="true" works on the first view only.


  16. YiTIng says:

    Even I had read the article, I still can’t freeze columns (right or left) and footers.

    Can you help me .

                     YiTing from Taiwan

  17. YiTing says:

    Even I had read the article, I still can’t freeze columns (right or left) and footers.

    Can you help me ?

                     YiTing from Taiwan

  18. Mike Lockhart says:

    I’m using the BulkEditGridView and can’t get it to save changes. the HandleRowChanged event handler never gets called when I change the value in the textbox. Got any ideas on a cause?

    Thanks for the nice control.

  19. DWeber56 says:

    Is there any solution that works with Firefox 1.5x ?

  20. Stephan from germany says:

    Very helpful and interesting artical. I found some other articals which cover the same problem but use an pure CSS approach without CSS expressions.

  21. MIke says:

    I need to allow users to use the arrow keys on a BulkEditGridView. I figured I’d add an event handler to the RowCreated event so I could find the TextBox instance being created and then add a javascript handler to the textbox for the keyup event. However, things aren’t working quite like I’d assumed they would. Any suggestions?

  22. Winsome says:

    Where is the Database "Pubs"?

  23. Richard Peters says:

    The requirement I had was that within the frozen grid table the first column had a series of hyperlinks. Clicking on the hyperlink would load details for that person and display those details loaded in another table AND that the scroll position of the first table be maintained. A solution for this is as follows:

    Add the following function top a js file that will be referenced.

    function scrollToRow(inputId)


       var aObj = document.getElementById(inputId);


       var offsetY = 0;

       var elem = aObj;

       offsetY += elem.offsetTop;

       elem = elem.offsetParent;

       offsetY += elem.offsetTop;

       elem = elem.offsetParent;

    elem.offsetParent.scrollTop = offsetY – document.getElementById([1].offsetTop;


    Download and modify FrozenGridView Render method as follows:

    protected override void Render(HtmlTextWriter writer)







               }            this.RenderContents(writer);

               if (!this.DesignMode)



                   // Now check if we are in a post back situation

                   // If we are then need to check that if the user has trigged the postback by clicking

                   // select on one of the rows of the table.



                       string ctrl = this.Page.Request.Params["__EVENTTARGET"].Replace("$", "_");

                       // handle the situation of multiple frozen grids on a single page.

                       if (ctrl.StartsWith(this.ClientID + "_"))


                           this.Page.ClientScript.RegisterStartupScript(typeof(Page), "frozenGrid", "<script>scrollToRow(‘" + ctrl + "’);</script>");




    Richard Peters

    Senior Application Developer

    Core Technology Ltd

    t:  +64 (0)4 801 2250

    m: +64 (0)21 114 7132



  24. Jesse says:

    Matt, awesome example!  I ended up creating a custom control within a web user control.  Two things for anyone that might be stuck out there.

    1.) To see the scrolling do not let the GridView control its own size.  Set the Height and Width properties.

    2.) I used used the @Register assembly in my .ascx file rather than in the web.config.  I couldn’t get the assembly to load that way.

    Matt, thanks again!

    – Jesse Williams

  25. Roy Miller says:

    Can’t seem to get the vb version working. Can you please help on anything that i may be doing wrong.  What do i have to add to the page after implementing all of this in a class?

  26. Jean-François says:

    Hi Matt!

    This is great stuff! ASP.NET 2.0 as multiple serious lacunes gaps and you just solved a big one… or almost. 😉 I am having the same problem as Mehul above. As soon as I use DropDownFields with EnableInsert in a BulkEditGridView, the insert rows DropDowns don’t get populated. Is there an easy solution to this problem? Can you post a bugfix?

    Best regards,


  27. Sakthi says:

    Is there any solution to fix dropdown problem in gridview

  28. foobar says:

    Brett Merkey’s solution looks just like this one:

  29. Scott Roberts says:

    "Usually you have a GridView working with a FormView to show details of the selected record.

    The unanswered question is how to maintain scroll position of the GridView after postback functions, problematic if you have more than one view inside a multiview linked to the GridView. SmartNavigation="true" works on the first view only.

    Anyone?? "

    Good Question from bdc604. I didn’t use multiviews, but I did get it to work across a postback and inside an AJAX UpdatePanel.

    First, to save the scroll position across a normal postback, you only need a few lines of code.

    1. Add a private property to the FrozenGridView class.

    private HiddenField positionfield = new HiddenField();

    2. Override CreateChildControls() to add our new HiddenField to the gridview control collection.

    protected override void CreateChildControls()



       positionfield.ID = "scrollpos";



    3. Add an attribute to the wrapping DIV to store the scrollTop to the hidden field when the DIV scrolls.

    writer.AddAttribute("onscroll", "document.getElementById(‘" + positionfield.ClientID + "’).value = document.getElementById(‘" + divID + "’).scrollTop;");

    4. Register a startup script for the page. This will reset the "scrollTop" to the correct position when the page loads.

                   Page.ClientScript.RegisterStartupScript(this.GetType(), "testload", "document.getElementById(‘" + divID + "’).scrollTop = document.getElementById(‘" + positionfield.ClientID + "’).value;", true);

    That’s it!

    Unfortunately, when a partial page render occurs (like on an AJAX UpdatePanel) the startup script registered on the Page does not execute and you lose your scroll position. To combat this, you must register the startup script using the AJAX ScriptManager class. Ideally, you’d add a property to the grid to allow specification of a parent UpdatePanel then have the component register the startup script internally. However, I didn’t want to add a reference to the AJAX libraries because then the grid wouldn’t work unless the host computer has the AJAX libraries installed. Instead, I exposed the startup script as a string property and let the page register the script on the appropriate UpdatePanel.

    1. New property on the FrozenGridView.

    public string LoadScript




           if (positionfield.ClientID != null)


               string divID = String.Format(CultureInfo.InvariantCulture, "__gv{0}__div", ClientID);

               return "document.getElementById(‘" + divID + "’).scrollTop = document.getElementById(‘" + positionfield.ClientID + "’).value;";



               return string.Empty;



    2. From the page, register the LoadScript using the AJAX ScriptManager.

    Microsoft.Web.UI.ScriptManager.RegisterStartupScript(UpdatePanel1, GridView1.GetType(), "GridLoadScript", GridView1.LoadScript, true);

    Hope this helps someone.

  30. Scott Roberts says:

    I’m not sure why Microsoft hates me. I’m sure I haven’t done anything evil to them. Regardless, the GridView appears to clear the controls collection when the GridView is data bound (GridView1.DataBind()). That causes my hidden field to be cleared, which causes it not to be rendered to HTML, which breaks all of the javascript. Bleh.

    Fortunately, there’s an easy fix. Simply re-add the hidden field to the controls collection in the OnDataBound event:

    protected override void OnDataBound(EventArgs e)




       // This is the pertinent line.



    Now the hidden field (and therefore your scroll position) is maintained even when the GridView is re-bound.

  31. Mitch says:

    Hey Scott Roberts,  thanks for posting the code snippet on saving scroll postion on postback in an AJAX update panel.  This is exactly what I needed.

    I ran into one issue in point 3 where the compiler complains that divID does not exist in the current context.

    Any thoughts on how to solve?

    Thanks in advance

  32. Karen Grube says:

    I can’t seem to be able to link the Frozen Grid to a SQL Data source.  It tells me it can’t find the data source ID – Error setting value ‘dsCustomers’ to property ‘DataSourceId. The Method or Operation is not implemented.

    Any suggestions?

  33. AsbjornM says:

    I was trying to use this, but I could not find that it honour Theming and Skins?

    All styles was removed, and I still could not get the header to freeze.

    I have several grids on one page, so maybe that is the problem?

    I also use your twoline header fix.

    The gotdotnetworkspace has problems with using the sourcecontrol, so it is not possible to get something out of that.

    Maybe you could move to codeplex?

  34. Raag says:

    The row freezing is a excellent solution which i ever seen

    Is there as solution for first freezing column…?im not able to get right code…

  35. Karen Grube says:

    Okay, I’m totally lost.  Between work-around code-snippets for Ajax and a control that isn’t working using a SQL data source for me either and a really stupid simple basic need to freeze the header of a gridview and yet maintain the scroll position of a selected or edited item, I’m totally cnofused.

    Please provide a link or a zip or something with a really small but complete example of how to do this in VB using a SQL Data source on an Ajax-enabled webform that uses a master page.  I think that would help a lot of people.


  36. BGT says:

    Great article!!

    I can’t undestand when and by who the ScrollHeight and ScrollWidth are call?

  37. BGT says:

    In this example the sroll bar is in head and body. I tried to put the sroll only in body but i think that’s not possible for IE7 only for firefox

  38. Ben says:

    Ahhh! After weeks of getting "Property value is not valid" and "The method or operation is not implemented" errors when trying to set the ScrollHeight and ScrollWidth properties in the property window in the designer (VS 2005), I finally changed the name of the ScrollHeight and ScrollWidth properties in the control to ScrollGridHeight and ScrollGridWidth, respectively.  And voila, no more errors!  I am assuming that ScrollHeight and ScrollWidth are reserved html properties???  When I would try to set the ScrollHeight property using the property window, I noticed that the properties were not being persisted to the html on the aspx page.  Just don’t ask why it took me two weeks to try changing the property names.  Matt, thanks for the code.

  39. R says:

    can somebody send me aspx page with the whole code working?


  40. Helpneeded says:


    Can someone please gove me the compiled library as I dont have visual studio 2005 though I want to use the library in Visual web developer express. Please email to yousuf.khaan AT rediffmail DOT com


  41. W says:

    Any help?

    When in The VS2005 Design View, it parse the properties Task very slowly.  The BulkEdit GrivView contain <asp:TextBox> <asp:Label>

    I switch between Disign View betweenm Source View and click to use the Wizard to edit. It took very long to read the context.

    I use the VS 2005 put the .dll into

    C:Program FilesMicrosoft Visual Studio 8Common7IDE

  42. Srinivasreddy says:

    Hi every body,

    I have read this article , Its perfectly working in IE 7, But its not working in Netscapae Navigator,

    Help me

    Why its not working in Netscape navigator


  43. jags says:

    Any one has solution to fix the dropdown list isse with the fixed header?

  44. jags says:

    Any one has solution to fix the dropdown list issue with the fixed header?

  45. Badri says:


    i am having a page, and a grid inside a div which has both the scroll bars. if i fixed the header and if i have more columns then the headers are moving out of the div. how will you avoid this. you can mail me at


  46. mgrockin says:

    I could not find any discussion regarding the problem about dropdowns being present in the DataGrid. Kindly discuss this as all the solutions fail there.

  47. Ravi says:


    I have problem with dropdownlist in IE6. It floats over the fixed header of scrollable Gridview.Please help me .Its urgent.

  48. JP says:

    how to fix the header if there is drop dwon list in a template column

  49. marcus says:

    If I have dropdownlist as a Template column then it scrolls over the Header region…plz help

  50. marcus says:

    If I have dropdownlist as a Template column then it scrolls over the Header region…plz help

  51. Unni says:


    Is any one has the source code for this? please upload the same.


  52. Hi,

    I wrote a different option to the gridview Freeze header.

    in this option you don not need css or javascript

    i hope this help you.

  53. Avik Mukherjee says:

    Great article.

    Keep it up sir…

  54. Drigg says:

    Your story was featured in Drigg! Here is the link to vote it up and promote it:

  55. Enes Pekkaya says:

    Matt, thanks for these articles, they are greatfuly helpful.

    I wonder that, is there any possible situation for sorting header? For I have not been succesfull :s

    thanks right now.

  56. Stickler for Standard Coding Conventions says:

    Your code is very good. I’m coding in VB so I had to convert it, but it only took about 15 minutes to do so because you were so explanatory with your texts in all areas….except one, and I just had to comment on it because It bothers me a lot… (brace yourself, as it is vert trivial)

    Your Gridview’s name…."FreezeHeader"??????? I give you credit for your exceptional code, but please, use standard naming conventions i.e. "grdFreezeHeaderGridView"…

  57. Tom Pingry says:

    Hi Matt,

    Is there any way to change the border around the control from 3px to 1 without recompiling (with say, css or override)?

    For some reason I can only use your compiled version.  When I recompile (w/ VS2008) it won't run on our server.

    Anyway, thanks.


Skip to main content