ASP.NET WebForms: Taking Back the HTML

There’s a lot of debate these days about the ASP.NET WebForms model vs. the newer ASP.NET MVC model. There are advantages to both. Disadvantages to both. Pick the one that best fits your needs. Nuff said. But sometimes that choice isn’t so obvious.

MVC for example essentially gives you much more control over the generated HTML for the page. Well, complete control. But sometimes you don’t really need complete control, and the lack of an encapsulated control kind of sucks. HTML Helpers help, but they have no design time experience. Hence, there are the MVC Controls in the MVC Futures project on CodePlex and an interesting compromise between the two models, at least when it comes to the rendering part of the application.

But then there’s the other side of the equation – you’ve got a WebForms application, and you need a little more control over the HTML. What’s the compromise there? Well, server controls try to give you control over the markup. Some of them do a pretty good job at it, like the ListView control added in ASP.NET 3.5. Others, not so good. If a control doesn’t let you do what you need, you are pretty much stuck – either abandon the control and all its usefulness, or write your own control.

What’s in a control anyway?

If you think about it, controls at the highest level are really two very different things.

  1. Controls abstract the rendering of HTML.
  2. Controls provide client-server integration by managing data between client and server.

Take the CheckBox control. It renders an input and label, and connects them with the ‘for’ attribute, so clicking the text also checks and unchecks the checkbox. That’s it’s HTML abstraction. It provides client-server integration by allowing you to dynamically read and write to the checked property on a server-side proxy.

A simple example of #2 is the TextBox control. Double meaning, uh, not intended. Type some text into the box and submit the form – magically, the server-side instance knows what its Text property should do. The control can also push data back to the client. Set the Text property, and magically the value is reflected in the rendered page. This can be much more complex though – the control might manage a hidden field, or rely on data stored in that catch-all hidden field we love to hate, ViewState.

The thing is, HTML abstraction is useful, but a lot of the time it doesn’t provide a ton of benefit. Client-server integration, however, is usually much more useful. If you don’t need that there’s a good chance you don’t need a control to begin with.

Html Controls

I would be wrong not to point out that there’s a whole namespace of controls in ASP.NET that are largely underused. HTML controls allow you to add a runat=”server” to an HTML element and get reasonable client-server integration with it. So if you don’t like how the CheckBox control renders, just use the HtmlCheckBox instead. But what if you’re using a more complex control that has no HTML equivalent?

What if you could control the HTML for any server control without losing the client-server interaction?

Wouldn’t that be nice?

Taking Control

Introducing the CustomRender control. This control suppresses the rendering of any control(s) you put in it, while allowing you to define your own, replacement HTML. It gets a little nicer than that, though. One step at a time – first, lets choose the enemy. A Button control that sets the text of a Label control.

<script runat="server">
    private void ButtonClick(object sender, EventArgs args) {
        Label1.Text = "You clicked Button1";
    }
</script>
<asp:Button ID="Button1" runat="server" Text="Go" OnClick="ButtonClick" />
<asp:Label ID="Label1" runat="server" />

You’re going down, Button1. Bring on the CustomRender:

<i88:CustomRender runat="server">
    <ControlTemplate>
        <asp:Button ID="Button1" runat="server" Text="Go" OnClick="ButtonClick" />
        <asp:Label ID="Label1" runat="server" />
    </ControlTemplate>
</i88:CustomRender>

That’s step 1. Now how do we get our own HTML in here? As this stands, the button and label will not render anything. They still participate in the page lifecycle and all that goodness. But nothing renders. So we could just put the HTML next to the CustomRender control. But it would be nice if this control helped me out a little by giving me what these controls would have rendered, wouldn’t it? Lets swap over to Design View.

If you’re one of the types that never uses Design View – it’s that button that says ‘Design’ on the lower left, or Shift-F7.

image

Click on “Update HtmlTemplate”, then switch back on over to Source view. Voila…

<i88:CustomRender runat="server">
    <ControlTemplate>
        <asp:Button ID="Button1" runat="server" Text="Go" OnClick="ButtonClick" />
        <asp:Label ID="Label1" runat="server" />
    </ControlTemplate>
    <HtmlTemplate>
        <input ID="Button1" name="Button1" type="submit" value="Go" /> <span ID="Label1">
        </span>
    </HtmlTemplate>
</i88:CustomRender>

The designer for the control has rendered its ControlTemplate and put the result into the HtmlTemplate. And just to prove it works, modify the HTML a little. Lets change the ‘value’ of the input from ‘Go’ to ‘Gone’.

image

And now lets click the button and see if the server control still works. Remember, it’s supposed to set the text of the label to “You clicked Button1.”. Hmmmm.. nothing. It broke!!! No… remember, when you do this, you take complete control over the HTML. That text doesn’t just come from no where. The code is running, but you haven’t put it into your HTML.

How do you do that? No problem – think of the HTML template like a mini-MVC View.

<HtmlTemplate>
    <input ID="Button1" name="Button1" type="submit" value="Gone" />
    <span ID="Label1"><%= Label1.Text %></span>
