An HttpModule that moves ViewState to the bottom

Nov 28, 2006

There are two reasons why it is desirable to do so. The first is for letting search engines see more of your content rather than the big portion of ViewState many sites have. The other is perceived rendering time, which means that the content loads faster because it renders before the ViewState while the total rendering time remains the same. That will decrease the load time of your website’s content.

Techniques to move the ViewState to the bottom of the WebForm has been published many times before. What I wanted was adding the functionality to an HttpModule. The technique to move the ViewState is borrowed from Scott Hanselman while the HttpModule implementation is my own. As Scott writes, it is a very low impact technique (0.000995 second) even though it hasn’t been fully tested for a variety of scenarios.

The goal I’m trying to achieve is to build a reusable component that has 100% plug ‘n play capabilities. That’s where the HttpModule comes in. You can just drop it into any existing website without changing any code.

I see no reasons why not to move the ViewState to the bottom, which makes me believe that Microsoft should have done that by default in the first place.

Implementation

Download the ViewstateModule.cs below and put in the App_Code folder of your website. Then add these lines to the web.config and you’re ready to go.

<httpModules>

  <add type="ViewstateModule" name="ViewstateModule" />

</httpModules>

Download

ViewstateModule.zip (1,06 KB)

* $4.95/month BlogEngine.net Hosting – Click Here!

Comments (34) -

 NinjaCross
NinjaCross
11/28/2006 5:52:17 PM #

Thanks for posting, but unfortunately the module doesn't work correctly.
The viewstate is removed, but never restored on the bottom cause the following condition (line 123) is never true.

if (formEndStart >= 0)
{
   html = html.Insert(formEndStart, viewstateInput);
}

The test application is so basic, and no other http modules are hosted (so no interferencies are expected).
I didn't look deeply in the code, so i can't figure out the motivation.
If I've got a little spare time, I'll try to investigate Smile

Mads Kristensen
Mads Kristensen
11/28/2006 6:11:37 PM #

That's strange. It works perfectly correct for me, even on a basic application. The only way the statement can never be true is because you don't have any form on your web page or the form-tag is not lowercased. If that's not the case, then I'm a little curios to hear what you find out from your investigation. I'll try to debug and see if I can come up with something. I think it is unlikely I find something wrong because it has worked for me on different websites in the past.

 NinjaCross
NinjaCross
11/28/2006 6:27:58 PM #

There is a form, and it's tag is lower-cased.
Maybe other factors are concurring in the problem... I'll look at the code a little bit deeply and I'll let you know

Mads Kristensen
Mads Kristensen
11/28/2006 6:28:49 PM #

Thanks. I look forward to hearing about your findings.

 NinjaCross
NinjaCross
11/28/2006 6:41:56 PM #

Ok, I  found a first hint.
I noticed that the whole html stream is not send in a unique call to "Write(byte[] buffer, int offset, int count)".
It's instead splitted into many chunks and sent to the method with sequential calls.
The problem is in the fact that on the first call
if (startPoint >= 0)
is true, but in the current chunk there isn't the form closing tag cause it's in the second (or Nth) call of the method Write.
When in the Nth call of Write the buffer contains the form closing tag, the  condition
if (startPoint >= 0)
is not true anymore.

A solution could be to extract  
if (formEndStart >= 0)
from the block of
if (startPoint >= 0)
and handling them separately, but this would imply conflicts in the case of a page with multiple forms if an appropriate check is not provided too.
A second solution would be to force (in some way) ASP.NET to send to Write the whole stream, instead of splitting it into many calls.
I'll leave to you the honour to find the most correct way to solve this problem ;)

Mads Kristensen
Mads Kristensen
11/28/2006 7:28:09 PM #

That might be because you have set Buffer="false" on either the page or in the web.config.

 NinjaCross
NinjaCross
11/28/2006 7:54:53 PM #

