Software to help you build a better store
SEARCH
October 28, 2005

Moving VIEWSTATE to the bottom of the page redux

Scott Hanselman and Jeff Atwood recently wrote about moving the ASP.NET hidden _VIEWSTATE form field to the end of the form. The theory behind this movement is that indexers such as Google only read the beginning of the page HTML or treat the beginning HTML with more respect than the rest of the page. If that is true, than it is a bad thing to have the viewstate, which is basically gobbletygook, taking up precious real estate near the top of the page. A side benefit to moving viewstate is that the page source is much easier to read if you don't have to scroll down past the big block of gibberish.

Scott shows how to move the viewstate by overriding the Render method of a base Page class. I believe this is the same approach used by the DotNetNuke folks. I show how to do the same thing with an HttpModule. The main advantage of using an HttpModule is that you can add this to your site without implementing a base Page class or even compiling anything, all you need is one of the assemblies attached below. There are some restrictions:

  • This module must come before any compression module or any other module that changes the page HTML in such a way that the __VIEWSTATE field is hidden. Note that I had to disable the blowery.HttpCompression module that was running on this site to get the MoveViewState module to work. No matter what order the two modules were loaded, the content was always compressed before MoveViewState saw it. My guess is that ASP.NET calls the BeginRequest handlers in some specific order that I don't understand.
  • This module assumes that the first </form> tag on the page corresponds to the first __VIEWSTATE field (normally true, but if you have somehow implemented multiple forms, it may not work for you).
  • This module assumes UTF8 encoding will work to decode and encode your page contents. If you are using some other encoding, then the contents will not look like text and viewstate will not be moved [the page will still display correctly].

The code itself is really very simple. There are just two classes: MoveViewStateModule and MoveViewStateFilter. MoveViewStateModule implements IHttpModule. During IHttpModule.Init it adds an event handler to the HttpApplication.BeginRequest event. During the BeginRequest event, it creates a new MoveViewStateFilter and assigns it to the Response.Filter. Here's the code in both VB and C#:

MoveViewStateModule.vb
Public Class MoveViewStateModule
    Implements System.Web.IHttpModule

    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
    End Sub

    Public Sub Init(ByVal context As System.Web.HttpApplication) 
Implements System.Web.IHttpModule.Init AddHandler context.BeginRequest, AddressOf BeginRequestHandler End Sub Private Sub BeginRequestHandler(ByVal sender As Object, ByVal e As EventArgs) Dim application As System.Web.HttpApplication = CType(sender, System.Web.HttpApplication) application.Response.Filter = New MoveViewStateFilter(application.Response.Filter) End Sub End Class
MoveViewStateModule.cs
public class MoveViewStateModule : System.Web.IHttpModule
{
    public MoveViewStateModule() {}
    void System.Web.IHttpModule.Dispose() {}

    void System.Web.IHttpModule.Init(System.Web.HttpApplication context) 
    {
        context.BeginRequest += new EventHandler(this.BeginRequestHandler);
    }

    void BeginRequestHandler(object sender, EventArgs e)
    {
        System.Web.HttpApplication application = (System.Web.HttpApplication) sender;
        application.Response.Filter = new MoveViewStateFilter(application.Response.Filter);
    }
}

Response.Filter is a System.IO.Stream. Whatever filter we assign must be chained to the previous filter. [And whater filter is assigned later will do the same thing]. The upstream filter will write to our MoveViewStateFilter and our MoveViewStateFilter must write to the downstream filter.

MoveViewStateFilter is implemented as a type of System.IO.MemoryStream. All it does is capture everything that is written to it in a buffer. When the Close method is called, the buffer is converted into a string. The string is searched for the hidden __VIEWSTATE form field. If the field is found, then the entire viewstate form field is moved to the end of the form. Here is the code in both VB and C#:

