Microsoft & .NETASPA Better Fixed GridView Header for ASP.NET

A Better Fixed GridView Header for ASP.NET

Introduction

There are several articles on fixed headers. I even wrote one called “Implementing a Fixed GridView Header in ASP.NET.” What I discovered was that the technique described in that article and some others doesn’t work very well when controls are nested or pages become complex. What I was looking for was a more durable solution.

What I started with was a technique that developers have been using for a while: shadowing a grid with a separate table. The challenge with aligning a separate control with a grid is aligning the separate control with the grid and the columns of the grid as it shrinks and grows and the columns change size. In this article, I present a better, improved spin on an old standby. Some interesting JavaScript is used to clone a fix the position of a grid header, handling runtime changes in the associated grid’s appearance.

As you read this article, you will have an opportunity to explore and expand your knowledge of JavaScript and ASP.NET, learn about object-oriented JavaScript, and gain a better understanding of the relationship between ASP.NET Web controls, HTML, and JavaScript.

An Overview of the Solution

Have you ever sat in a presentation listening to a lot of technical details without an idea of where you are going? I have. Many times it would have helped to have a thesis statement, a sort of, overview of what will be learned, why, and how we might get there. Hence this section.

In this article, you will learn a technique that will help you pin a header for a table (specifically the one generated by a ASP.NET GridView) even when the page containing the GridView scrolls. Along the way, you will have an opportunity to learn about object-oriented JavaScript and might glean a better understanding of ASP.NET web controls. If you want your grid headers to stay put, you are in the right place. If you are curious about object-oriented JavaScript, you are in the right place. If you are bored, stuck in an airport, or just curious, welcome.

This better fixed grid header solution has three elements: object-based JavaScript that manages the grid header, a stub separate HTML table for pinning the header, and some sample code that demonstrates a GridView with enough data to need scrolling. Hence, there are three sections to this article: the presentation view and the grid that has enough data to require scrolling, a simple section, the HTML tags that will ultimately represent the pin-able header, another relatively simple section, and the JavaScript that does all of the heavy lifting. The last section has all of the code.

Making a Page with a Scrollable Div and GridView

There are a couple of well-used ways to manage the location of controls. Two popular ways are the HTML Table and HTML Div. In this section, you will use a Div to house the GridView. You could make the Div scrollable or just add enough data to the GridView to make the page scroll. With the totality of the code in this article, either approach will permit the header to scroll correctly. To simplify the creation of the presentation layer, you’ll use a scrolling page and a lot of data in a GridView.

To implement a test presentation—web page—you can create a dummy class, use a generic List, and bind that list to a GridView. Before you write the code in Listing 1, drop a GridView on a web page.

Listing 1: A sample class and code that populates a GridView.

Imports System.Collections.Generic

Partial Class _Default
   Inherits System.Web.UI.Page

   Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.Load

      If (IsPostBack) Then Return

      Dim list As List(Of Sample) = New List(Of Sample)

      Dim I As Integer

      For I = 1 To 100
         list.Add(New Sample(I, "Name" + I.ToString(), _
                             I.ToString()))
      Next

      GridView1.DataSource = list
      GridView1.DataBind()

   End Sub
End Class

Public Class Sample

   Private _iD As Integer
   Public Property ID() As Integer
      Get
         Return _iD
      End Get
      Set(ByVal Value As Integer)
         _iD = Value
      End Set
   End Property

   Private _name As String
   Public Property Name() As String
      Get
         Return _name
      End Get
      Set(ByVal Value As String)
         _name = Value
      End Set
   End Property

   Private _filler As String
   Public Property Filler() As String
      Get
         Return _filler
      End Get
      Set(ByVal Value As String)
         _filler = Value
      End Set
   End Property

   ''' <summary>
   ''' Initializes a new instance of the Sample class.
   ''' </summary>
   ''' <param name="iD"></param>
   ''' <param name="name"></param>
   ''' <param name="filler"></param>
   Public Sub New(ByVal iD As Integer, _
      ByVal name As String, ByVal filler As String)
      _iD = iD
      _name = name
      _filler = filler
   End Sub

End Class

You can use anything to populate the GridView; it really isn’t relevant to the discussion. All you should care about is that you have something that generates an HTML table and enough data to make that table need to scroll.

