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: http://blogs.msdn.com/mattdotson/articles/541795.aspx

 

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. 

 

<div>

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

        <tr>

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

            ...

        </tr>

        <tr>

            <td>Data</td>

            ...
        </tr>
    </table>
</div>

 

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 (http://web.tampabay.rr.com/bmerkey/examples/locked-column-csv.html).  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)

{

    base.OnPreRender(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

{

    get

    {

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

        {

            return "scroll";

        }

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

        {

            return "auto";

        }

        else

        {

            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

{

    get

    {

        return base.Height;

    }

    set

    {

        base.Height = value;

    }

}

 

public override Unit Height

{

    get

    {

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

        if (null == val)

        {

            return Unit.Empty;

        }

 

        return (Unit)val;

    }

    set

    {

        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");

writer.RenderBeginTag(HtmlTextWriterTag.Div);

 

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 @ http://www.codeplex.com/ASPNetRealWorldContr .  You can actually see what I am working on next if you look around!!