Tuesday, 20 April 2010

Building A PageStatePersister With AppFabric

One of my favourite demos to do at user group sessions is to show off how you can use the SessionPageStatePersister to store page state information (Viewstate and Controlstate) on the server in Session state instead of round-tripping it to the client. But yesterday it occurred to me that you could do the same thing quite easily with AppFabric caching*.

ASP.NET has shipped with two page state persistence mechanisms since ASP.NET 2.0 was released - HiddenFieldPageStatePersister, which is the one used by default and produces those <input id="__VIEWSTATE" type="hidden" value="AnEnormousBase64EncodedTree" /> tags that we all hate so much, and SessionPageStatePersister, which instead stores the state on the web server in Session state and cuts out the roundtripping. To use the SessionPageStatePersister you need to write an Adapter class and a .browser file:

using System.Web.UI;
using System.Web.UI.Adapters;

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

<browser refID="default">
    <controladapters>
        <adapter controltype="System.Web.UI.Page">
            adapterType="Adapter.Adapter" />
    </controladapters>
</browser>

SessionPageStatePersister is built into the .NET Framework, so our adapter can just new it up and return it in the GetStatePersister function. If we want to build a PageStatePersister with AppFabric, however, we've got a bit more work to do. We need a class that inherits from PageStatePersister so we can implement the Save and Load methods.

using System.Web.UI;
using System.Web.UI.WebControls;

namespace AppFabricPageStatePersister
{

    public class Persister : System.Web.UI.PageStatePersister
    {
        public Persister(Page page) : base(page)
        {
        }


        private const string HiddenFieldId = "StateIdHiddenField";


        public override void Load()
        {
            // Read page's incoming state Id from hidden field
            string stateIdString = Page.Request.Form[HiddenFieldId];


            Pair cachedState = (Pair) CacheHelper.Get(stateIdString);


            ViewState = cachedState.First;
            ControlState = cachedState.Second;
        }


        public override void Save()
        {
            Pair statePair;


            // Build a Pair from the Page's View- and ControlState
            statePair = new Pair(ViewState, ControlState);


            // Generate a new ID to use as the key for storing in the cache
            Guid stateId = Guid.NewGuid();


            // Put the Pair in the cache
            CacheHelper.Put(stateId.ToString(), statePair);


            // Write the key out into the page in a hidden field so we can get it back later
            Page.ClientScript.RegisterHiddenField(HiddenFieldId, stateId.ToString());
        }
    }
}

CacheHelper is an abstraction over AppFabric so my Persister class isn't cluttered with calls to the AppFabric objects:

using System.Web.Configuration
using Microsoft.ApplicationServer.Caching;

namespace AppFabricPageStatePersister
{
    class CacheHelper
    {
         public static object Get(string Key)
         {
            DataCacheFactory factory;
            DataCache cache;
            string cacheName;
            factory = new DataCacheFactory();
            cacheName = WebConfigurationManager.AppSettings["StatePersistenceCacheName"].ToString();
            cache = factory.GetCache(cacheName);
            return cache.Get(Key);
       }

       public static void Put(string Key, object Value)
       {
            DataCacheFactory factory;
            DataCache cache;
            string cacheName;

            factory = new DataCacheFactory();
            cacheName = WebConfigurationManager.AppSettings["StatePersistenceCacheName"].ToString();
            cache = factory.GetCache(cacheName);

            cache.Put(Key, Value);
        }
    }
}

The adapter is straightforward - like the SessionPageStatePersister adapter, all it needs to do is return a new instance of AppFabricPageStatePersister, and the .browser file just needs to point at AppFabricPageStatePersister.Adapter.

namespace AppFabricPageStatePersister

{
    public class Adapter : System.Web.UI.Adapters.PageAdapter
    {
        public override System.Web.UI.PageStatePersister GetStatePersister()
        {
            return new Persister(this.Page);
        }
    }
}

<browser refID="default">

    <controlAdapters>
        <adapter controlType="System.Web.UI.Page"
adapterType="AppFabricPageStatePersister.Adapter" />
    </controlAdapters>
</browser>
Simples.

Download this demo code: C# VB

* Yes, I know you could put your Session state in AppFabric and continue to use the SessionPageStatePersister but I'm assuming you can't/don't want to use Session state.

1 comment:

Hermes said...

It might be worth to mention that you should use ScriptManager.RegisterHiddenField when using ajax in your application instead of Page.ClientScript.RegisterHiddenField.

(I've seen a few people baffled about doing this so I'm saying that the easy way is to use the page version, not the control one)

For more information refer to this page