</HtmlTemplate>

Now when we click the button, it works:

image

Show Off – The Challenge

“Okay,” you say, “but that’s just a button and a label. What about a real world scenario,” you spout in disbelief? I dunno if you said that, but it’s more interesting to pretend like you are challenging me to put this technique to the test. I accept your pretend challenge.

Way back in ASP.NET 1.x there was the DataGrid control. As great as it was at the time, it’s limitations eventually led to the GridView control. As great as that was, you still didn’t get enough control over the markup and CSS, so the ListView control was born (I often wonder what we’d call the next iteration – running out of two-word data-like combinations aren’t we? No, there already is a DataList control!). My point being that in the current set of databound controls, the DataGrid control is probably the oldest and most set in its ways, and most known for its lack of customizability. It renders as a <Table>, and that’s that. How 1990’s of it.

A perfect target for the challenge – let’s do the unthinkable. Get a DataGrid to render as <UL> and <LI> and be styled with 100% CSS, while maintaining the server-side features like SelectedIndex.

First, lets take a look at a DataGrid utilizing the SelectedIndex feature.

<asp:DataGrid ID="dg1" runat="server" 
    AutoGenerateColumns="false" SelectedIndex="1" SelectedItemStyle-CssClass="selected"
    DataSource='<%# DummyData %>'>
    <Columns>
        <asp:BoundColumn DataField="Name" />
        <asp:ButtonColumn DataTextField="Price" DataTextFormatString="{0:c}" CommandName="Select" />
    </Columns>
</asp:DataGrid>

It shows two columns – a product name, and a price, formatted as currency. You can click on the price to select the item, which then has a different CSS class. The DummyData property is just returning an ArrayList with a few sample items. Let’s not forget how you databind this thing… you may be using a DataSource control. For this purpose we’ll just do it the old fashioned way.

protected override void OnLoad(EventArgs e) {
    if (!Page.IsPostBack) {
        dg1.DataBind();
    }
    base.OnLoad(e);
}

Now lets take a look at the rendering.

<table cellspacing="0" rules="all" border="1" id="dg1" style="border-collapse:collapse;">
    <tr>
        <td>&nbsp;</td><td>&nbsp;</td>
    </tr><tr>
        <td>Widget</td><td><a href="javascript:__doPostBack('dg1$ctl02$ctl00','')">$19.95</a></td>
    </tr><tr class="selected">
        <td>Horn</td><td><a href="javascript:__doPostBack('dg1$ctl03$ctl00','')">$2.99</a></td>
    </tr>
</table>

image

A few things of note going on here. For the SelectedIndex feature to work, the ButtonColumn has rendered an <A> tag which does a postback. The ID it passes to the __doPostBack function is the UniqueID of a link button it has added to the control tree. The LinkButton raises a bubble command named “Select”, which the DataGrid picks up and sets its SelectedIndex property. While rendering, the DataGrid applies a different style to the selected item.

Translating this into your own custom HTML is pretty easy actually. The trick is to do rendering only – you could in theory use a Repeater to bind over the same items. We’re trying to leverage the existing features of the DataGrid, so rather than grab the data directly we’re enumerate the DataGrid’s Items collection and try to reuse as much as we can.

Again, think of this template as a mini-MVC view, and the actual control we are targeting is like our ViewData. That way, changes to the DataGrid don’t require changes to the custom rendering.

<i88:CustomRender runat="server" ID="c1">
    <ControlTemplate>
        <asp:DataGrid ID="dg1" runat="server" SelectedItemStyle-BackColor="Red"
            AutoGenerateColumns="false" SelectedIndex="1"
SelectedItemStyle-CssClass="selected"
            DataSource='<%# DummyData %>'>
            <Columns>
                <asp:BoundColumn DataField="Name" />
                <asp:ButtonColumn DataTextField="Price"
DataTextFormatString="{0:c}" CommandName="Select" />
            </Columns>
        </asp:DataGrid>
    </ControlTemplate>
    <HtmlTemplate>
        <% for (int i = 0; i < dg1.Items.Count; i++) {
               // enumerate the DataGrid's items so we can choose how to render them
               var item = dg1.Items[i];
               // the ButtonColumn is rendering a LinkButton or Button 
               var priceLink = (IButtonControl)item.Cells[1].Controls[0];
               %>
        <ul class="item<%= dg1.SelectedIndex == i ? " selected" : "" %>">
            <li><%= item.Cells[0].Text %></li>
            <li class='price' 
onclick="<%= Page.GetPostBackEventReference((Control)priceLink) %>">
<%= priceLink.Text %>
</li>
        </ul>
        <% } %>
    </HtmlTemplate>
</i88:CustomRender>

A for loop enumerates the DataGrid.Items collection. For the first column we just repeat the cell’s text. The 2nd column is more complex as it contains a LinkButton with a postback reference. Even so, it’s not too complex – get a reference to the LinkButton and use Page.GetPostBackEventReference() to generate the necessary postback script. We are now free to apply CSS in anyway we like. Watching for the the SelectedIndex, we apply a ‘selected’ class, for example.

