Development With A Dot

Blog on development in general, and specifically on .NET

Sponsors

News

My Friends

My Links

Permanent Posts

Portuguese Communities

Saving View State in Cache

Most people who have worked with ASP.NET web forms know about view state; they may have both benefited from and cursed it, since it allows simple things to be done very easily but also as easily doing very stupid things, like filling up an HTML page with several megabytes of data which have to be submitted on each postback.

What some people won’t know is that the way the view state is maintained is extensible and based on a provider model. Besides the default way of storing view state in an hidden field, ASP.NET includes another implementation of the provider which allows state to be stored in the session, thus keeping it out (but for a small part) of the HTML. The abstract base provider class is PageStatePersister and the two included implementations are HiddenFieldPageStatePersister and SessionPageStatePersister.

So, what keeps you from storing everything in the session? It seems like an easy choice… there are some drawbacks, however:

  • All browser tabs share the same session, so care must be taken if the same page is open on two tabs, because changes to the session made in one page will affect the other, since the data is shared between them;
  • Because data is kept indefinitely at the session, either until the session expires – which may never happen – or it is explicitly removed from it, memory will grow up enormously.

So I decided to go for another option: storing data on the cache, with an expiration value. This should be a reasonable default, after which, data would be gone, if no postback happens in the mean time. I made it configurable: a global default timeout may be specified on the Web.config file or specifically for a web Page in its Items collection. It is using sliding expiration, so each time the cache is touched is will keep it alive for an equal period of time.

One problem is how to identify uniquely a page instance, so that its view state is local to it only, but I have already solved some time ago.

OK, so let’s see the code:

   1: public class CachePageStatePersister : PageStatePersister
   2: {
   3:     private const String RequestId = "__REQUESTID";
   4:     private const String ViewStateId = "VIEWSTATE:{0}";
   5:     private const String ControlStateId = "CONTROLSTATE:{0}";
   6:     private const String CacheTimeoutMinutesKey = "CacheTimeoutMinutes";
   7:  
   8:     private const Int32 DefaultCacheTimeoutMinutes = 10;
   9:  
  10:     public CachePageStatePersister(Page page): base(page)
  11:     {
  12:  
  13:     }
  14:  
  15:     public override void Load()
  16:     {
  17:         String id = this.Page.ID;
  18:  
  19:         if ((String.IsNullOrWhiteSpace(id) == true) || (id == "__Page"))
  20:         {
  21:             id = this.Page.Request.Form[RequestId];
  22:         }
  23:  
  24:         if (String.IsNullOrWhiteSpace(id) == true)
  25:         {
  26:             throw (new InvalidOperationException("Missing page id"));
  27:         }
  28:  
  29:         this.Page.ID = id;
  30:  
  31:         this.Page.ClientScript.RegisterHiddenField(RequestId, id);
  32:  
  33:         String viewStateId = String.Format(ViewStateId, id);
  34:         String controlStateId = String.Format(ControlStateId, id);
  35:  
  36:         this.ViewState = this.Page.Cache[viewStateId];
  37:         this.ControlState = this.Page.Cache[controlStateId];
  38:     }
  39:  
  40:     public override void Save()
  41:     {
  42:         if ((this.ControlState != null) || (this.ViewState != null))
  43:         {
  44:             String id = this.Page.ID;
  45:  
  46:             if ((String.IsNullOrWhiteSpace(id) == true) || (id == "__Page"))
  47:             {
  48:                 id = Guid.NewGuid().ToString();
  49:  
  50:                 this.Page.ID = id;
  51:  
  52:                 this.Page.ClientScript.RegisterHiddenField(RequestId, id);
  53:             }
  54:  
  55:             Int32 cacheTimeoutMinutes = DefaultCacheTimeoutMinutes;
  56:  
  57:             if (String.IsNullOrWhiteSpace(ConfigurationManager.AppSettings[CacheTimeoutMinutesKey]) == false)
  58:             {
  59:                 Int32.TryParse(ConfigurationManager.AppSettings[CacheTimeoutMinutesKey], out cacheTimeoutMinutes);
  60:             }
  61:  
  62:             if (this.Page.Items[CacheTimeoutMinutesKey] is Int32)
  63:             {
  64:                 cacheTimeoutMinutes = (Int32)this.Page.Items[CacheTimeoutMinutesKey];
  65:             }
  66:  
  67:             String viewStateId = String.Format(ViewStateId, id);
  68:             String controlStateId = String.Format(ControlStateId, id);
  69:  
  70:             if (this.ViewState != null)
  71:             {
  72:                 this.Page.Cache.Add(viewStateId, this.ViewState, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(cacheTimeoutMinutes), System.Web.Caching.CacheItemPriority.Default, null);
  73:             }
  74:  
  75:             if (this.ControlState != null)
  76:             {
  77:                 this.Page.Cache.Add(controlStateId, this.ControlState, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(cacheTimeoutMinutes), System.Web.Caching.CacheItemPriority.Default, null);
  78:             }
  79:         }
  80:     }
  81: }

Finally, how to use this. There are two options:

   1: protected override PageStatePersister PageStatePersister
   2: {
   3:     get
   4:     {
   5:         return (new CachePageStatePersister(this));
   6:     }
   7: }
  • Or you can apply the persister through a control adapter, which is less intrusive, in two easy steps:

- Create a custom PageAdapter:

   1: public class CachePageAdapter : PageAdapter
   2: {
   3:     public override PageStatePersister GetStatePersister()
   4:     {
   5:         return (new CachePageStatePersister(this.Page));
   6:     }
   7: }

- Add a file named, for example, Default.browser, to the App_Browsers folder:

   1: <browsers>
   2:     <browser refID="Default">
   3:         <controlAdapters>
   4:             <adapter controlType="System.Web.UI.Page" adapterType="CachePageAdapter" />
   5:         </controlAdapters>
   6:     </browser>
   7: </browsers>

Of course, you can target page types other than the base Page itself by changing the value on the controlType attribute.

You can change the default cache timeout from 10 minutes to something else, globally in the Web.config:

   1: <configuration>
   2:     <appSettings>
   3:         <add key="CacheTimeoutMinutes" value="15"/>
   4:     </appSettings>
   5: ...
   6: </configuration>

Or by code, page by page:

   1: protected override void OnLoad(EventArgs e)
   2: {
   3:     this.Items["CacheTimeoutMinutes"] = 15;
   4:  
   5:     base.OnLoad(e);
   6: }
As always, hope this is useful, and looking forward to hearing your feedback!

Comments

lynn eriksen said:

You can try that. But you would be better served by letting ViewState work the way it is supposed to and designing your pages effiently. Or try asp.net mvc.

# April 24, 2012 4:16 PM