Click here to Skip to main content
15,892,005 members
Articles / Web Development / ASP.NET

Efficient Server-Side View State Persistence

Rate me:
Please Sign up or sign in to vote.
4.96/5 (28 votes)
8 Aug 2008Ms-PL7 min read 126.3K   1.2K   62  
Increasing the performance of your ASP.NET website by reducing the download footprint of your pages.
using System;
using System.IO;
using System.Web.Caching;
using System.Web.UI;
using System.Web.UI.Adapters;

/// <summary>
/// Summary description for PageStateAdapter
/// </summary>
public class PageStateAdapter : PageAdapter
{

    public enum StateStorageTypes { Default, Cache, Session, InPage }


    /// <summary>
    /// Returns an object that is used by the Web page to maintain the control and view states.
    /// </summary>
    /// <returns>
    /// An object derived from <see cref="T:System.Web.UI.PageStatePersister"/> that supports creating and extracting the 
    /// combined control and view states for the <see cref="T:System.Web.UI.Page"/>.
    /// </returns>
    public override PageStatePersister GetStatePersister()
    {
        PageViewStateStorageAttribute psa =
            Attribute.GetCustomAttribute(Page.GetType(), typeof(PageViewStateStorageAttribute), true) as PageViewStateStorageAttribute ??
            new PageViewStateStorageAttribute(StateStorageTypes.Default);

        PageStatePersister psp;
        switch (psa.StorageType)
        {
            case StateStorageTypes.Session:
                psp = new SessionPageStatePersister(Page);
                break;
            case StateStorageTypes.InPage:
                psp = new HiddenFieldPageStatePersister(Page);
                break;
            //case StateStorageTypes.Cache:         // Redundant.. but left in for clarity
            //case StateStorageTypes.Default:
            default:
                psp = new CachePageStatePersister(Page);
                break;
        }
        return psp;
    }

    /// <summary>
    /// Attribute to be applied to a page object.  When applied, determins how the viewstate storage is treated for that individual page.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class PageViewStateStorageAttribute : Attribute
    {
        private readonly StateStorageTypes storageType = StateStorageTypes.Default;

        /// <summary>
        /// Initializes a new instance of the <see cref="PageViewStateStorageAttribute"/> class.
        /// </summary>
        /// <param name="stateStorageType">Type of the state storage.</param>
        public PageViewStateStorageAttribute(StateStorageTypes stateStorageType)
        {
            storageType = stateStorageType;
        }

        internal StateStorageTypes StorageType
        {
            get { return storageType; }
        }
    }

}


public class CachePageStatePersister : PageStatePersister
{

    private const string VSKEY = "__VSKEY";
    private const string VSPREFIX = "VIEWSTATE_";
    private const string CACHEFOLDER = "~/App_Data/Cache";

    public CachePageStatePersister(Page page) : base(page) {}


    /// <summary>
    /// Overridden by derived classes to deserialize and load persisted state information when 
    /// a <see cref="T:System.Web.UI.Page" /> object initializes its control hierarchy.
    /// <para>
    /// 1. Get the viewstate key from the hidden form field
    /// 2. Retrieved the serialized viewstate object from the physical cache
    /// 3. deserialize the vs into a pair object
    /// 4. populate our viewstate and control state objects in the page
    /// </para>
    /// </summary>
    public override void Load()
    {
        if (!Page.IsPostBack) return;   // We don't want to load up anything if this is an inital request

        string vsKey = Page.Request.Form[VSKEY];

        // Sanity Checks
        if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
        if (!vsKey.StartsWith(VSPREFIX)) throw new ViewStateException();

        IStateFormatter frmt = StateFormatter;
        string state = string.Empty;

        string fileName = Page.Cache[vsKey] as string;
        if (!string.IsNullOrEmpty(fileName))
            if (File.Exists(fileName))
                using (StreamReader sr = File.OpenText(fileName))
                    state = sr.ReadToEnd();

        if (string.IsNullOrEmpty(state)) return;

        Pair statePair = frmt.Deserialize(state) as Pair;

        if (statePair == null) return;

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

    /// <summary>
    /// Overridden by derived classes to serialize persisted state information when 
    /// a <see cref="T:System.Web.UI.Page" /> object is unloaded from memory.
    /// <para>
    /// 1. Save our viewstate and controlstate to a new pair object
    /// 2. Serialize the pair object to a string
    /// 3. Save the string to persistant storage (cache folder)
    /// 4. save the pointer to the file in Page Cache
    /// 5. Save the cache pointer to a hidden field object on the page
    /// </para>
    /// </summary>
    public override void Save()
    {
        if (ViewState != null || ControlState != null)
        {
            if (Page.Session == null)
                throw new InvalidOperationException("Session is required for CachePageStatePersister (SessionID -> Key)");

            string vsKey;
            string cacheFile;

            if (!Page.IsPostBack) // create a unique cache file and key based on this user's session and page instance (time)
            {
                string sessionId = Page.Session.SessionID;
                string pageUrl = Page.Request.Path;
                vsKey = string.Format("{0}{1}_{2}_{3}", VSPREFIX, pageUrl, sessionId, DateTime.Now.Ticks);

                string cachePath = Page.MapPath(CACHEFOLDER);
                if (!Directory.Exists(cachePath))
                    Directory.CreateDirectory(cachePath);
                cacheFile = Path.Combine(cachePath, BuildFileName());
            }
            else    // get our vs key from the page, re use it, and the cache file (pulled from page.cache)
            {
                vsKey = Page.Request.Form[VSKEY];
                if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
                cacheFile = Page.Cache[vsKey] as string;
                if (string.IsNullOrEmpty(cacheFile)) throw new ViewStateException();
            }

            IStateFormatter frmt = StateFormatter;
            string state = frmt.Serialize(new Pair(ViewState, ControlState));
            using (StreamWriter sw = File.CreateText(cacheFile))
                sw.Write(state);

            Page.Cache.Add(vsKey, cacheFile, null, DateTime.Now.AddMinutes(Page.Session.Timeout),
                           Cache.NoSlidingExpiration, CacheItemPriority.Low, ViewStateCacheRemoveCallback);
            Page.ClientScript.RegisterHiddenField(VSKEY, vsKey);
        }
    }

    /// <summary>
    /// Call back method for Page.Cache to remove the persisted storage of the cache object from the cache folder
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    /// <param name="reason"></param>
    public static void ViewStateCacheRemoveCallback(string key, object value, CacheItemRemovedReason reason)
    {
        string cacheFile = value as string;
        if (!string.IsNullOrEmpty(cacheFile))
            if (File.Exists(cacheFile))
                File.Delete(cacheFile);
    }

    /// <summary>
    /// Builds a valid file name for our persistant cache storage based on sessionid and requested path
    /// </summary>
    /// <returns><see cref="T:System.String" /></returns>
    private string BuildFileName()
    {
        string fileName = string.Format("{0}{1}__{2}__{3}", VSPREFIX, Page.Session.SessionID, Page.Request.Path, DateTime.Now.Ticks);
        char[] badChars = Path.GetInvalidPathChars();
        foreach (char c in badChars)
            fileName = fileName.Replace(c, '-');
        badChars = Path.GetInvalidFileNameChars();
        foreach (char c in badChars)
            fileName = fileName.Replace(c, '_');
        fileName = string.Concat(fileName, ".cache");
        return fileName;
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Team Leader ICAN
United States United States
Jason Monroe is a Project Manager / Architect with the ICAN grant for the Indiana Department of Education. He has been a professional software developer for 13 years, with the last 8 years focusing on Microsoft technologies.

Comments and Discussions