A few days ago someone shared his concern about how ASP.NET renders the view-state field before most of the content. Specifically his concern was how some search engines only parse the first 'n' bytes of a page, which means that in case of ASP.NET there's a good chance that by default a chunk of those bytes will be the view-state. That view-state is going to be useless for a search engine.
Somehow this was the first time I had heard about this problem. A quick search showed up a bunch of posts confirming the problem, as well as suggesting a few ways to render the view-state to the bottom of the page. It seems all of them involve modifying the HTML string to move the view-state field.
I've taken a slightly different approach with the form in the Wilco.Web library. By default, the ASP.NET form does the following:
1
2
3
4
5
6
7
8
9
10
11
12
|
protected override void RenderChildren(HtmlTextWriter writer) {
Page page = this.Page;
if (page != null) {
page.OnFormRender();
page.BeginFormRender(writer, this.UniqueID); // In this call the page will render the view-state.
}
base.RenderControl(writer); // Renders the content.
if (page != null) {
page.EndFormRender(writer, this.UniqueID);
page.OnFormPostRender();
}
}
|
Basically what we want to do is move the call to 'BeginFormRender'. Unfortunately these methods are internal, so we have to resort to some reflection. Directly invoking those methods via reflection is relatively expensive, so instead we will only do the reflection once and hookup some delegates to the methods we reflected on.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
private delegate void OnRenderMethod(Page self);
private delegate void RenderMethod(Page self, HtmlTextWriter writer, string clientID);
static HtmlFormEx() {
[...]
// Cache the methods to reduce the overhead per request.
Type pageType = typeof(Page);
BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
MethodInfo onFormRender = pageType.GetMethod("OnFormRender", flags);
MethodInfo beginFormRender = pageType.GetMethod("BeginFormRender", flags);
MethodInfo endFormRender = pageType.GetMethod("EndFormRender", flags);
MethodInfo onFormPostRender = pageType.GetMethod("OnFormPostRender", flags);
if (onFormRender != null && beginFormRender != null && endFormRender != null && onFormPostRender != null) {
HtmlFormEx.onFormRenderMethod = (OnRenderMethod)Delegate.CreateDelegate(typeof(OnRenderMethod), onFormRender);
HtmlFormEx.beginFormRenderMethod = (RenderMethod)Delegate.CreateDelegate(typeof(RenderMethod), beginFormRender);
HtmlFormEx.endFormRenderMethod = (RenderMethod)Delegate.CreateDelegate(typeof(RenderMethod), endFormRender);
HtmlFormEx.onFormPostRenderMethod = (OnRenderMethod)Delegate.CreateDelegate(typeof(OnRenderMethod), onFormPostRender);
HtmlFormEx.supportsRenderingToBottom = true;
}
}
|
This code will only run once per application start, so the overhead we introduce here is negligible. The only thing we use per page request is the actual delegate.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
[...]
Page page = this.Page;
if (page != null) {
// We should still call this before rendering the children, because otherwise when we use controls
// such as TextBox, we will get an error because it thinks it's not placed within a form.
HtmlFormEx.onFormRenderMethod.Invoke(page);
}
// Since we can't call base.RenderChildren (that would execute the default code), we introduce a container control to which all
// controls get added (see Controls/CreateChildControls). This way we can still render all the children, but by-pass the
// code in base.RenderChildren.
EnsureChildControls();
this.container.RenderControl(writer);
if (page != null) {
HtmlFormEx.beginFormRenderMethod.Invoke(page, writer, this.UniqueID); // We now render the view-state field here...
HtmlFormEx.endFormRenderMethod.Invoke(page, writer, this.UniqueID);
HtmlFormEx.onFormPostRenderMethod.Invoke(page);
}
[...]
|
This reflects the code we wish we could write well, and the overall performance overhead is negligible, and better than that of modifying the HTML directly. There's one caveat though. This only works in full-trust, or when you explicitly granted the ReflectionPermission to this library. To deal with this, the form will automatically fall back on rewriting the HTML directly, so it should at least always work.
Update:
Please check the comments of this post as well for an even better suggestion from Dave Reed.