Friday, 28 March 2008

Fix for my Moving ViewState solution

An embarrassingly long time ago a nice reader called Corné commented that my solution did not work with AJAX. I believe this was due to the AJAX Javascript getting getting out of sequence on the page. Anyway, today I started building a page for one of our sites that uses an ICallbackEventHandler. This page is "Search Engine Facing" and therefore uses the Form control I developed to move the ViewState to the bottom of the form. Unfortunately, when I came to try it out the page encountered a javascript error when it loaded and it stopped any callbacks working. So I finally decided to rollup my sleeves and look into it...

After a little investigation I discovered the source of the problem. The WebForm_InitCallback(); function used for Callbacks appears to rely on the "theForm" variable created in the __doPostBack function script. Worse still I discovered that my previous solution accidentally reverses (oops) the order of these two scripts, so that when WebForm_InitCallBack() is called "theForm" variable has not yet been defined!

I needed to find a way to move the viewstate to the bottom of the form without affecting the order of scripts etc. To do this I needed to understand what was writing out the two scripts in question and then figure out a way to make sure I didn't mess the ordering up.

Firstly, if you want to figure out how .NET actually works an invaluable tool (before they finally get round to making the source code available) is Lutz Roeders .NET Reflector. If you've never heard of it, then let me explain. Basically it lets you peek under the covers of .NET and see exactly whats going on by letting you read the disassembled code. This is invaluable when you want to root cause issues like this or if you just want to understand it a bit better.

Using Reflector and a couple of simple tests I figured out how the re-ordering was happening. I must confess I actually realised the re-ordering was occuring when I developed the initial solution, but I didn't understand the extent of the problem. I also had deadlines to meet and it worked for the job I needed it for, so I ignored it (shame on me). My sincerest apologies if you used it and encountered this issue. Anyway, for those of you who didn't read my last post, here's a quick recap of how .NET writes out the ViewState field...

1. ViewState is written (along with lots of other bits and pieces) by a method called Page.BeginFormRender().

2. Page.BeginFormRender() is called by the HtmlForm's RenderChildren() method before it calls base.RenderChildren()

3. Control.RenderChildren() causes the rendering of the Form controls children

4. Ideally we want to switch the order of the calls to Page.BeginFormRender() and base.RenderChildren() so that BeginFormRender() happens afterwards.

5. Unfortunately Page.BeginFormRender() is defined as "internal" so we can't call it directly (boo).

There are other solutions to the problem of moving ViewState, which are covered in my earlier post, as are my reason for not choosing them. In case you've forgotten, here is a quick recap of my solution...

My idea was to try to separate "our" HTML from the bits of HTML that ASP.NET adds to the Form to make it all work. I did this by introducing a surrogate HtmlTextWriter.InnerWriter at the start of the HtmlForm.RenderChildren method that actually wrote to a local StringWriter instance. Then when my first child's Render method was called I raised an event to my Form to tell it to swap the surrogate back out for the original InnerWriter. Then after all the children had been written out (i.e. base.RenderChildren() had completed) I could write out the contents of my surrogate StringWriter. This moved the ViewState et all to after Form children's HTML on the page and everything seemed rosey.

Unfortunatelty, ASP.NET does not write all of it's HTML before the form's child controls are rendered. It also writes bits of HTML afterwards when HtmlForm.RenderChildren() calls Page.EndFormRender(). Page.EndFormRender() writes out things like the "__PREVIOUSPAGE" and "__EVENTVALIDATION" hidden fields for example. Unfortunately, I had inadvertently moved the HTML written in Page.BeginFormRender() to after the HTML written out by Page.EndFormRender(). Page.BeginFormRender() declares "theForm" in the "__doPostBack" script and Page.EndFormRender() writes out the call to "WebForm_InitCallBack()", which is why the order of the two scripts got fatally reversed.

The solution was simple...

I needed to make sure that I wrote out the buffered pre-children HTML captured in Form.RenderChildren() before the call to Page.EndFormRender(), thus preserving the order of things. Since I can't hook into the ASP.NET code, the only solution was to insert another dummy control as the last child in the Form (after the "real" children). This control would raise an event back to the Form when it's Render() method was called. On notification of this event the Form would write out the buffered pre-children HTML to the page. Since my dummy control's Render() method is called before the call to Page.EndFormRender() in HtmlForm.RenderChildren() the ViewState is moved but the order is preserved!

The changes are actually quite small:

1. Buffer the HTML captured in Form.RenderChildren() that we want to move in a StringBuilder.

2. Remove the line of code in MyForm.RenderChildren() that used to write out the buffered HTML after the call to base.RenderChildren() had completed.

3.In the BeginRender event handler for our lastChild control, write out the contents of the StringBuilder to the HtmlTextWriter.

That's it. I've tested it with ICallbackEventHandlers and everything seems OK now. All the code is included below. Please let me know if you have a different experience....

MyRenderingPlaceHolder class




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);
}
}
}


MyForm class




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

public MyForm() : base()
{
}

