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 its output. Your page is normally littered with a bunch of excessively long IDs 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) {
StringWriter output = new StringWriter();
HtmlTextWriter html = new HtmlTextWriter(output);
base.Render(html);
string rendered = output.ToString();
html.Dispose();
output.Dispose();
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.
MatchCollection matches = Regex.Matches(
rendered,
"</?form[^>]*>",
RegexOptions.IgnoreCase
);
Regex expectedId = new Regex(
string.Format("id=\"?({0}|aspnetForm)\"?", this.Form.ID),
RegexOptions.IgnoreCase
);
Regex closeForm = new Regex(
@"</\s?form\s?>",
RegexOptions.IgnoreCase
);
Match open = null;
Match close = null;
int depth = 0;
for (int i = 0; i < matches.Count; i++) {
Match match = matches[i];
if (expectedId.IsMatch(match.Value) && !(open is Match)) {
open = match;
}
if (!(open is Match)) { continue; }
depth += closeForm.IsMatch(match.Value) ? -1 : 1;
if (depth == 0) {
close = match;
break;
}
}
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 IDs 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.
MatchCollection extraIds = Regex.Matches(
rendered,
@"<[^>]*actualId=""(?<id>[^""]*)""[^>]*>",
RegexOptions.IgnoreCase
);
for (int i = extraIds.Count; i-- > 0; ) {
Match extra = extraIds[i];
string newElement = extra.Value;
string newID = extra.Groups["id"].Value;
newElement = Regex.Replace(
newElement,
@"actualId=""[^""]*"" ?",
string.Empty,
RegexOptions.IgnoreCase
);
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,
"\" "
);
});
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.
Absolute Power Corrupts
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!
Note: As the commenter below noted, 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!!).