Friday, March 6, 2009

ASP.NET GridView Scrollable area and Fixed Header Solution

I have search long and hard and spent much of my own time trying to find a good Fixed Header and Scrollable rows for the ASP.NET GridView. I think I have finally come up with a combination of my own efforts and the best of borrowed from other solutions. Unfortunately, I didn't keep track of where I got some of the code that I borrowed. My apologies to the original authors.

This solution attempts to remedy all the buggy solutions I found on the internet. Some of the common problems were

  • The solution did work for the GridView
  • The solution assumed we had more control of the generated html from the GridView
  • The solution required modifying the html generated by the GridView
  • The solution stopped working when windows is resized
  • The solution assumed that there was only one scrollable area on the page. (To be fair, my solution doesn't assume this, but it would require some duplication of style sheets, jscript code, etc.)

Here is a sample implementation of my solution. It works quite well with only one scrollable area that has a fixed header. You will need to change duplicate the JavaScript file and CSS and all references to "container" if want more than one scrollable area that has a fixed header. Another alternative is to generate a customized .css .js file based on a special url. That is beyond the scope of this though.

There are several key things I would like to point about the .aspx page. The .js and .css are just really items you need to reference and don't really require any changes (except as noted above). So, I really just want to highlight what you would need to add to your page (that has a working GridView that does not have any special Fixed column header) to make this solution work.

  1. The DOCTYPE line is VERY important. This is NOT the default that Visual Studio adds to your page. Replace the line that Visual Studio puts in your .aspx page with the one shown below.
  2. Copy and Paste the GridView1_PreRender event handler to your .cs file. If you have a different name for your GridView you will need to change references to it to make the name you gave it. You will also want to set the height and width that you want. A word of warning, I did not implement the width yet, so actually that doesn't do anything. Currently the width of the GridView is not set here. Don't forget to change the references to GridView1 in the literals to match the name you use. Also, un/comment the appropriate example. If you want the GridView to be the Maximum height available and expand as the window resizes use the SetFixedHeaderWithMaxHeight call. Otherwise, if you want a fixed height, use the SetFixedHeader example.
  3. Register the GridView1_PreRender event handler with the GridView1. One easy way to do this is to add the following line to the GridView1 tag.
    OnPreRender="GridView1_PreRender"
    NOTE: You will of course need to make this match what you specified in step 2.
  4. Create a .css file by copying the CSS lines below into its own file. Alternatively, you could just include it in a style tag in the head of the page, though I don't recommend this approach.
  5. Create a .js file by copying the JavaScript lines below into its own file. Alternatively, you could just include it in a script tag in the head of the page, though I don't recommend this approach.
  6. Reference the .js and .css file in the head of the page.
  7. Copy and Paste the HeaderStyle tag in the GridView.
  8. Copy and Paste the DIV tag that has the id="container" and is directly around the GridView. It is important that there is no other DIV tags or other tags in between the DIV and the GridView. If you change this, you will need to make changes to the CSS and JavaScript.

<%@ Page Language="C#" %>

<!-- This comment keeps IE6/7 in the reliable quirks mode -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "
http://www.w3.org/TR/html4/loose.dtd">


<script runat="server">
    protected void GridView1_PreRender(object sender, EventArgs e)
    {
        if (GridView1.Rows.Count > 0)
        {
            //This replaces <td> with <th> and adds the scope attribute
            GridView1.UseAccessibleHeader = true;

            //This will add the <thead> and <tbody> elements
            GridView1.HeaderRow.TableSection = TableRowSection.TableHeader;
        }

        GridView1.Style["border-collapse"] = "separate";

        string height = "450px";
        string width = "200px";
        string gv1FixedHeaderJScript = string.Format("SetFixedHeader('{0}', '{1}', '{2}');", GridView1.ClientID, height, width);

        // MAX Height Example
        string gv1FixedHeaderJScript = string.Format("SetFixedHeaderWithMaxHeight('{0}', '{1}', '{2}');", GridView1.ClientID, width, "20px");

        // Fixed Height Example
        //ScriptManager.RegisterStartupScript(this, this.GetType(), "gvGridView1FixedHeaderKey", gv1FixedHeaderJScript, true);


    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>GridView Test</title>
    <link href="FixedHeader.css" rel="stylesheet" type="text/css" />
    <script language="JavaScript" src="FixedHeader.js"></script>
</head>
<body>
    <form id="form1" runat="server">
   
     <div id="container" style="border-style:none;">
        <asp:GridView ID="GridView1" runat="server" DataSourceID="ObjectDataSource1"
            OnPreRender="GridView1_PreRender">
         <HeaderStyle CssClass="DataGridFixedHeader" />
        </asp:GridView>
       
    </div>
        <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="GetDelinquentActions"
            TypeName="DAL"></asp:ObjectDataSource>
    </form>
</body>
</html>


// JScript File

// This is the function that gets called to resize the scrollable area to the size we want.
// We have to use the setTimeout() with a 1ms delay IF we call this function
// from within the form tag instead of just before the bottom of the body end tag.
// The reason is that in IE any of the height and width (scrollHeight, offsetHeight, style.height)
// are not set until after the form has finished rendering. This means, that the call to the
// SetFixedHeader2 method must occur after the form has been rendered. When using master pages
// as we are doing the location we need can only be specified in the .aspx page of the master page.
// Since we use this setting for different pages we can't hard code it there.
// Also, we can't use the ScriptManager.RegisterStartupScript unless we use the
// setTimeout either. This is because using this method puts the code just before the form end tag,
// which is again not where we need it to be. However, using setTimeout allows us to use this method.
function SetFixedHeader (gvClientID, height, width)
{
    var expr = "SetFixedHeader2('" + gvClientID + "', '" + height + "', '" + width + "')"
    setTimeout(expr, 1);
}

// This is essentially the same as SetFixedHeader function except the height is
// always going to be the height on the body - bottomMargin - top of GridView.
// bottomMargin is the number of pixels that create the gap between the bottom of
// the window and the bottom of the GridView
function SetFixedHeaderWithMaxHeight (gvClientID, width, bottomMargin)
{
    // get the max height that the GridView can be
    var maxHeight = getMaxHeight() - parseInt(bottomMargin);
   
    // We need to resize the GridView when the Window is resized
    window.onresize = MaximizeGridViewScrollableArea;
       
    var expr = "SetFixedHeader2('" + gvClientID + "', '" + maxHeight + "', '" + width + "')"
    setTimeout(expr, 1);
   
    gridViewClientID = gvClientID;
    desiredHeight = maxHeight;
    desiredWidth = width;
    desiredBottomMargin = bottomMargin;
}

function MaximizeGridViewScrollableArea()
{
    SetFixedHeaderWithMaxHeight(gridViewClientID, desiredWidth, desiredBottomMargin);
}

var gridViewClientID = null;
var desiredHeight = null;
var desiredWidth = null;
var desiredBottomMargin = null;

// height - string - the height of the scrollable area in pixels (not percent) i.e. 330px
// width - string - the width of the scrollable area in pixels or percent i.e. 600px or 50%
function SetFixedHeader2 (gvClientID, height, width)
{   
    // get numeric values for height and width
    var heightNum = parseInt(height);
   
    // adjust the size since the grid view needs to be slightly smaller
    // than the container div
    var heightAdjustment = 40;
    heightNum = heightNum - heightAdjustment;
   
    // do we need scrolling or not?
   
    var gv = document.getElementById(gvClientID);
    var tbody = null;
   
    // loop through the four (or fewer) child nodes of the table
    // and find the tbody node
    for (var i=0; i<gv.childNodes.length; i++)
    {
        var child = gv.childNodes[i];
       
        if (child.tagName)
        {
            if (child.tagName.toUpperCase() == "TBODY")
            {
                tbody = child;
                // we found what we needed, exit the loop
                i = gv.childNodes.length;
            }
        }
       
    }
   
    if (tbody != null)
    {
   
        var gvDiv = GetDivGeneratedByGridView();
       
        // scrolling is needed
        if (parseInt(tbody.scrollHeight) > parseInt(heightNum))
        {
        //alert('needs scrolling');
            //tbody.style.height = height;
            tbody.style.height = (heightNum) + "px"
                     
            if (gvDiv != null)
            {
                // add the height adjustment back in for the container, so it is bigger
                gvDiv.style.height = (heightNum + heightAdjustment) + "px";
            }
        }
        // scrolling is NOT needed
        else
        {
        //alert('NO scrolling');
            tbody.style.height = '100%'
           
            if (gvDiv != null)
            {
               
                gvDiv.style.height = "100%";
            } 
        }        
    }
}

// returns the DIV surrounding the GridView.
// NOTE: This is NOTE the DIV with id="container" that we added.
// This is the DIV that is generated by the GridView when it is rendered.
// This the DIV between teh DIV with id="container" and the table that is
// generated by the GridView.
function GetDivGeneratedByGridView()
{
    // set the size of the container div to be just a little bigger
    // than the grid view
    var container = document.getElementById("container");
   
    var isIE = typeof container.children == 'object';
    var gvDiv = null;
   
    if (isIE)
    {
        gvDiv = container.children[0];   
    }
    else // Firefox
    {
        // NOTE: First childNode is a textnode that is a new line
        gvDiv = container.childNodes[1];   
    }
   
    return gvDiv;
}


// get the max height the GridView can have
// NOTE: This is based on where the GridView is vertically on the page
//       For example, if the GridView is 100 pixels from the top of the
//       top of the body, then this will return the height of the body - 100.
function getMaxHeight() {
  var div = GetDivGeneratedByGridView();
 
  myHeight = 0;
//  alert(document.body.topMargin);
  if( typeof( window.innerWidth ) == 'number' ) {
    //Non-IE
    myHeight = window.innerHeight;
  } else if( document.documentElement && document.documentElement.clientHeight) {
    //IE 6+ in 'standards compliant mode'
    myHeight = document.documentElement.clientHeight;
  } else if( document.body && document.body.clientHeight) {
    //IE 4 compatible
    myHeight = document.body.clientHeight;
  }

  var maxGridViewHeight = myHeight - div.offsetTop;

  return maxGridViewHeight;
 
 
}



/*** The Fixed Header Stylesheet ***/

.DataGridFixedHeader { POSITION: relative; TOP: expression(this.parentNode.parentNode.parentNode.scrollTop-1);}

#container div {
 overflow: auto; /* so the extra columns and rows flow as needed */
 margin: 0 auto;
 }

#container table {
 width: 99%;  /*100% of container produces horiz. scroll in Mozilla*/
 
 /* Gets rid of the 1 pixel space on the top of the header that shows through when scrolling */
 border: none ! important;
 }
 