protected override void RenderChildren(HtmlTextWriter writer)
{
_viewStateBuilder = new StringBuilder();

using (System.IO.StringWriter viewStateWriter = new System.IO.StringWriter(_viewStateBuilder))
{
_htmlWriter = writer;
_savedInnerWriter = writer.InnerWriter;

writer.InnerWriter = viewStateWriter;

base.RenderChildren(writer);

viewStateWriter.Close();
}
}

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);
}

protected override void OnPreRender(EventArgs e)
{
// Register a rendering placeholder as the last child so we can switch the writer back before
// any scripts / html written by page.EndFormRender(writer, this.UniqueID) and page.OnFormPostRender();
// occurs. This ensures we preserve the order of javascript which otherwise fails for Callbacks due to
// undeclared variables...

base.OnPreRender(e);

MyRenderingPlaceHolder renderingPlaceHolder = new MyRenderingPlaceHolder();

renderingPlaceHolder.ID = "LastChild";
renderingPlaceHolder.BeginRender += new EventHandler(LastChild_BeginRender);
renderingPlaceHolder.EnableViewState = false;

Controls.Add(renderingPlaceHolder);
}

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

private void LastChild_BeginRender(object sender, EventArgs e)
{
// Write the viewstate before the Page.EndFormRender and Page.OnFormPostRender code executes so that
// we maintain javascript order etc..
_htmlWriter.Write(_viewStateBuilder.ToString());
}
}


AJAX ScriptManager Problem



Well thats not quite it. My solution encounters an additional problem when using AJAX. The AJAX ScriptManager control renders it's own Javascript (Sys.WebForms.PageRequestManager._initialize...) that relies on code defined in a script included through ScriptResource.axd. Unfortunately, the reference to ScriptResource.axd gets moved with the ViewState and ends up after the ScriptManager controls' javascript in the document order. This results in a "Sys is undefined" error on the page and AJAX is broken.

Unfortunately, there is the potential for this to occur with any control that renders its own Javascript (in-place) which a) Executes when the page is loaded and b) Depends on an included script.

My current workaround is to modify the code so that you can specify exactly where you want the ViewState moved to in the rendering sequence of the Form's children. You do this by inserting a MyRenderingPlaceHolder on the page with a specific ID of "ViewStatePlaceHolder". When the form is rendered and this controls Render() method is called it raises an event that causes the buffered ViewState (and script includes) to be written out. Therefore, by positioning the ViewStatePlaceHolder control before the ScriptManager control in the forms children, you can ensure that the script included by ScriptResource.axd occurs before the inline script that depends on it. If you do not add your own "ViewStatePlaceHolder" control to the Form one will be automatically added for you after the last child. This means that you only have to use this technique when you are using the ScriptManager control (or another control that behaves in the same way).

The code changes to support this workaround are included below:

1. Add this function to the MyForm control.




private MyRenderingPlaceHolder GetViewStateRenderingPlaceholder()
{
foreach (Control candidate in Controls)
{
if (candidate.ID != null && candidate.ID.Equals("ViewStatePlaceHolder") && candidate is MyRenderingPlaceHolder)
{
return (MyRenderingPlaceHolder)candidate;
}
}

return null;
}


2. Replace MyForm.OnPreRender with the following code:




protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

MyRenderingPlaceHolder renderingPlaceHolder = GetViewStateRenderingPlaceholder();

if (renderingPlaceHolder == null)
{
renderingPlaceHolder = new MyRenderingPlaceHolder();

renderingPlaceHolder.ID = "ViewStatePlaceHolder";
renderingPlaceHolder.BeginRender += new EventHandler(ViewStatePlaceHolder_BeginRender);
renderingPlaceHolder.EnableViewState = false;

Controls.Add(renderingPlaceHolder);
}
else
{
renderingPlaceHolder.BeginRender += new EventHandler(ViewStatePlaceHolder_BeginRender);
}
}


3. Rename LastChild_BeginRender event to ViewStatePlaceHolder_BeginRender



This name makes more sense now since it is not necessarily the last child. The code has not changed at all.


private void ViewStatePlaceHolder_BeginRender(object sender, EventArgs e)
{
// Write the viewstate before the Page.EndFormRender and Page.OnFormPostRender code executes so that
// we maintain javascript order etc..
_htmlWriter.Write(_viewStateBuilder.ToString());
}


Include the rendering placeholder on your Page



If you want (or need) to explicity control where the ViewState and script includes are rendered then include the MyRenderingPlaceHolder as a child of your MyForm control as follows...


<sug:MyForm ID="form1" runat="server">
<div>
<!-- Put your non script-include sensitive controls here -->

<sug:MyRenderingPlaceHolder ID="ViewStatePlaceHolder" runat="server" />

<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
</div>
</sug:MyForm>


Unfortunately, time constraints have limited my AJAX testing to a simple "hello world" application, so please let me know if any further problems arise. And finally, a big thank you to Corne for all his AJAX testing.

Labels: ,


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

Subscribe to Posts [Atom]