I’ve just quickly put together these styles to make each ‘item’ float left, so the “grid” isn’t even a grid anymore, just to show how it can be totally different.

<style type="text/css">
    ul.item {
        float: left;
        list-style-type: none;
        padding: 0px 10px 0px 10px;
    }
    ul.selected {
        border: solid 1px red;
    }
    li.price {
        cursor: pointer;
        color: Blue;
        text-decoration: underline;
    }
</style>

And here it is running.

image

Mission Accomplished

Perhaps you are thinking that is pretty ugly and difficult. If you are, remember that is why controls abstract that stuff away for you, so it’s easy and expressive. This is by no means “the end” of control rendering. It’s a tool you should keep in your toolbox. If you have a difficult to fix rendering issue for a specific control, now you don’t have to say goodbye to the whole thing.

How does it work?

It’s actually pretty simple! All controls create their HTML in the Render() method. To control the HTML, you just need a way of injecting your own HTML while suppressing the stuff it’s Render() method produces. Ok – so, don’t call Render(). The Render method unfortunately does not only produce HTML. Sometimes it produces script references or “startup” scripts. Sometimes it registers with the ASP.NET Event Validation feature for security purposes. So this control still calls Render, but it throws away the resulting HTML. It instantiates the ControlTemplate like any other template, adding it to its own control tree so they particulate in the lifecycle. It just chooses to actually render the HTML template instead. That is, unless it is design time. Then it renders the control template – allowing you to use the controls in the designer like you normally could.

Gotchas

Controls will not necessarily take kindly to being rendered “for reals” at Design Time. The page isn’t real, and the other controls on the page don’t necessarily exist or are in their proper hierarchy. So the “Update HtmlTemplate” designer action may not always work. It might even cause an exception to be thrown. It depends on the control.

Also, I find that although the correct HTML is being rendered internally, VS gets it’s hands on it and makes a few modifications to the persistence of it for some reason. It seems mostly benign, like reordering element attributes. But one bad thing it does is capitalize the ‘id’ attribute, which is supposed to be in lower case if you care about XHTML compliance. Easy enough to fix once you get the HTML bootstrapped. I’m trying to find out from someone on the team why this happens and if we can fix it. If all else fails, you can always run the page and use the actual source to get the HTML rather than use the designer trick.

Download & Discuss

It’s up on code gallery, including the sample of the world’s first cannibalized DataGrid.

http://code.msdn.microsoft.com/aspnetcustomrender

13 Comments

  • Any chance of getting this into ASP.NET 4.0? Would be great to see it there, that would guarantee that control gets long *supported* life..

  • Great post. Any chance of a screencast on this?

  • Great work. A really useful approach.

  • As always, great article! Thank you for doing what you do!

  • Thanks for this. Really useful article.

  • Excellent work as always!

    One small suggestion - you should probably render the _controlHolder before the _htmlHolder. Some controls create or modify their control tree in the Render method, so your template might be referencing an invalid version of the control tree if you render it first.

  • Nice experiment! I've also fought with ASP.NET markup... After using controls for a longer period of time, I've found that I end up using them LESS, and rely more on repeaters for most stuff.

    Also, I like to add javascript to a lot of pages for nice effects etc, and getting away from the controls has helped a lot. I would say the biggest advantage is when doing long signup forms, its nice to use the view state when validating etc.

    While I don't think I would use your technique, it is nice to see how other developers are getting webforms to work for them.

    You might want to check out Telligent's work for community server and their CMS. Some of the control stuff they add is pretty interesting, and makes paging data dead simple.

  • @RichardD -- excellent point, I'll refresh it with the change when I can. Another good reason why Render() must still occur on the controls.

    @Jason -- Reducing your dependency on controls when you don't need them is definitely the way to go.

    Found this great article on some of the basic mistakes people make, relying on controls too much:

    http://weblogs.asp.net/craigshoemaker/archive/2008/12/02/controlling-html-in-asp-net-webforms.aspx

  • Fantastic! An interesting follow-up would be how to encapsulate a given overridden rendering (for GridView for example) into a reusable control: once you've decided how you'd like a GridView to render, it would be nice to be able to reuse that work without copy-paste.

  • This is really great. There are many of us who have gravitated to the repeaters so that we can create simpler, standard html that can be manipulated client side with javascript or with the application of CSS. Dave Ward and Rick Strahl have done great work integrating the .Net back end with jQuery via PageMethods.

    This is a nice alternative, as you retain the ability to use postbacks and ViewState while gaining control back.

  • What is the difference between this control and how ControlAdapters work?

  • Herman -- control adapters don't let you do anything declaratively, and IRC they apply to a control type as a whole, you can't apply them to a particular instance of one. Also, this technique can be used across multiple controls at the same time -- whatever you put inside it. Quite different.

  • Technically this is pretty cool. But I can't imagine using it in a real production application though. It creates far too much overhead just so I can have "complete control".

Comments have been disabled for this content.