#container table>tbody {  /* child selector syntax which IE6 and older do not support*/
 overflow: auto;
 overflow-x: hidden;
 }
 
#container thead tr {
 position:relative;
 top: expression(offsetParent.scrollTop); /*IE5+ only*/
 }
 
#container table tfoot tr { /*idea of Renato Cherullo to help IE*/
      position: relative;
      overflow-x: hidden;
      top: expression(parentNode.parentNode.offsetHeight >=
   offsetParent.offsetHeight ? 0 - parentNode.parentNode.offsetHeight + offsetParent.offsetHeight + offsetParent.scrollTop : 0);
      }

#container td:last-child {padding-right: 20px;} /*prevent Mozilla scrollbar from hiding cell content*/

#container thead td, thead th {
 
 /* the background color for the header to something other than transparent
  so that the rows don't show behind while scrolling */
 background-color:white;
 
 }
 
 
/*** Purely Cosmetics ***/

#container div {
 width: 99%;  /* table width will be 99% of this*/
 height: 50px;  /* a small efault value so user won't really see resize if delay rendering 1ms. it is changed by the SetFixedHeader() javascript function. Must be greater than tbody*/
 } 

/*** print style sheet ***/

@media print {

#container div {overflow: visible; }
#container table>tbody {overflow: visible; }
#container td {height: 14pt;} /*adds control for test purposes*/
#container thead td {font-size: 11pt; }
#container tfoot td {
 text-align: center;
 font-size: 9pt;
 border-bottom: solid 1px slategray;
 }
 
