Wednesday, January 14, 2009

Rendering an ASP.Net Page twice???

Not to long ago, I found myself in a peculiar situation which I will explain shortly. Essentially I found myself needing to render an ASP.Net Page, modify an attribute or two and then re-render. I know you must be asking yourself why in the world would you want to do this? Let me explain first why I needed to do this, then we can look at why this doesn't work and how you can hack around the Page model to get it to work.

The web application I've been working is a LOB application, and printing forms is very important. Furthermore these forms will be provided to our customers customers so we needed to have the utmost control over the form. IE doesn't give you much options when controlling the output. For example using standard methods there is no way to tell IE not to print out the url on the page. The user can modify this setting but relying upon the user to modify their page settings on every print reduces the usability of the application.

What's a poor web developer to do? PDF's are a portable format and we can have a lot of control over it. Now the challenge became do we learn a whole new document model or can we find a tool to convert Html to PDF's. We opted for converting HTML to PDF for a couple of reasons including:
  1. We is actually I, and I didn't want to waste any time learning a new document model. My current employer is a small start up and we just don't have the time to waste.
  2. Many of our forms are displayed to the user in a preview mode. So had we gone with not converting the Html we'd have to synchronize our changes, again the "we" is actually "I"...

We create the PDF's by overriding the page's Render method. We call a method RenderHtml() which looks like:


protected virtual string RenderHtml(string baseUrl)

{

RemapImageUrl(baseUrl);

StreamWriter sw = new StreamWriter(new MemoryStream());

HtmlTextWriter writer = new HtmlTextWriter(sw);

base.Render(writer);

writer.Flush();

StreamReader sr = new StreamReader(sw.BaseStream);

sr.BaseStream.Position = 0;

return sr.ReadToEnd();

}


The RemapImageUrl() provides absolute paths to the images in the document. This could also be done by setting a base; however, in my exact example I had to change the actual URL to get some dynamic images. After we call this method we take the Html and send it to a Html to Pdf converter which then in turn send to the client.

This was working without any issues until like all solutions, a new business requirement came in. We had a form that we sometimes wanted to print a single copy, but in other cases we wanted to print two copies. The second copy would be identical to the first save for a watermark.

I had done something simillar by rendering smaller portions of page indepedantly but in this case, I had a Gridview which requires a form, and needless to say I was trying to jump through so many hurdles guessing at a magic combination of controls and method calls to make this happen

The solution I decided upon was to render my page twice, create two PDF documents then combine them. I know I could have probally done some string parsing, but the documents and format was very fluid at the time, and I simply needed to find a solution and quick.

I figured I could use roughly this code:


string firstDoc=RenderHtml();

EnableWatermark(); //This method turned the water

string secondDoc=RenderHtml();

PdfConvert.ConvertFromHtml(firstDoc)

.AppendPdf(PdfConverter.ConvertFromHtml(secondDoc));


After firing this up, I got an error indicating that a form was already on the page, and there could only be one form. Opening reflector to see what was going on made me cry as I realized what ASP.Net was doing. Now I realize at this point I am far beyond what anyone would consider acceptable use...but lets look real quick why it doesn't work.

Open up reflector and look at the HtmlForm.RenderChildren method this basically calls the Page.OnFormRender then Page.BeginFormRender followed by rendering it's children. It finishes by calling Page.EndFormRender and Page.OnFormPostRender. So let's start with Page.OnFormRender.

This method looks like:

internal void OnFormRender()

{

if (this._fOnFormRenderCalled)

{

throw new HttpException(SR.GetString("Multiple_forms_not_allowed"));

}

this._fOnFormRenderCalled = true;

this._inOnFormRender = true;

}



This is where we erroring on our second call to the Page's render method. This method makes sense, essentially it ensures there is only form by tracking this at the page level in the _fOnFormRenderCalled variable. Now in the FormPostRender event they are reseting the flag which tracks if a form is activley being rendered; however they do not reset the _fOnFormRenderCalled which is correct. The problem is this variable is never set back to false. Even when the page is finished rendering.

So our solution is to manually reset this private variable using reflection:


FieldInfo fi = typeof(Page).GetField("_fOnFormRenderCalled", BindingFlags.NonPublic BindingFlags.Instance);

if (fi == null)

{

LogWriter.LogError("FieldInfo is null verify _fOnFormRenderCalled still exists on the Page object.");

}

fi.SetValue(this, false);

I highly recommend avoiding this, and if anyone has a better solution feel free to share. I'd also write your code defensivley. Well I hope at least someone somwhere finds this helpful, and if you happen to be a dev at Microsoft in the ASP.Net team why not reset these variable before the Page's render method exits.

2 comments:

David said...

That field is no longer available.

I've resorted to calling Response.Clear after rendering the page, and then making a Server.Transfer back to the same URL with any necessary values in the Querystring.

Maybe that info will help someone else who's stuck. I'm using VB.NET.

Josh Berke said...

I assume your using 4.0, Were using 3.5 and this still works. I'll give your method a shot when we upgrade.