It is worth noting that ASP.NET web controls are HTML generators that spit out HTML on the server. Basically, if it looks like a duck, it is a duck. That is, a GridView is actually rendered as an HTML table, so this technique will work on anything that is ultimately an HTML table.

Adding the HTML Div and Table Stubs

The GridView will have a header. For the technique for pinning it, you could use the GridView’s actual header, but is actually a little easier to use a clone. The reason is that if you pin the GridView’s actual header, you have to adjust its position differently when you have scrolled to the top (or you hide row 1), and you might have to account for a positioned header’s index when manipulating the data. For these reasons, I elected to use a stub div and table to mimic the fixed header.

Tip: Did you know that a table header is rendered as a <TR> with <TH> children instead of <TD> (cells) children? Did you also know that you can place the row containing the header (or <TH> cells) anywhere in a Table? You can.

To add the div and table stub, open your web page and add the following HTML just above the GridView’s tag. Here is the additional HTML followed by complete HTML/ASP for the sample form (in Listing 2).

<div id="fixedHeader" style="position: absolute">
   <table id="header" style="position: absolute">
   </table>
</div>

Listing 2: The complete listing for the sample web page, showing the relationship of the GridView and the HTML that will be used to affix a header.

<%@ Page Language="VB" AutoEventWireup="true"
         CodeFile="Default.aspx.vb"
         Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html  >
<head id="Head1" runat="server">
<title>Fixed Header</title>
<script language="javascript"
        src="fixedHeader.js"
        type="text/javascript"></script>

<script language="javascript" type="text/javascript">

   InitializeFixedHeader("GridView1");

</script>

</head>
<body>
<form id="form2" runat="server">
<div id="fixedHeader" style="position: absolute">
   <table id="header" style="position: absolute">
   </table>
</div>
<div>
<asp:GridView ID="GridView1"
              runat="server"
              Height="200px"
              Width="574px">
   <HeaderStyle BackColor="Navy"
                Font-Bold="True"
                ForeColor="White" />
</asp:GridView>
</div>
</form>
</body>
</html>

The control whose ID is fixedHeader and the nested control whose ID is header will ultimately play the role of pinned header. Notice that the <TR> tag containing the <TH> cells is missing. It is the JavaScript that will clone and insert this row and make sure its position is correct that you have to tackle next.

Implementing the JavaScript to Manage the Header over Its Lifetime

This part by far is the most complicated. The bad news is that this solution uses object-oriented JavaScript and the code is a little heavy. The good news is that the code is presented as a .js script file and you only have to write it once, get it working correctly (debug it), and you are done.

To recap, you have a control that renders an HTML table (or an actual HTML table will work). You have a <div> with a <table> that will be used to represent a pinned table, and these two controls will be absolutely positioned (see Listing 2). Finally, you need a couple of hundred lines of JavaScript to handle scrolling, positioning, and resizing of the containing page.

Defining a JavaScript Class

There is a resurgent in interest in JavaScript because of Ajax. Asynchronous ASP.NET/HTTP depends on JavaScript, but JavaScript is powerful in its own right and can still be used to create a better client-web experience (as you are doing here). That said, JavaScript is not really object-oriented but it’s close enough.

JavaScript classes are really functions that you use with the new keyword. These special class-like functions can contain fields and methods. Everything is internal (think private in OOP vernacular) unless you explicitly indicate that external callers can have access. So, to define a “class” in JavaScript, you simply define a function and create an instance with the new keyword. Listing 3 contains a really simple example of a page containing a JavaScript class.

Listing 3: A web page containing a JavaScript “class.”

<%@ Page Language="VB" AutoEventWireup="false"
         CodeFile="Default2.aspx.vb" Inherits="Default2" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html  >
<head runat="server">
   <title>Untitled Page</title>
<script language="javascript">

function Foo()
{
   alert("This is a simple JavaScript class");
}

var foo = new Foo();


</script>
</head>
<body>
   <form id="form1" runat="server">
   <div>

   </div>
   </form>
</body>
</html>

In the listing, the function Foo is treated like a class and the statement alert… basically plays the role of initialization (or construct) code. The statement var foo = new Foo(); ultimately creates an instance of Foo (upper case) and runs the initialization code.

