Rendering viewstate to the bottom of the page

Posted at Fri, 31 Aug 2007 17:45:47 GMT by Wilco Bauwer

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:

C#:
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.

C#:
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.

C#:
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.

You should be able to do it with a custom persister as well. Long time ago I created one that puts ViewState into a not-so-hidden field. In fact using it you could put the viewstate anywhere you wanted to by just placing a textbox where you want it.

http://infinitiesloop.blogspot.com/2006/07/bringing-viewstate-into-light.html#comments

Not exactly what you were going for but its interesting :) I hate to link to my old blog since I'm trying to let it DIE but I never ported that article to my new one. oh well.

Ah, very clever. Hadn't thought about that one. Nice.
Wilco, wouldn't your solution fail in medium trust? Private/internal Reflection isn't allowed in that scenario...

Thanks to Dave too for his hint. I didn't know that this facility was built into ASP.NET either...
See the end of the post. :) Basically I also implemented the HTML rewriting hack which will be used when the code doesn't have the ReflectionPermission (which it doesn't by default in medium trust). So it should always work. It's just when it does have the ReflectionPermission, you should get a small perf. boost.
Hi Wilco, I read your solution and came up with a slightly different approach to re-ordering the rendering that doesn't require the use of Reflection. I'm basically swapping out the HtmlTextWriter.InnerWriter for a surrogate in the Form.RenderChildren method (so the ASP.NET stuff is all written to a StringWriter) before calling base.RenderChildren. I then have a placeholder control that is inserted first in the forms child controls to swap the HttpWriter back in before the real children get rendered. Finally, I write out the contents of my surrogate writer to the HtmlTextWriter, so that anything ASP.NET writes out before the first child control of the form is moved. You can find the full details at my blog entry

http://www.dotnetdiary.com/labels/Moving%20ViewState%20Field.html
Hello David,

I have a question about your article "Moving the ViewState hidden field in ASP.NET 2.0"

First of all i have to say that it's a great article and works fine for normal pages.

However, when i use AJAX in combination with your script I get the following javascript error:
'Sys' is undefined

This is because the resource files also move to the bottom of the page below the call:
Sys.Application.initialize();

I tried several things but can't get it to work.

Hope you have a solution in mind.

Thanks in advance.

Regards,

Corné
corne@netbiz.nl
Corné.

Just wanted to let you know that I resolved the problem with the ScriptManager, here is my solution.
In the RenderChildren method of HtmlFormEx, instead of calling "this.container.RenderControl",
I am doing this:

foreach (Control c in Controls)
{
if (c is ScriptManager)
{
HtmlTextWriter w = new HtmlTextWriter(new System.IO.StringWriter());
c.RenderControl(w);
string scriptManHtml = w.InnerWriter.ToString();
if (!string.IsNullOrEmpty(scriptManHtml))
{
Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "scriptmaninit", scriptManHtml, false);
}
}
else
{
c.RenderControl(writer);
}
}

The ScriptManager nevers anything else than the script, as far as I can tell from the source.
It does register a startupscript, but this will still work since we are calling the render method before the startupscripts are rendered.

Hope this helps.

Einar
Btw, "nevers" is short for "never renders"...

Guess I have to start reading my posts before submitting them.

Einar
Corne, I believe I have fixed the problem with the Javascript getting out of sequence causing errors. I encountered the same problem using ICallbackEventHandlers and have no ensured that the HTML I capture to move is always written out before the HtmlForm calls Page.EndFormRender(). This preserves the order whilst moving the Viewstate et al down the page. It's a very small change to make it work, details can be found here:

http://www.dotnetdiary.com/2008_03_01_archive.html
I was chatting to a designer today on this very subject, he has rduced the string to 240 charachters and moved it to the bottom of the page,will be interesting to see if SERPS improve
Your message will be encoded/formatted when it is displayed. If you want to post code, please put the code inside [code=X][/code] tags, where X is the language of your code (C#, ASPX, SQL, etc).
Name:
Email:
(will be encoded using JavaScript to keep it functional and prevent it from being picked up by spammers)
Url:
 
Message:
3 + 3 =