Keep ViewState out of Page for Performance Enhancement Redux

New features in ASP.NET 2.0 enable developers to get Viewstate out of the Page and on to the server for up to 500% better throughput.

 ASP.NET 2.0 now provides built in support for PageStateAdapters, which utilize a page adapter and the new SessionPageStatePersister class.  You can also write your own Adapter class. You can find examples by other developers typically storing ViewState in files on the server, although this really doesn't make much sense to me since storing a judicious amount of Viewstate in memory (Session or Cache) is orders of magnitude faster.

The cool thing about ASP.NET 2.0 is that Scott Guthrie and his merry band have either providerized or adapterized just about everything, making the developer's job of "doing stuff" humongously easier.

The SessionPageStatePersister class (when decompiled) looks like so:

[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.LinkDemand, Level=AspNetHostingPermissionLevel.Minimal)]
public class SessionPageStatePersister : PageStatePersister
{
  // Fields
  private const string _viewStateQueueKey = "__VIEWSTATEQUEUE";
  private const string _viewStateSessionKey = "__SESSIONVIEWSTATE";

  // Methods
  public SessionPageStatePersister(Page page) : base(page)
  {
    HttpSessionState session = null;
    try
    {
      session = page.Session;
    }
    catch
    {
    }
    if (session == null)
    {
      throw new ArgumentException(SR.GetString("SessionPageStatePersister_SessionMustBeEnabled"));
    }
  }

  public override void Load()
  {
    if (base.Page.RequestValueCollection != null)
    {
      try
      {
        string requestViewStateString = base.Page.RequestViewStateString;
        string second = null;
        bool flag = false;
        if (!string.IsNullOrEmpty(requestViewStateString))
        {
          Pair pair = (Pair) Util.DeserializeWithAssert(base.StateFormatter, requestViewStateString);
          if ((bool) pair.First)
          {
            second = (string) pair.Second;
            flag = true;
          }
          else
          {
            Pair pair2 = (Pair) pair.Second;
            second = (string) pair2.First;
            base.ControlState = pair2.Second;
          }
        }
        if (second != null)
        {
          object obj2 = base.Page.Session["__SESSIONVIEWSTATE" + second];
          if (flag)
          {
            Pair pair3 = obj2 as Pair;
            if (pair3 != null)
            {
              base.ViewState = pair3.First;
              base.ControlState = pair3.Second;
            }
          }
          else
          {
            base.ViewState = obj2;
          }
        }
      }
      catch (Exception exception)
      {
        HttpException e = new HttpException(SR.GetString("Invalid_ControlState"), exception);
        e.SetFormatter(new UseLastUnhandledErrorFormatter(e));
        throw e;
      }
    }
  }

  public override void Save()
  {
    bool x = false;
    object y = null;
    Triplet viewState = base.ViewState as Triplet;
    if ((base.ControlState != null) || ((((viewState == null) || (viewState.Second != null)) || (viewState.Third != null)) && (base.ViewState != null)))
    {
      HttpSessionState session = base.Page.Session;
      string str = Convert.ToString(DateTime.Now.Ticks, 0x10);
      object obj3 = null;
      x = base.Page.Request.Browser.RequiresControlStateInSession;
      if (x)
      {
        obj3 = new Pair(base.ViewState, base.ControlState);
        y = str;
      }
      else
      {
        obj3 = base.ViewState;
        y = new Pair(str, base.ControlState);
      }
      string str2 = "__SESSIONVIEWSTATE" + str;
      session[str2] = obj3;
      Queue queue = session["__VIEWSTATEQUEUE"] as Queue;
      if (queue == null)
      {
        queue = new Queue();
        session["__VIEWSTATEQUEUE"] = queue;
      }
      queue.Enqueue(str2);
      SessionPageStateSection sessionPageState = RuntimeConfig.GetConfig(base.Page.Request.Context).SessionPageState;
      int count = queue.Count;
      if (((sessionPageState != null) && (count > sessionPageState.HistorySize)) || ((sessionPageState == null) && (count > 9)))
      {
        string name = (string) queue.Dequeue();
        session.Remove(name);
      }
    }
    if (y != null)
    {
      base.Page.ClientState = Util.SerializeWithAssert(base.StateFormatter, new Pair(x, y));
    }
  }
}

So, for example, if I wanted to create a CachePageStatePersister, I would model it after the above class, derive from PageStatePersister base, and substitute Cache for Session where appropriate. We could just as well have added in compression here, although I wonder how much of an improvement the additional CPU churning might get us.