MoveViewStateFilter.vb
Public Class MoveViewStateFilter
  Inherits System.IO.MemoryStream

  Private _filter As System.IO.Stream
  Private _filtered As Boolean = False

  ''' <param name="filter">A reference to the downstream HttpResponse.Filter.</param>
  Public Sub New(ByVal filter As System.IO.Stream)
    _filter = filter
  End Sub

  ''' <remarks>
  ''' The contents of this filter are written to the downstream filter after the hidden
  ''' __VIEWSTATE form field is moved.
  ''' </remarks>
  ''' <summary>Closes this filter stream.</summary>
  Public Overrides Sub Close()
    If _filtered Then
      If Me.Length > 0 Then
        Dim bytes() As Byte
        Dim content As String = System.Text.Encoding.UTF8.GetString(Me.ToArray)
        Dim viewstateStart As Integer
        viewstateStart = content.IndexOf("<input type=""hidden"" name=""__VIEWSTATE""")
        If viewstateStart >= 0 Then
          Dim viewstateEnd As Integer
          viewstateEnd = content.IndexOf("/>", viewstateStart) + 2
          Dim viewstate As String
          viewstate = content.Substring(viewstateStart, viewstateEnd - viewstateStart)
          content = content.Remove(viewstateStart, viewstateEnd - viewstateStart)
          Dim formEndStart As Integer = content.IndexOf("</form>")
          If formEndStart >= 0 Then
            content = content.Insert(formEndStart, viewstate)
          End If
          bytes = System.Text.Encoding.UTF8.GetBytes(content)
        Else
          bytes = Me.ToArray
        End If
        _filter.Write(bytes, 0, bytes.Length)
      End If
      _filter.Close()
    End If
    MyBase.Close()
  End Sub

  Public Overrides Sub Write(ByVal buffer() As Byte, _
      ByVal offset As Integer, ByVal count As Integer)
    If Not System.Web.HttpContext.Current Is Nothing _
        AndAlso System.Web.HttpContext.Current.Response.ContentType = "text/html" Then
      MyBase.Write(buffer, offset, count)
      _filtered = True
    Else
      _filter.Write(buffer, offset, count)
      _filtered = False
    End If
  End Sub

End Class
MoveViewStateFilter.cs
using System;

namespace StructuredSolutions.MoveViewState
{
  /// <summary>Moves the hidden __VIEWSTATE form field to the end of the form.</summary>
  public class MoveViewStateFilter : System.IO.MemoryStream
  {
    System.IO.Stream _filter;
    bool _filtered = false;

    /// <param name="filter">A reference to the downstream HttpResponse.Filter.</param>
    public MoveViewStateFilter(System.IO.Stream filter)
    {
      _filter = filter;
    }

    /// <summary>Closes this filter stream.</summary>
    /// <remarks>
    /// The contents of this filter are written to the downstream filter after the hidden
    /// __VIEWSTATE form field is moved.
    /// </remarks>
    public override void Close()
    {
      if (_filtered)
      {
        if (this.Length > 0)
        {
          byte[] bytes;
          string content = System.Text.Encoding.UTF8.GetString(this.ToArray());
          int viewstateStart = content.IndexOf("<input type=\"hidden\" name=\"__VIEWSTATE\"");
          if (viewstateStart >= 0)
          {
            int viewstateEnd = content.IndexOf("/>", viewstateStart) + 2;
            string viewstate = content.Substring(viewstateStart, viewstateEnd - viewstateStart);
            content = content.Remove(viewstateStart, viewstateEnd - viewstateStart);
            int formEndStart = content.IndexOf("</form>");
            if (formEndStart >= 0)
              content = content.Insert(formEndStart, viewstate);
            bytes = System.Text.Encoding.UTF8.GetBytes(content);
          }
          else
          {
            bytes = this.ToArray();
          }
          _filter.Write(bytes, 0, bytes.Length);
        }
        _filter.Close();
      }
      base.Close();
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
      if ((System.Web.HttpContext.Current != null)
        && ("text/html" == System.Web.HttpContext.Current.Response.ContentType))
      {
        base.Write(buffer, offset, count);
        _filtered = true;
      }
      else
      {
        _filter.Write(buffer, offset, count);
        _filtered = false;
      }
    }
  }

Once you have the module in hand, all you have to do is convince ASP.NET to load it. This is done by putting the assembly in the bin folder of the site and adding the module to the list of <httpModules> in web.config.

web.config
<system.web>
  <httpModules>
    <!-- To use the C# version, use this add instead of the other one
    <add type="StructuredSolutions.MoveViewState.MoveViewStateModule, StructuredSolutions.MoveViewStateCS"
name="MoveViewStateModule" /> --> <add type="StructuredSolutions.MoveViewState.MoveViewStateModule, StructuredSolutions.MoveViewStateVB"
name="MoveViewStateModule" /> </httpModules> </system.web>

 

Assemblies

Copy either one of the assemblies from the zip file to the bin directory of your site. Make sure the <httpModules> section is loading the one you choose.

File Attachment: MoveViewState.zip (4 KB)

Project Files

File Attachment: MoveViewStateVB.zip (2 KB)
File Attachment: MoveViewStateCS.zip (2 KB)

Update October 22, 2005: Disable filter if content type is not text/html.

Update July 17, 2006: Fix insertion point and add search for meta tag to disable moving viewstate on a specific page:

<meta name="moveviewstate" content="nomove">

This site looks much better in a browser that supports current web standards, but it is accessible to any browser. Download one now This link is to a third party web site which will open in a new window

Some parts of this site will not work effectively on this older browser.
Please consider updating your browser This link is to a third party web site which will open in a new window

SSL