I CAN HAS CODE?!?

Posts Tagged ‘WebForms’

WebForms Knows Better Than You – Part Duex!

Thursday, July 17th, 2008

It must just be my luck.  This is the second time in not so many weeks that I’ve ran into an issue with WebForms where something decides that it knows better than me, ignores what I tell it, and goes off on its own merry way.  At least it offers a good opportunity to break out .NET Reflector and do some detective work.

Last time we looked over an issue with the CssTextWriter writing out invalid “background-image” attributes.  This time we are going to be looking over an issue with the GridView and it mis-calculating column span for pager sections when adding cells to the GridView in a very specific manner.

Now, there are several ways to go about adding cells to the GridView.  The most common way, and generally the simplest way, is to add an “empty” column to the Columns collection and doing what you will with it, either with a template or by modifying in OnRowCreated.  This works for most situations, spans correctly, and is the easiest to manage.  Let’s imagine for a second, though, that you cannot do it that way.  Perhaps you want a little more fine grained control over where those cells get added, or perhaps, as it was in my case, you’ve already extended the GridView and have a lot of existing code in consumers of your GridView that rely on an explicit ordering of the cells and spectacularly explode when you add the cells this way.  Now, how do you go about adding the cells?

Before we get into the code of adding the cells, let’s just get a basic extended GridView setup:

public class MyGridView : GridView
{
}
<wfs:MyGridView ID="MyGridView" runat="server" AutoGenerateColumns="false" Width="300" PageSize="4" AllowPaging="true">
    <PagerSettings Mode="NumericFirstLast" Position="TopAndBottom" />
    <PagerStyle BackColor="SteelBlue" ForeColor="White" />
    <HeaderStyle BackColor="Gainsboro" />
    <Columns>
        <asp:BoundField HeaderText="One" DataField="one" />
        <asp:BoundField HeaderText="Two"  DataField="two" />
        <asp:BoundField HeaderText="Three"  DataField="three" />
    </Columns>
</wfs:MyGridView>

Now, the way we are going to add our extra TableCells is by overriding OnRowCreated and adding the cells *after* calling base.OnRowCreated().  By doing the manipulation after calling base, any consumers that manipulate the cells will be working with the un-modified cell list.

base.OnRowCreated(e);
if (e.Row.RowType == DataControlRowType.DataRow ||
    e.Row.RowType == DataControlRowType.Header ||
    e.Row.RowType == DataControlRowType.Footer)
{
    TableCell myCell = new TableCell();
    myCell.Text = "AMG";
    e.Row.Cells.AddAt(0, myCell);
}

Now, if we go and view the site, it looks like this:

original-after-col

Doh!  That’s not quite what we were expecting.  If you go and inspect the HTML you will quickly notice that the “colspan” attribute on the top and bottom pagers is incorrect.  In fact, it’s off by one and is the same as the original number of columns.  Well, so how do we get rid of it.  The way you would expect to get rid of it is to override the InitializePager method of the GridView.  The InitializePager method accepts a parameter for the “colspan”, so it should be easy to just override it, add +1 to the input “colspan” parameter and pass it back to the base, right?

protected override void InitializePager(GridViewRow row, int columnSpan, PagedDataSource pagedDataSource)
{
    columnSpan += 1;

    base.InitializePager(row, columnSpan, pagedDataSource);
}

So, that shouldn’t be too hard. After a quick recompile and going back to the site, you’ll find it looks exactly the same as before. Well, poop! Time to break open the debugger and see if it’s actually being set.

If we set a breakpoint in the OnRowCreated method (and go to a row after the pager is created) you’ll see that the TopPagerRow is does in fact have the correct values.

col-span-after

At least at this point we can reasonably assume it’s not necessarily our fault.  Time to put on our detective hats and break out .NET Reflector; first, though, we need to determine where to look.  I like to do that by sticking breakpoints in at key points and seeing where the values change (or don’t change).  A good place to start is with the Render method.  Simply override it, call the base version, and set a breakpoint on that line.  Now, if you were to do that in this situation, you’ll notice that the ColumnSpan is still correct which means that something in the Render function is killing it.

If you navigate to the GridView’s Render method in .NET Reflector you’ll see the following:

reflector-render

The best way to find these things is to look for anything outside the ordinary and as luck would have it, we don’t have to look very far.  What’s this PrepareControlHeirarchy method and what does it do?

reflector-prepare