I've also seen a number of articles and blog posts recently where developers are attempting to hijack the ViewState hidden field and its contents and add it back in at the bottom of the Page, just before the closing </FORM> tag. This is done ostensibly for SEO reasons as the GoogleBot and other crawlers supposedly only read the first "XX" bytes of the page and if it is glommed up with ViewState, the page isn't indexed properly. There is little point in doing this in my opinion, because you may have made your page "SEO Friendly", but you may still have a huge StateBag of ViewState making the round trip with the page, only now it is in a different position on the page! Fawlty logic, methinks...

One of the biggest performance killers in ASP.NET applications is ViewState. Period! If you doubt this, please read Tess Ferrandez excellent post here, subtitled "Death by ViewState". She's nailed the problem cold, she just doesn't offer all the solutions.

The very first thing we want to do of course is disable ViewState on the page unless it is absolutely necessary.  Failing that, the following solution will go a long way towards not only making the page SEO-Friendly, but eliminating that glop of ViewState by storing it on the server.

In a previous article,   http://www.eggheadcafe.com/articles/20040613.asp , I did considerable testing on this and determined that storing ViewState in Cache was the most efficient. Session was a bit slower, but it obviated the need to create a unique key for each instance. The new built - in SessionPageStatePersister class automatically accounts for nine (9) pages "back" of ViewState due to users pressing the "Back" button on their browser. So, in exchange for sacrificing a small amount of throughput due to falling back to Session storage of ViewState instead of the slightly faster Cache storage, we get a lot of new built-in functionality without having to custom-code anything. To me, that's a pretty good trade-off.

The first step is to create a Page Adapter that returns an instance of the built-in SessionPageStatePersister class (instead of the default HiddenFieldPageStatePersister class which stores viewstate in a hidden field on the client, as we all well know). 

First, create a class library that contains the following code. The easiest way to do this (especially for debugging and testing) is to simply add a new "class library" project to your solution:

using System;
using System.Collections.Generic;
using System.Text;

using System.Web.UI;

namespace PAB.Web
{
    public class PageStateAdapter : System.Web.UI.Adapters.PageAdapter
    {
        public override PageStatePersister GetStatePersister()
        {
            return new SessionPageStatePersister(this.Page);
        }
    }
}

That's all it takes. But, what about customization? For example, one one site I have a custom 404.aspx page that is used to handle extensionless UrlRewriting. Unfortunately, I cannot have session state involved on this one page as it interferes. So, I can customize the StatePersister:

public override PageStatePersister GetStatePersister()
{
if (HttpContext.Current.Request.RawUrl.IndexOf("404.aspx") > -1)
{
      return new HiddenFieldPageStatePersister(this.Page);
}
else
{
     return new SessionPageStatePersister(this.Page);
}
}

Next, create a new "Default.browser" special browser config file that specifies that your new page adapter should be used for all browsers:

<browsers>
    <browser refID="Default">
        <controlAdapters>
           <adapter controlType="System.Web.UI.Page" adapterType="PAB.Web.PageStateAdapter" />
        </controlAdapters>
    </browser>
</browsers>


 Add a new ASP.NET  "App_Browsers" Folder to your web app, using the built-in context menu option in Visual Studio 2005, and then add your new Default.browser file into the folder (and bring it into your project from within Visual Studio Solution Explorer).

You will need to copy your new App_Browsers folder containing your Default.browser Browser file to the root of your web application on your server,along with a copy of   your (in my case) PAB.Web.dll assembly from your new project into the /bin folder on the server.

 Now, all requests that come in from any type of browser will sink using your new adapter that returns a SessionPageStatePersister instance.  You can verify that this is working by viewing a page on your site, then use trace.axd and view the session information; you should see that it contains the viewstate info entries from the decompiled class example above.  Viewing the client page source will also show that the majority of the viewstate is no longer stored on the client; there will still be a nominal amount (about 200 bytes) of Viewstate present in the __VIEWSTATE field.

This is the same Adapter configuration that allows us to do many other things such as CSS-friendly control adapters and much more -- well worth some additional study.

I have this running on two separate sites now, and the improvements in throughput are so noticeable, there is really no need for any speed testing. Not only that, but I've cut my bandwidth consumption considerably, which makes the sites more profitable since I don't have to pay for extra bandwidth from the hosting company.

By Peter Bromberg   Popularity  (6162 Views)