#container thead {display: table-header-group; }
#container tfoot {display: table-footer-group; }
#container thead th, thead td {position: static; }

#container thead tr {position: static; } /*prevent problem if print after scrolling table*/
#container table tfoot tr {     position: static;    }

}


/*** Global Print Styles ***/
@media print {

.noprint {display: none;}

body {
 font-family:"Palatino Linotype", Georgia, Garamond, serif;
 background-image: none;
 }
 
#container {
 border: none;
 padding: 0;
 }

}

20 comments:

Anonymous said...

This works well and it's pretty straightforward.
I have one question. I am adding a 2nd header to my grid. How could I get that included in the "thead" section?

Thanks - this is great.

Brent V said...

Hi anonymous,

Thank you for the kudos. I don't understand what you mean by a 2nd header to your gridview. What are you doing to do this?

Thanks,

Brent

Anonymous said...

Hi Brent,

I'm adding the new GridViewRow row in the PreRender. I just changed the type form DataControlRowType.Separator to DataControlRowType.Header there and now the 2 'heading' rows are fixed. Yeah!

I really like this solution!
Anne

Brent V said...

Anonymous,

Thank you again for the kind words. It sounds like you have it figured out now. Good job!

Let me know if you have any issues using it.

Thanks,

Brent

Nabeel Faruqui said...