You could move the alert statement to a function within Foo and invoke that function. This will give you a better sense of the object-oriented nature of JavaScript when used in this manner (see Listing 4).

Listing 4: Adding a member function to Foo.

<%@ Page Language="VB" AutoEventWireup="false"
         CodeFile="Default2.aspx.vb" Inherits="Default2" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html  >
<head runat="server">
   <title>Untitled Page</title>
<script language="javascript">

function Foo()
{
   this.MemberFunc = memberFunc;
   function memberFunc()
   {
      alert("This is a simple JavaScript class");
   }
}

var foo = new Foo();
foo.MemberFunc();


</script>
</head>
<body>
   <form id="form1" runat="server">
   <div>

   </div>
   </form>
</body>
</html>

In the revision, Foo has a member function called memberFunc (notice the case) and makes it accessible externally by assigning it to this.MemberFunc. Without the this.member statement, the member is treated as inaccessible externally.

Cloning the Grid Header

To begin wrapping things up, you now can implement what I refer to as the PositionClass. The PositionClass will figure out where the grid is, where the header is, and keep track of where the header should be depending on the scroll position and the size of the window containing the grid. Listing 5 is a couple of hundred lines long, but a complete explanation is offered after the listing. To get started, add a new JavaScript file to your solution and put the code in Listing 5 in that file.

Listing 5: JavaScript code to manage and position a row containing header information.

