Complete Control Over Your Webforms Output

I like Regular Expressions... a lot. I know I'm not an expert with them, but even an amateur can do some pretty amazing things with just a couple Regular Expressions.

WebForms has always been a little... messy with it's output. Your page is normally littered with a bunch of excessively long ID's and name on content that doesn't really need it (for example, I saw a vendors program with this little gem in it accounts_overview_body_quickAction_quickActionTitleLabel all for a span element).

You're also stuck with the ViewState, which can get out of control in a hurry. You can disable it, but it still renders some content no matter what. It might not be that much, but when you don't need it you may prefer it to be gone.

One of the last annoying things about WebForms is that you can't put additional forms within your Page level form. Some browsers don't mind, but others won't work at all. If you didn't need the WebControls to do anything (like persist their state) it would be nice to be able to drop them completely.

Get Control Over Your Page

Like I mentioned at the start, by overriding the Render event on a page you can have full access to your output before it gets sent down to the user. I posted some code about this before when I talked about minifying your content. If you read that post then you'll have a general idea where this is heading.

The first step is to actually render our content so we can manipulate it. Without getting into too much detail we're going to write our content to our own local HtmlTextWriter so we can manipulate it before we send it to the actual HtmlTextWriter. It's not hard to do...

protected override void Render(HtmlTextWriter writer) {

    //render our output
    StringWriter output = new StringWriter();
    HtmlTextWriter html = new HtmlTextWriter(output);
    base.Render(html);

    //get the rendered string
    string rendered = output.ToString();

    //Make our changes
    //... discussed shortly

    //cleanup our writers
    html.Dispose();
    output.Dispose();

    //then write it
    writer.Write(rendered);
}

Pretty cool, right? We've got our string, now we can get started making our changes.

Drop The ViewState

Some pages you have really just don't need the ViewState at all. Maybe it's some product information or maybe a few photos, but there aren't any controls on the page that the user is going to post back. It doesn't take much to write a regular expression to remove it.

rendered = Regex.Replace(
    rendered, 
    "<input.*name=\"__VIEWSTATE\"[^>]*>", 
    string.Empty, 
    RegexOptions.IgnoreCase
    );

That little snippet of code will drop the ViewState completely from the page. You could modify it a little further to drop other things (like the __EVENTSTATE) by changing the expression to "<input.*name=\"__\\w+\"[^>]*>".

Knock Out The Page Form

As I said earlier in the post, if you want to have other forms on your page then they have to be outside of the Page Form. Not all browsers work with nested forms.

With WebForms, you can have a Page Form, use all your controls inside and then drop the markup before it is sent to the client. That way you can use WebControls and nested forms.

This process is naturally a little more complicated.

//find all the forms and closed elements
MatchCollection matches = Regex.Matches(
    rendered, 
    "</?form[^>]*>", 
    RegexOptions.IgnoreCase
    );

//create the expression to match for the ID
//master pages rename the form to 'aspnetForm' -- weird
Regex expectedId = new Regex(
    string.Format("id=\"?({0}|aspnetForm)\"?", this.Form.ID), 
    RegexOptions.IgnoreCase
    );

//expression to check for a close of a form
Regex closeForm = new Regex(
    @"</\s?form\s?>",
    RegexOptions.IgnoreCase
    );

//loop through and remove the page form
Match open = null;
Match close = null;
int depth = 0;

//we're checking for the open form THEN checking for the 
//matching close tag (checking for nesting)
for (int i = 0; i < matches.Count; i++) {
    Match match = matches[i];

    //check if this is the page form
    if (expectedId.IsMatch(match.Value) && !(open is Match)) {
        open = match;
    }

    //if we aren't tracking yet, just continue
    if (!(open is Match)) { continue; }

    //change the direction of the nesting
    depth += closeForm.IsMatch(match.Value) ? -1 : 1;

    //if the nesting is back to zero we can assume this 
    //is the correct tag
    if (depth == 0) {
        close = match;
        break;
    }

}

//remove the tags - make sure to start with the close tag
//since this will affect the index of the Matches
if (open is Match && close is Match) {
    rendered = rendered.Remove(close.Index, close.Length);
    rendered = rendered.Remove(open.Index, open.Length);
}

With this code we can remove the Page Form from the page, but preserve all of the other forms so they can handle their postbacks however the see fit.

It's worth noting though, like removing the ViewState, postbacks and WebControls that rely on these features will most likely no longer work (or at least as expected).

Shorten Your IDs

ASP.NET 4.0 is going to offer some nice features to help developers push meaningful ID's down to the client. Right now IDs are generated by using the parents, parents, parents, etc, ID. It's great to avoid naming collisions - but it's horrible if you're trying to do anything client side. Not only that, but sometimes you end up with IDs on elements that you have no intention of working with so it's just wasted space.

Again, with a little Regular Expression magic, we can make changes to the IDs on our page.

//lastly, drop any unwanted IDs
MatchCollection extraIds = Regex.Matches(
    rendered, 
    @"<[^>]*actualId=""(?<id>[^""]*)""[^>]*>", 
    RegexOptions.IgnoreCase
    );

//loop backwards to avoid affecting indexes on the matches
for (int i = extraIds.Count; i-- > 0; ) {
    Match extra = extraIds[i];

    //drop the unwanted parts
    string newElement = extra.Value;
    string newID = extra.Groups["id"].Value;

    //lastly, remove the actual ID field
    newElement = Regex.Replace(
        newElement, 
        @"actualId=""[^""]*"" ?", 
        string.Empty, 
        RegexOptions.IgnoreCase
        );

    //if the ID is blank, just remove it                
    newElement = Regex.Replace(newElement, @"(id|name)=""[^""]*"" ?", (str) => {
        if (string.IsNullOrEmpty(newID)) { return string.Empty; }
        return string.Concat(
            str.Value.StartsWith("id", StringComparison.OrdinalIgnoreCase) ? "id" : "name",
            "=\"",
            newID,
            "\" "
            );
    });

    //finally, replace the new string
    rendered = rendered.Remove(extra.Index, extra.Length);
    rendered = rendered.Insert(extra.Index, newElement);

}

Clearly, this is a custom solution to say the least. The idea here is that we can now add the property "actualId" onto our controls and the id and name attributes will be updated to reflect that value. Additionally, our actualId attribute will be removed completely from the element.

You could adjust the code a little more to grab up all IDs that aren't flagged in some way to retain their ID.

It is fun to get in to your rendered output and make changes to what is sent to the client. You have a lot of power to completely change what your content looks like. But be careful - the more you change, the more likely you are to break the way ASP.NET works.

If you come up with some cool ways to use this, let me know!

This doesn't work with UpdatePanels -- at least not without more work. The content from UpdatePanels is delimited with pipes and has character counts for the content with all of the IDs it is returning (I haven't used them in a while so this is from memory). It would be pretty tricky to make changes without breaking anything (but not impossible!!).

June 4, 2009

Complete Control Over Your Webforms Output

Post titled "Complete Control Over Your Webforms Output"