Not working in firefox. Any idea ? Try latest version of firefox and it will not be working.

thanks.

Brent V said...

Nabeel,

It is hard for me to say why it is not working for you. I would recommend checking the steps again. Perhaps you forgot something.

It works fine for me in FireFox 3.0.11 which is the newest that I am aware of. Are you using a beta version of FireFox?

What exactly isn't working? Are you using it in full screen or fixed size for the GridView?

Let me know,

Thx,

Brent

Kara said...

hai Brent,

This is working perfectly! but in ie when i increase the container height(fixed height), row height also getting increase. so any idea?

Brent V said...

Hi Karla,

Sorry for the delay. This is a good point. I have not tested this, but looking through my actual code, I think you need to set the height of your rows. I didn't notice that issue on my implementation because I set the height of the rows.

To set the height there are many ways to do this. I did this via ASP.NET Skin File. In the skin I told it to use use a particular CSS class for RowStyle and AlternatingRowStyle. In the css class I specify the height. In any event, you need to somehow set the height of the rows.

Here is an example of my css styles:

.GridViewAlternatingRow
{
font-family: Tahoma, Arial, Helvetica;
font-size: 12px;
height: 15px;
font-weight: normal;
text-align:left;
color: #003063;
background-color: #FFFFFF;

}


.GridViewRow
{
font-family: Tahoma, Arial, Helvetica;
font-size: 12px;
height: 15px;
font-weight: normal;
text-align:left;
color: #003063;
background-color: #d9e6ff;

}

Let me know if that doesn't work for some reason.

Thank you for the feedback.

Brent

Kara said...

hi Brent,

Thanks for your feedback! I' l come to u as soon.

cheers,
Kara

Brent V said...

Kara,

Sorry for spelling your name incorrectly. I was in too big of a hurry. My apologies. I hope that helped. I'll see if I can fix it. :)

Brent

Kara said...

hay Brent it doesn' t matter! 'Kara' is my yahoo login name.. Actually my real name is Hiran! he he..

These days i' m also busy with office work!

I have another issue to ask from u! related to same scenario...

I' l come to u as soon as possible.

Thank u very much for your cooperation Brent!

cheers,
Kara

Kara said...

hi Brent,

Thank u very much! That container height problem has been fixed!

Thanks again!

One of my project member came up with a problem after implementing fixed header! The page which i implemented there is an update process going! when the user click the update button and after that will display message saying "successfully updated". after that fixed header data grid scroll will lose! it completely getting disappear.
Bent has faced this type of problem before? Could i know about that if happened!
and can i use this same method for asp ListView data grid? any thoughts!

Thank u!


cheers,
Kara

Brent V said...

Hi Kara,

I have seen the GridView disappear, but not with my fixed headers solution though. That was one of the reasons I came up with my solution was that simpler approaches had a problem with this. Especially when the browser is resized. That is the reason for the using JavaScript quite a bit if I remember correctly.

One other thought, are you posting back and losing the scroll position? If that is the case, that would have to be added to the JavaScript code.

As far as the solution working with a ListView, I have not tried it, but my solution requires that specific html elements must be in place in order for the stylesheet to find the items correctly. I would say, if you make sure you ListView has all the right table, tbody, td, th, tr tags that it may work, but will likely require some more modification. Let me know if you get that to work.

I hope this helps.

Brent

James said...

great post.

I'm going to try it inside of a User control...any tips on going in that direction?

Brent V said...

Hi James,

I think it will work ok in a user control. Some of the issues you will need to address will be registering the javascript so that when it is included on the page multiple times you don't duplicate the code. Also, you may need to modify the code if any variables are not local. The actual references to the css and javascript files will need to be done programmatically, which is a bit more complicated, but not bad if I remember correctly. On the other hand the DOCTYPE line needs to go at the top of the page. Not a big deal, but not totally encapsulated in the user control either.

Brent

John Eric Sobrepena said...

Hi Brent,

You can also look at my solution here: Extend ASP.NET GridView to Support Fixed Header, Fixed Footer and Scrollable Content

vishal said...

hi Brent,

Fixed header solution not working for firefox 3.5.

Seeking ur help,
Vishal Mishra

Er. Pulkit Goel said...

hey Brent I want to fix my gridview header i am using a css for this purpose that works fine with IE but working with Firefox whole get shaken when scroll bar is moved please help me

sena reddy said...

not working in firefox 3.5..

header is not fixed.

Anonymous said...

Build scrolling of GridView control in C#.NET