// JScript File - by PTK
// Implements a fixed header for HTML tables (like a GridView,
// which renders and HTML table)
function PositionClass()
{
   this.Top          = top;
   this.Left         = left;
   this.Width        = width;
   this.Height       = height;
   this.ClientID     = clientID;
   this.SavePosition = savePosition;
   this.Reposition   = reposition;
   this.SetClientID  = setClientID;
   var top, left, width, height, clientID;
   var head, caption;

   // the client ID of the table/gridview
   function setClientID(id)
   {
      clientID = id;
   }

   // determines the absolute position of X or Y--determined by
   // the getOffset function to handle control nesting
   function getAbsolutePosition(control, getOffset, adjustCaption)
   {
      var result = 0;
      while(control)
      {
         if(control.tagName)
            if((control.tagName == "TBODY") ||
               (control.tagName == "TR"))
            {
               if(control.parentElement)
                  control = control.parentElement;
               else
                  break;
               continue;
            }

         if(control.style.position == "absolute")
            return result;

         result += getOffset(control);

         if(control.parentElement)
            control = control.parentElement;
         else
            break;
      }

      if(adjustCaption && caption && isAlignedTop(caption))
         result -= caption.clientHeight;
      return result;
   }

   // returns the x offset
   function getXOffset(control)
   {
      if(control.offsetLeft)
         return control.offsetLeft;
      else
         return 0;
   }

   // returns the y offset
   function getYOffset(control)
   {
      if(control.offsetTop)
         return control.offsetTop;
      else
         return 0;
   }

   // get the grid header row. It may not exist or there may be a
   // caption above it
   function getHeaderNode(grid)
   {
      if(!grid) return null;
      for(var i=0; i<grid.rows.length; i++)
      {
         var s = new String();
         if(grid.rows[i].childNodes.length > 0)
         {
            s = grid.rows[i].childNodes[0].tagName;
            if(s.toLowerCase() == "th")
               return grid.rows[i];
         }
      }
      return null;
   }

   // everything but "bottom" is top=aligned
   function isAlignedTop(theCaption)
   {
      if(!theCaption) return false;
      var tag = theCaption.align.toLowerCase();
      return (tag == "top") || (tag == "left") || (tag == "right")
         || (tag == "");
   }

   // find the caption; we have to allow for a caption above the
   // header
   function getCaptionNode(grid)
   {
      if(!grid) return null;
      return grid.caption;
   }

   // stores the current position of the control
   function savePosition()
   {
      // debugger;
      // get the grid
      var grid = document.all[clientID];
      if(!grid) return;
      header.cellPadding = grid.cellPadding;
      header.cellSpacing = grid.cellSpacing;
      header.border      = grid.border;
      header.bgColor     = grid.bgColor;
      header.className   = grid.className;
      caption = getCaptionNode(grid);
      if(caption && (caption.style.backgroundColor == ""))
      {
         // debugger;
         caption.style.backgroundColor = "white";
      }
      head = getHeaderNode(grid);
      if(head == null) return;
      head.style.visibility = "hidden";
      // get the header position
      top      = getAbsolutePosition(head, getYOffset, true);
      left     = getAbsolutePosition(head, getXOffset, false);
      width    = head.clientWidth;
      height   = head.clientHeight;
      var temp = head.cloneNode(true);
      // store sizes in style attributes
      for(var i=0; i<head.childNodes.length; i++)
      {
         temp.childNodes[i].style.width =
            head.childNodes[i].clientWidth;
         temp.childNodes[i].style.height =
            head.childNodes[i].clientHeight;
      }
      head = temp;
      // clone the header
      head.style.visibility = "visible";
      // insert the header (and caption) into our copy table
      if(caption && isAlignedTop(caption))
      {
         var newCaption = caption.cloneNode(true);
         if(header.caption)
            header.replaceChild(newCaption, header.caption);
         else
            header.appendChild(newCaption);
         caption.style.visibility = "hidden";
      }

      // place the new table header in the table-playing header
      var th = getHeaderNode(header);
      if(th)
         header.childNodes[0].replaceChild(head, th);
      else
         header.childNodes[0].appendChild(head);

      // fix the position of the div to overlap the gridview's header
      fixedHeader.style.posLeft    = left;
      fixedHeader.style.left       = left + "px";
      fixedHeader.style.posTop     = top;
      fixedHeader.style.top        = top + "px";
      fixedHeader.style.width      = width + "px";
      fixedHeader.style.height     = height + "px";
      fixedHeader.style.visibility = "visible";
      fixedHeader.style.zIndex     = 0;
   }

   // repositions the control if necessary
   function reposition()
   {
      if(!document.all[clientID]) return;
      // added silent try..catch because masterpages can have other
      // things that scroll;
      // for instance we scroll the treeview that fires this event
      try
      {
         if(document.body.parentNode.scrollLeft > 0)
         {
            fixedHeader.style.posLeft =
               document.body.parentNode.scrollLeft;
            fixedHeader.style.left =
               document.body.parentNode.scrollLeft + "px";
         }
         else
         {
            fixedHeader.style.posLeft =
               document.body.parentNode.scrollLeft + left;
            fixedHeader.style.left =
               document.body.parentNode.scrollLeft + left + "px";
         }
         if(document.body.parentNode.scrollTop > 0)
         {
            fixedHeader.style.top =
               document.body.parentNode.scrollTop + "px";
            fixedHeader.style.posTop =
               document.body.parentNode.scrollTop;
         }
         else
         {
            fixedHeader.style.top =
               document.body.parentNode.scrollTop + top + "px";
            fixedHeader.style.posTop =
               document.body.parentNode.scrollTop + top;
         }
         fixedHeader.style.width      = width + "px";
         fixedHeader.style.height     = height + "px";
         fixedHeader.style.visibility = "visible";
      }
      catch(oException)
      {
      }
   }
}

The first ten lines or so beginning with this of var define public members and fields for the PositionClass. As you might expect, you need to store the position and instance of the header as well as the caption, if present. It is these items that you will need to resize (absolutely) if the grid scrolls or the window is scrolled or resized.

Any element that you want to be a public member you need to assign to a local-named element with the this keyword, for instance this.Width = width. Statements like the preceding will introduce an externally accessible member Width (note the capital W) and associate it with variable width (note the lowercase w). This relationship is similar to the field and property relationship in VB. (It is worth noting that JavaScript is case sensitive.)

Because you are defining controls such as a GridView in code-behind, and these controls will generate an HTML table with a specific client-id, it is convenient to store the actual client-id in the PositionClass. setClientID does this. In simple pages, you might hard code the client-id with the identifier given in the code-behind. For example, on a simple page a GridView control with the name GridView1 may actually get a client-id of “GridView1”. However, when you build more complicated pages with user controls, you are more likely to get a client-id that includes all of the names of parent controls concatenated together.