I already checked this... and the Buffer attribute is not specified in the page, neither in the web.config or in the machine.config.
Maybe it's value is optional also in the machine.config (I didn't check the schema declaration, so I can only suppose...) and the default value make the module behave in this way.
Unfortunately, if this is the real cause of the problem, I think that the module should be redesigned in some ways to make it more robust.
Infact I dont' think it would be recommendable to use it if it's prone to global configuration aspects (think i.e. to deploy your applications on cutomers' servers where you can't decide how to apply this kind of politics)
Anyway, thanks again for posting Smile

Mads Kristensen
Mads Kristensen
11/28/2006 8:12:51 PM #

The Buffer attribute is True by default and it makes no sense to change it on the machine.config level because it is by nature an application setting. I still don't understand why you cannot make it work when it has been working perfectly for me on several projects.

 Adel K. Khalil
Adel K. Khalil
11/28/2006 10:49:07 PM #

i just wanted to thank you so much, simple yet great idea.

Ingmar Hoogendoorn
Ingmar Hoogendoorn
11/29/2006 10:37:45 AM #

Nice! I think it's better to change the source a bit. If (on whatever reason) your code can't find the </form> tag, the viewstate will be removed and not inserted. So it's better to change the code to:

        int endPoint = html.IndexOf("/>", startPoint) + 2;
        int viewstateLength = endPoint - startPoint;
  string viewstateInput = html.Substring(startPoint, viewstateLength);
    
        int formEndStart = html.IndexOf("</form>") - 1;
        if (formEndStart >= 0)
        {
      html = html.Remove(startPoint, viewstateLength);
          html = html.Insert(formEndStart - viewstateLength, viewstateInput);
        }
      }

 Paul Kinlan
Paul Kinlan
11/29/2006 1:27:42 PM #

I seem to remember that the reason why viewstate is at the top of the page, is because if the page posts back before it finishes rendering at least the internal state of the page on the server can restore it's state correctly.  If you have the viewstate at the end of the page and it hasn't finished rendering (i.e the viewstate has not been rendered) before the screen posts back then you are up a certain creek without a paddle...

For that reason alone I would not recommend anyone use this method.

Ingmar Hoogendoorn
Ingmar Hoogendoorn
11/29/2006 3:14:33 PM #

If a page load is taking so long, there is something wrong. I don't think it's a problem, 'normal' pages are being pushed to the server in no time and before a user clicks something, their will be even more time.

Remember ASP.NET renders the page before it's getting submit to the client, so this shouldn't be a problem at all. And it does help for search engine optimization.

 NinjaCross
NinjaCross
11/29/2006 4:10:40 PM #

Uhm... I think you are not considering applications that imply large interactive reports.
Sometimes my xhtml reports has got a weight of 800-900KB, and I can ensure you that the loading+rendering time is not exactly something that I would call "short" ;)
In that case, a module like this (even if so interesting and usefull for many other cases) could be the source of big headeaches.
Obviously, everything has got its own application field, so also the usage of this module should be reasoned

 Paul Kinlan
Paul Kinlan
11/29/2006 4:39:05 PM #

"If a page load is taking so long, there is something wrong"

Well not really, 3 seconds is not considered un-normal and whilst the page may have rendered with the viewstate not the users might have already clicked the link because they knew where it would be.  

As long as you can stop them from submitting requests back to the server, you are okay otherwise you could easily have the situation I have described, for instance if someone has a slow internet connection.

 David Neal
David Neal
11/29/2006 10:12:21 PM #

Very cool!  However, I found that in my case:

int formEndStart = html.IndexOf("</form>") - 1;

should actually be:

int formEndStart = html.IndexOf("</form>");

If there happens to be no space between the last HTML tag and </form> then it was inserting the viewstate before the closing ">" of the previous HTML tag.

Deepesh shah
Deepesh shah
11/30/2006 3:16:18 PM #

Worked Like a charm for me Smile

Many thanks

Deepesh

 Sachman Bhatti
Sachman Bhatti
11/30/2006 11:16:43 PM #

Like David Neal I also found that same problem.  It would produce an extra ">" and not validate.

 Dotnetshadow
Dotnetshadow
1/18/2007 3:30:28 PM #

Hi Guys,