Notice the rows I’ve underlined.  The above code looks through the cells in any given row that are of the DataControlFieldCell type, which ours is definitely not, and it increments a variable for each one found that is in a data row.  After looking for where the “num” variable is used, you’ll find the following:

reflector-prepare-2

And there we have it!  That’s the cause of our ColumnSpan being off.  Since our cell was not of the “correct” type, it happily ignored it and did what it thought it should do.  Nice.  Well, at least in this case the PrepareControlHeirarchy method is virtual.  If we override that method and adjust it like so, we should be golden.

protected override void PrepareControlHierarchy()
{
    base.PrepareControlHierarchy();

    if (TopPagerRow != null && TopPagerRow.Cells.Count > 0)
        TopPagerRow.Cells[0].ColumnSpan = 4;
}

I’m only “fixing” the TopPagerRow here with regards to space and to clearly illustrate the fix.  A quick rebuild and run and we are greeted with the following, wonderous, sight:

finished

Which is exactly what we were going for.

I mentioned at the start of this post that this situation is not one that is likely encountered, but it’s always good to know how to dig into the framework and see what’s going on under the covers.  .NET Reflector a very valuable tool to keep in your toolkit.

WebForms Knows Better Than You!

Thursday, June 19th, 2008

Sometimes I get annoyed with ASP.NET WebForms … No, make that greatly annoyed and disappointed with web forms.  As I mentioned in my last post, I was recently troubleshooting a 2nd incarnation of the mysterious multiple request syndrome (MMRS for short).  What made this time worse was what ending up being the cause.  In this case the request was being caused by a “url(none)” in a style attribute.  After tracking down the offending component I found something similar to the following example in the code.

Panel inner = new Panel();
inner.ID = "inner";
inner.Style.Add("background-image", "none");

Notice the last line.  Looks correct doesn’t it?  WRONG!  It’s not correct because WebForms Know Better Than You! ™.  When the above is rendered out to the page, it looks like this:

<div id="inner" style="background-image: url(none);"> </div>

Notice anythign wrong?  Yep, right there in the middle … the wonderful “url(none);” and a 2nd request in your browser for the “/none” path.  Well, poo… How did that get in there?  More importantly why did it get in there?  Especially considering the CSS spec specifically allows “none” as a value for the “background-image” property.  In fact the w3 page for the CSS1 (and CSS2) spec says “Value: <url> | none”.

Being the inquisitive sort, there was only one thing that I could do;  Fire up Reflector.NET and go to town.  First order of business was to find the Style property for the Panel control, which ends up being the Style property of the Panel’s base class, WebControl.  It is of type CssStyleCollection … My answer must lie somewhere in there.  The Add method in the CssStyleCollection class isn’t that special and just adds the items to some hash tables.  The Render methods might be interesting however.  After using the Analyzer on those and seeing how they are used we can determine two things.  First, the Render method that takes an HtmlTextWriter merely calls AddStyleAttribute on the writer and adds the CSS properties while the other one takes a CssTextWriter … Wait, what?  That’s a bit different.  It seems to call a WriteAttribute method of the CssTextWriter class.  After making our way to the static WriteAttribute method, we see the following:

if (key != ~HtmlTextWriterStyle.BackgroundColor)
{
    isUrl = attrNameLookupArray[(int) key].isUrl;
}
if (!isUrl)
{
    writer.Write(value);
}
else
{
    WriteUrlAttribute(writer, value);
}

Hmm, what is this “isUrl” and WriteUrlAttribute? Let’s go find out.  A quick Analyze on the “attrNameLookupArray” member shows that it is set in the static constructor so off we go.  In there we see a bunch of RegisterAttribute calls and we see one for the property we set at the beginning of this ordeal, “background-image”.  Digging into the RegisterAttribute method, we see that the last parameter to the function, which in the case of “background-image” is “true”, is the “isUrl” value we are looking for.  So the CssTextWriter considers the “background-image” attribute a URL based attribute … Makes sense, as it is one, so the issue must lie somewhere within WriteUrlAttribute.  Looking at the code for that function:

if (StringUtil.StringStartsWith(url, "url("))
{
    int startIndex = 4;
    int length = url.Length - 4;
    if (StringUtil.StringEndsWith(url, ')'))
    {
        length--;
    }
    str = url.Substring(startIndex, length).Trim();
}
// str is rendered out as "url(" + str + ")" after.

Well, that is unfortunately where are issue lies and there isn’t a damn thing we can do about it.  No special handling of “none”; Just a regurgitation of whatever we set the property to.  That is just too awesome.