Friday, 5 October 2007

Moving the ViewState hidden field in ASP.NET 2.0

Breaking news...Fix for ICallbackEventHandler available!


This solution has been updated to fix a javascript sequencing problem encountered with pages/controls that use ICallbackEventHandler. Click here for details and a full code listing.

The Problem:

I wanted to produce XHTML Strict compliant ASP.NET output that was "search engine friendly". My goal was to produce ASP.NET output that was as close to our legacy XHTML as possible. We are moving lots of static (aside from the odd Server Side Include) pages to ASP.NET so that we can inject dynamic content. Getting ASP.NET to produce XHTML Strict output was pretty easy, although there are a couple of "gotchas". For example, browser capabilities mean that what you see in IE / Firefox isn't necessarily what every user-agent will receive. Obviously, you want to make sure that search engines aren't getting "dirty" HTML that is claiming to be XHTML Strict. Anyway, I'm going to write a separate posting about those issues later. I should mention that although moving the ViewState field was my main target, I was really interested in relocating all the "control" fields and code further down the page. Now I'm not going to get into debates about whether this is necessary or not, thats a separate discussion, but my approach has always been "better safe than sorry" when it comes to not falling out with the search engines.


Scott Hanselman's Solution:

So naturally I googled this problem first and checked out quite a few of the solutions that came up. The most referenced appears to be Scott Hanselman's solution of intercepting the HTML string produced by the HTMLTextWriter, finding and moving the ViewState field within it and passing it on. Initially, I tried this and it worked fine. However, once I turned on XHTML Strict (which wraps the ViewState field in a DIV for compliance) I was left with an empty DIV. Furthermore, once I added cross page posting (needed for our search script) I saw additional fields added to the top of the form which I really wanted to move too. I didn't really want to create a lot of string manipulation code that was totally dependent on how ASP.NET decided to write out its control fields and scripts, so I started digging some more...


Wilco Bauwer's Solution

I was trying to understand how and when ASP.NET was rendering the ViewState and other control fields, when I came across a blog Wilco Bauwer that discusses the problem and shows the rendering process. Personally, I was not happy with the full-trust security requirement of their solution and I'm uneasy about trying to replicate chunks of ASP.NET code (Form.RenderChildren) - what happens if it changes? However, reading this really helped me to come up with my own approach, so I'm definitely "standing on the shoulders of giants" here. This is a simplification of the ViewState rendering process:
  1. ASP.NET Renders the form contents by calling the Form.RenderChildren method
  2. The Form control calls Page.BeginFormRender
  3. The Page BeginFormRender() method writes out the ViewState and other control fields
  4. The Form control renders its children

The obvious solution was to move the call to Page.BeginFormRender to after the forms child controls had been rendered (i.e. switch steps 2 & 4). Wilco points to this solution too. Unfortunately, we cannot easily do that because Page.BeginFormRender is declared as internal so we can't call it directly. Also we would have to duplicate the original Form.RenderChildren code to allow us to tweak the ordering and as stated I don't like doing this as it creates a big code dependency. What happens when someone at Microsoft changes that method, suddenly my code breaks or something stops working! What I needed to do was to allow ASP.NET to do it's stuff as normal, but to somehow intercept its output so I could move it in it's entirety later. This is exactly what I ended up doing...

My Solution: Using A Custom Form and Placeholder Control

OK so this is my solution, let me know what you think. If you've posted this somewhere already and I've missed it I apologise.

If you look back at the problem what we really want to do is to is to split the Form.RenderChildren method into the piece that happens before the forms "real" children start getting rendered (i.e. when ViewState et al are written) and the piece where the "real" children are being rendered. We then want to swap the order of the two pieces. I acheived this by swapping in a surrogate StringWriter as the HtmlTextWriter.InnerWriter for ASP.NET to write to, which I then swap back to the original HttpWriter before the children are rendered. Then after the children are rendered I write the content of the surrogate StringWriter to the HtmlTextWriter before the form tag is closed. The key is having a control that notifies us when the rendering of the forms "real" children has started so we can do the switch back. I do this by using a placeholder control inserted at position 0 in the control collection. This placeholder raises an event in it's render method to signal that the switch back needs to occur. The code is included below...