I seem to have a problem with this code when I use a compression module, I've tried to make sure one module loads before the other but still the same problem. Has anyone tried this with a compression module?

Regards DotnetShadow

 Martin Meixger
Martin Meixger
2/23/2007 8:39:21 PM #

Hi,

thanks for this nice piece of code.
Moving the Viewstate by overriding Render() didnt work for me, because it breaks the asp wizard control behavior.
I used Mad's code and tweak it - as David Neal and NinjaCross already stated before.
Now it seems to work flawless.
Please let me know, if this works for you also.

here my tweaked code based on Mad's work:

www22.brinkster.com/gvspm/viewstatemodule.zip

Regards
Martin Meixger

 Michael Cole
Michael Cole
3/20/2007 9:21:50 PM #

Modified the code a little:

1. moved viewstateInput variable outside the write function
2. moved the check for closing form tag outside the check for viewstate startpoint if statement
3. removed the "-1" after the check for closing form tag

Renders perfectly just where it needs to be ... at the bottom of the page.

code:
      string viewstateInput;
    public override void Write(byte[] buffer, int offset, int count)
    {
      byte[] data = new byte[count];
      Buffer.BlockCopy(buffer, offset, data, 0, count);
      string html = System.Text.Encoding.Default.GetString(buffer);

      int startPoint = html.IndexOf(_viewstate tag here);
      if (startPoint >= 0)
      {
        int endPoint = html.IndexOf("/>", startPoint) + 2;
        viewstateInput = html.Substring(startPoint, endPoint - startPoint);
        html = html.Remove(startPoint, endPoint - startPoint);
      }
      int formEndStart = html.IndexOf(form tag here);
      if (formEndStart >= 0)
      {
        html = html.Insert(formEndStart, viewstateInput + "\n");
      }
      

      byte[] outdata = System.Text.Encoding.Default.GetBytes(html);
      _sink.Write(outdata, 0, outdata.GetLength(0));
    }
End Code

Thanks for the original code. Been trying to figure out how to use httpModules with no complete example until I stumbled upon this blog.

Cheers,
Michael Cole

accessdenied
accessdenied
3/30/2007 6:07:15 AM #

What if I have 2 "form" tags in my code? For example when I added Google Search to my page. This code inserts viewstate before first occurrence of "/form".

Free images for webmasters
Free images for webmasters
3/30/2007 6:56:10 AM #

If you have 2 (or more) "form" tags on a page, you have to change
int formEndStart = html.IndexOf  
on
int formEndStart = html.LastIndexOf  

Korayem.NET
Korayem.NET Egypt
6/17/2007 12:29:55 PM #

It's worth noting that this breaks ASP.AJAX behavior by returning errors: "object reference not set to an object".

Ted Jardine
Ted Jardine Canada
6/27/2007 3:21:30 PM #

Also note that if you're using an asp:Substitution control (for dynamic data in a page with PageOutput caching), this method does not work. Took me a bit to figure that out.

I'm sure there's a way around it, but the fact that problems can arise if there's a postback prior to full page render (as mentioned above), I'll just keep using a base page that removes all (well, most) traces of ViewState for the pages I don't need it on, and the very few I do, I'll just live with it as is.

[soksa]icy
[soksa]icy United States
6/27/2007 7:57:25 PM #

Perfect example. Thanks a milllion Ingmar Hoogendoorn. And of course Michael Cole, for saving me the trouble of correcting the code. It's so funny. I never read the comments, just copy & paste the code/module, run into all the troubles posted here, solve them, then come and try to post the solution only to find out that somebody had already been there, done that Smile)

...When everything else fails, read the documents!!! LOL

Kudos

Icy

[soksa]icy
[soksa]icy United States
6/29/2007 1:42:20 PM #

Here's a weird case I've run into today. The viewstate was sooo long that it was cut in half on the first loop of Write(). So I had to modify the routine a little bit:

/************* begin code ********************/
string viewstateInput;
bool isCompleted = false;
public override void Write(byte[] buffer, int offset, int count)
{
  byte[] data = new byte[count];
  Buffer.BlockCopy(buffer, offset, data, 0, count);
  string html = System.Text.Encoding.Default.GetString(buffer);
  int endPoint;
  int startPoint = html.IndexOf("<input type=\"hidden\" name=\"__VIEWSTATE\"");
  if (startPoint == -1) startPoint = 0;

  if (!isCompleted)
  {
    endPoint = html.IndexOf("/>", startPoint) + 2;
    if (endPoint < startPoint && !isCompleted)
    {
      //this is probably the first loop and the viewstate has started somewhere in the buffered output,
      //but not completed on this run. so grab whatever there is after the start point to the end
      endPoint = html.Length;
      viewstateInput = html.Substring(startPoint, endPoint - startPoint);
    }
    else
    {
      //viewstate end was captured
      viewstateInput = html.Substring(startPoint, endPoint - startPoint);
      isCompleted = true;
    }
    //remove whatever was captured from the buffer
    html = html.Remove(startPoint, endPoint - startPoint);
  }

  int formEndStart = html.IndexOf("</form>");
  if (formEndStart >= 0)
  {
    html = html.Insert(formEndStart, viewstateInput + "\n");
  }


  byte[] outdata = System.Text.Encoding.Default.GetBytes(html);
  _sink.Write(outdata, 0, outdata.GetLength(0));
}
/************* end code *******************/

But there's a bug here: What if the viewstate string was cut right after "/", before the ">" point? We capture the beginning of the viewstate input in a precise manner, but the closing of the tag is captured with the assumption that the buffered string will contain "/>" chars somewhere either after the capture point, or in the above example somwhere after the beginning of the string. What if the first buffer ended with "/" only, and the second buffer started with ">"? The first loop will catch the beginning, and trim the remainder of the output, the second loop will know that the viewstate was cut in half and that it has to finish the rest - but it can never know where to stop, since the "/>" won't be there, and will capture a bunch of code from the HTML output itself 'til the first encounter of "/>".

Himpf, this one's a bit tricky, any ideas? Smile

Cheers

Icy

huobazi
huobazi
7/3/2007 12:14:12 AM #

I think do it in a PageAdapter is better then in HttpModule
and my code is
        private string MoveViewState( string html )
        {
            int startPos = 0;
            startPos = html.IndexOf( "<input type=\"hidden\" name=\"__VIEWSTATE\"" );

            if ( startPos >= 0 )
            {
                int endPos = html.IndexOf( "/>" , startPos ) + 2;

                string viewStateInput = html.Substring( startPos , endPos - startPos );
                html = html.Remove( startPos , endPos - startPos );

                int formEndPos = html.IndexOf( "</form>" , startPos );
                if ( formEndPos >= 0 )
                {
                    html = html.Insert( formEndPos , viewStateInput );
                }
            }

            return html;
        }
The Adapter Pattern can do many thing in asp.net

such as move/rewrite the form action
view the current page of your blog(dotnetblogengine) you can find some code like
<form name="aspnetForm" method="post" action="../post.aspx?id=bf674c52-a761-4886-9cf7-e25532491660"
it is very ugly and display the real url not the rawurl in the browser address bar.
so you can do it in a HtmlFormAdapter
my code
//------------------------------------------------------------------------------
// <Disclaimer>
//     Author:Huobazi http://www.AspxBoy.com && http://huobazi.aspxboy.com
// </Disclaimer>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Reflection;
using System.IO;

namespace AspxBoy.DotNetJobs.Adapters
{
    public class HtmlFormAdapter : BaseAdapter
    {
  
        protected override void Render( System.Web.UI.HtmlTextWriter writer )
        {
            base.Render( new FormRewriteTextWriter( writer ) );
        }

    }

    internal class FormRewriteTextWriter : HtmlTextWriter
    {
        private static readonly string alreadyRewirteKey = "AspxBoy.DotNetJobsCn.FormActionAlreadyRewrote";
        public FormRewriteTextWriter( TextWriter writer )
            : base( writer )
        {
            if ( writer is HtmlTextWriter )
            {
                this.InnerWriter = ( writer as HtmlTextWriter ).InnerWriter;
            }
            else
            {
                this.InnerWriter = writer;
            }
        }