The getAbsolutePosition function determines the X or Y position of the header by adding all of the offsets of its parent controls together. The TBODY and TR parent controls are ignored, and this approach seems to yield the best result. The argument getOffset is actually a function argument. It is initialized with either of the functions getXOffset or getYOffset depending on which position—horizontal or vertical—you are resolving. The adjustCaption argument is used to determine whether you should adjust the vertical offset for a top-aligned caption.

The getHeaderNode function walks all of the HTML table rows—the grid rows—to find the row—TR control—containning the table headers—<TH> elements. It is worth noting that you actually could position the real table’s row containing table headers. That is, table headers, or any row for that matter, can be positioned absolutely with script regardless of its index in the table. This means that, instead of cloning the table header row and positioning the clone, you could position the original table header row. This is left as an exercise. The challenge with the latter approach is that the fixed table header will overlap row 1 when the grid is scrolled to the top-most row.

The isAligned function checks to see whether the table has a caption and its align property is top, left, right, or blank (“”). For all intents and purposes, any of these alignments mean you need to leave room for the caption when positioning a fixed header.

The savePosition function does all of the heavy lifting of storing the present position of the header, cloning it, duplicating its styles, and positioning the cloned-header correctly. Finally, reposition is called when the window (or panel, or anything that scrolls that the grid sits on) scrolls or is resized. This function figures out how far the container was scrolled and adjusts the fixed header accordingly.

Writing an Initialization Function in JavaScript

The final two steps are to write and initialize an invoke function that creates and locates the fixed header and inject a little JavaScript that ensures you have the correct client-id of the grid/table whose header you’d like to fix. Listing 6 shows a function—InitializeFixedHeader—that is not a member of the PositionClass but that will initialize an instance of the PositionClass and bind its behaviors to window events, including onload, onscroll, and onresize.

Listing 6: Initializing the fixed header class, PositionClass.

function InitializeFixedHeader(id)
{
   var pc = new PositionClass();
   pc.SetClientID(id);
   window.onload = pc.SavePosition;
   window.onscroll = pc.Reposition;
   window.onresize=function()
   {
      pc.SavePosition();
      pc.Reposition();
   }
}

Injecting the JavaScript Initialization Code

As you write more complication web applications and reuse user controls, you will need to ensure that the client-id passed to the ID argument in Listing 6 is correct. This can be done easily by injecting some startup script from the code behind. The simple code in Listing 7 demonstrates how you can inject a call to InitializeFixedHeader with the GridView’s ClientID property, ensuring the JavaScript id matches the name ASP.NET gives the GridView on the client.

Listing 7: Injecting code to initialize the fixed header.

Protected Sub Page_Load(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles Me.Load

   Dim Script As String = _
      String.Format("InitializeFixedHeader('{0}');", _
         GridView1.ClientID)

   Page.ClientScript.RegisterStartupScript(Me.GetType(), _
      "fixedHeader", Script, True)

Don’t use Response.Write to inject script any longer—if you ever did. The Document Type Definition (DTD) XHML 1.0—the new DTD standard for ASP.NET 2.0—gets wonky and you can get some subtle and annoying behaviors by injecting script with Response.Write.

Summary

This sample is long and by no means easy. JavaScript is harder to debug than VB, and it’s harder to code correctly because it is syntactically less forgiving. However, JavaScript can add some cool, client-side behaviors to your ASP.NET and VB solutions, so JavaScript (or VBScript) is worth learning. (You will also need JavaScript to make the most of Atlas/Ajax.)

In this article, you learned about object-oriented or object-based JavaScript. And, you learned how to fix a grid header no matter how complicated or nested your pages become. There are shorter techniques—including a previous one that I wrote. These shorter techniques only seem to work on fairly simple pages, though.

About the Author

Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his new book UML DeMystified from McGraw-Hill/Osborne. Paul is a software architect for Tri-State Hospital Supply Corporation. You may contact him for technology questions at pkimmel@softconcepts.com.

If you are interested in joining or sponsoring a .NET Users Group, check out www.glugnet.org. Glugnet is opening a users group branch in Flint, Michigan in August 2007. If you are interested in attending, check out the www.glugnet.org web site for updates.

Copyright © 2007. All Rights Reserved.
By Paul Kimmel. pkimmel@softconcepts.com

Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.
Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.

Latest Posts

Related Stories