1. Create a new WebControl called MyRenderingPlaceHolder. Override its Render method and raise the BeginRender event when it is called. Do not call base.Render() as we don't want any HTML to be written out for this control.



public class MyRenderingPlaceHolder : WebControl
{
public event EventHandler BeginRender;
public MyRenderingPlaceHolder() : base()
{
}
protected override void Render(HtmlTextWriter writer)
{
OnBeginRender(null);
}
protected virtual void OnBeginRender(EventArgs e)
{
if (BeginRender != null)
{
BeginRender(this, e);
}
}
}


2. Create your own Form control that inherits from HTMLForm called MyForm



public class MyForm : HtmlForm
{
private HtmlTextWriter _htmlWriter;
private System.IO.TextWriter _savedInnerWriter;


3. In your MyForm control override the OnInit method. Create a MyRenderingPlaceHolder control and add it as the first control in the collection. Register for the MyRenderingPlaceHolder.BeginRender event. Don't forget to call base.OnInit.



protected override void OnInit(EventArgs e)
{
base.OnInit(e);
MyRenderingPlaceHolder renderingPlaceHolder = new MyRenderingPlaceHolder();
renderingPlaceHolder.ID = "FirstChild";
renderingPlaceHolder.BeginRender += new EventHandler(FirstChild_BeginRender);
renderingPlaceHolder.EnableViewState = false;
Controls.AddAt(0, renderingPlaceHolder);
}


4. In MyForm override the RenderChildren method and subsitute the writer.InnerWriter for your own StringWriter instance (viewStateWriter). Keep the original HttpWriter to switch back to later, then call base.RenderChildren. After the call to base.RenderChildren write the contents of viewStateWriter to the HtmlTextWriter. By the time we reach this point the switch back will have occurred and we are now effectively writing the ViewState et al to the HttpWriter.



protected override void RenderChildren(HtmlTextWriter writer)
{
using (System.IO.StringWriter viewStateWriter = new System.IO.StringWriter())
{
_htmlWriter = writer;
_savedInnerWriter = writer.InnerWriter;
writer.InnerWriter = viewStateWriter;
base.RenderChildren(writer);
writer.Write(viewStateWriter.ToString());
viewStateWriter.Close();
}
}


5. In the MyForm handler for MyRenderingPlaceHolder.BeginRender event swap the writer.InnerWriter back to the original one we swapped out earlier. The subsequent child controls will now be rendered to the HttpWriter and underlying response stream.



private void FirstChild_BeginRender(object sender, EventArgs e)
{
_htmlWriter.InnerWriter = _savedInnerWriter;
}

To get viewstate moved on a page all you need to do is subsitute the MyForm control for the standard HTMLForm control on your aspx pages.



<mycontrols:myform id="form1" runat="server">


The main benefit of this solution is that it doesn't make any assumption about what "control" fields ASP.NET is going to write out or how the HTML for them is going to look. It also moves the whole lot to the end of the form and will automatically cope with any changes to that part of ASP.NET. Finally, it also does not rely on duplicating critical chunks of the existing HTML forms behavour and therefore has very few dependencies that might cause it to stop working in the future.

So that's it. I don't think my solution breaks anything, but if you find otherwise then let me know. Don't forget that when you move the ViewState you introduce the issue that you could get a viewstate exception if a user causes a form post before the ViewState field has rendered in the browser. However, as long as you keep your pages "light" (e.g. avoid using ViewState unnecessarily) and server response times low then this should not be a problem.

Anyway, hopefully I've managed to explain it well enough. Let me know what you think or if you have a better way.

Labels: ,


This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]