        public override void WriteAttribute( string name , string value , bool fEncode )
        {
            HttpContext context = HttpContext.Current;
            if ( name == "action" && context.Items[ alreadyRewirteKey ] == null )
            {
                value = context.Request.RawUrl;
                context.Items[ alreadyRewirteKey ] = true;
            }
            base.WriteAttribute( name , value , fEncode );
        }
    }

}

By the way : there is no 'China' in the Country dropdownlist when i want to add a comment, why???

Jonah
Jonah Sweden
10/3/2007 12:39:26 PM #

This might be, or not, important for some...

If you're using FreeTextBox with the Viewstatemodule it can create undefined errors.
I often got "object reference not set to an instance of an object" when trying to save data from the FreeTextBox.
It drove me nuts until I realized that the Viewstatemodule was to blame.

BrandSpankingNewbie
BrandSpankingNewbie United States
10/24/2007 7:50:43 PM #

Dotnetshadow (and Mads) - I have the same problem: when I try to use the compression module found here:  along with the 'move the viewstate' module on this page, the viewstate won't budge - it will render at the top of the output html.

If I comment out the compression module, the viewstate module works again - I'd love any input/direction/ideas that anyone has on this - thanks a bunch!

BrandSpankingNewbie
BrandSpankingNewbie United States
10/24/2007 7:52:14 PM #

Sorry, forgot to include the link of the page with the compression module that I'm using (also from Mads): blog.madskristensen.dk/.../...pages-in-ASPNET.aspx

Robi
Robi Israel
12/23/2007 7:57:33 PM #

The code doesn't work if the viewstate is too large.

I get this error: "Length cannot be less than zero.
Parameter name: length."

Tim Brandt
Tim Brandt United States
11/21/2008 8:18:42 PM #


Icy good work around. As for the bug with end symbols: /> being split, the work around isn't that difficult. Just make sure to incorporate additional logic where you continuously inspect your viewStateInput string. If the last char is / then you know to look for >.

Two things to note:

1. Potentially the first check for the viewstate itself could be split. The dirty fix would be to add some html padding to make certain that isn't happening.

2. Likewise the </form> could be getting split, once again some padding could resolve the issue.

Personally I would rather create a class that inherits off of page and override the render method and move the viewstate to the bottom myself. The only problem with that is the legacy site that we are working on has some ineritance going on that we don't want to much with.

PS I realize my post come a year late.

meNoCode

busby seo test
busby seo test United States
1/13/2009 1:27:17 AM #

wow! nice post, thanks for sharing ideas, I really appreciate it.

anandh
anandh India
4/17/2009 4:10:17 AM #

Thanks , We modified your function and it's working perfectly thanks


public override void Write(byte[] buffer, int offset, int count)
        {
            byte[] data = new byte[count];
            Buffer.BlockCopy(buffer, offset, data, 0, count);
            string html = System.Text.Encoding.Default.GetString(buffer);

            int startPoint = html.IndexOf("<input type=\"hidden\" name=\"__VIEWSTATE\"");
            if (startPoint >= 0)
            {
                int endPoint = html.IndexOf("/>", startPoint)+2;
                if (endPoint <= 0)
                {
                    endPoint = html.Length;
                }

                viewstateInput = html.Substring(startPoint, endPoint - startPoint);
                html = html.Remove(startPoint, endPoint - startPoint);
            }

                int formEndStart = html.ToUpper().IndexOf("</FORM>") - 1;
                int eNdPoint = html.IndexOf("</form>");

                if (formEndStart >= 0)
                {
                    html = html.Insert(formEndStart, viewstateInput);
                }
              
            byte[] outdata = System.Text.Encoding.Default.GetBytes(html);
            _sink.Write(outdata, 0, outdata.GetLength(0));
        }

Pingbacks and trackbacks (3)+

Comments are closed

About the author

Mads Kristensen

Mads Kristensen
Program Manager at the Microsoft Web Platform team and founder of BlogEngine.NET.

More...

Month List

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.