So, I made a post yesterday about the way we're keeping our view state persisted on the server.

I had been feeling pretty good about the methods that I came up with and the results of not only our tests, but the real world tests that we've seen with this code "out in the wild" per se...  I started to do a lot of reading.  When I say a lot, I mean I fired up google, started at the top and went through every "view state" related link I could find to see if there was something I was missing or hadn't thought of.

On the fine art of being humbled by those smarter than you...

Scott Hanselman has been known for putting together some view state hacks over the years.  He has even written a book (well, part of a book) that has had some of his view state hacks it in.  If you read through all of his blog posts relating to view state (and I have) you begin to get the subtle impression (that is, subtle like a jack hammer outside your bedroom window at 4am) that he is against keeping view state separated from the page.  His post title "Moving ViewState to the Session Object and more Wrongheadedness" on 2/28/2004 really grabbed my attention.  More specifically the paragraph where he said:

If you store ViewState in the Session object in this way, you are assuming the user will access only one page at a time, and you may confused other pages in their attempt to load values from Bogus ViewState.  More importantly, what happens if the user opens new browser window, and starts accessing DIFFERENT pages but sharing the same session.  Well, you get the idea.

But I SWEAR it worked on my machine!!

I know that I had tested the situation Scott had outlined above before pushing my code live.. I know that there was no way that it could fall prey to the edge situation that he mentions... Then the waves of self doubt start to roll in.

So I fired up the exact same sample application that I provided as a download and started running some tests.

  1. Single Browser (IE7) - No problems at all.  Everything works as expected
  2. Single Browser (FF3) - No problem at all.  Everything works as expected
  3. Two Browsers (IE7 / FF3) - No problem at all.  Everything works as expected. 
  4. Two Browsers (IE7 / IE7) - No problem at all.  Everything works as expected.

At that point, I started feeling pretty good about myself... thoughts of "Oh, he wrote that four and a half years ago.. things have changed since then." and "I knew I had tested this.. i r so smart!"...

And then the darkness came...

Scott had said something else in his post that I read and re-read over and over again (the bold is my emphasis):

It pulls the ViewState from the request NameValueCollection (including the Form collection, etc).  Each 'instance' of a page has it's own ViewState.

Then it hit me.... I fired up my test again with IE, created some view state with changed data... then, instead of opening up another IE window... I opened up my test page in another IE Tab.. Low and behold, the second tab was stomping all over the first tab's view state data.  Welcome to the black parade.

Get defines the start of a new page instance

After a brief period of huddling in the corner balled up in the fetal position crying over my own insignificance, I went back and started thinking about how we know when a page is a new instance or not.  The answer is actually really clear.  Every time a HTTP Get verb is called on a page, that page is spun up in a new instance.  with ASP.Net, all page interaction is handled through the POST verb (why it's called Post Back after all).  So all I have to do is key the persisted view state, not only to the user's session id and requested page path, but to an identifiable page instance when the Get was first called.  Once I establish that unique key based on those three factors, then I need to re-use that key and persisted cache file for every subsequent page post.

Realizing that, it's just a matter of modifying the Save() method of the CachePageStatePersister to only generate a new key and cache file if and only if the requested page is not in a post back state.  Otherwise, it should pull the key from the pages hidden field, and cache file path from the Page.Cache..

   1: public override void Save()
   2: {
   3:     if (ViewState != null || ControlState != null)
   4:     {
   5:         if (Page.Session == null)
   6:             throw new InvalidOperationException("Session is required for CachePageStatePersister (SessionID -> Key)");
   7:  
   8:         string vsKey;
   9:         string cacheFile;
  10:  
  11:         if (!Page.IsPostBack) // create a unique cache file and key based on this user's session and page instance (time)
  12:         {
  13:             string sessionId = Page.Session.SessionID;
  14:             string pageUrl = Page.Request.Path;
  15:             vsKey = string.Format("{0}{1}_{2}_{3}", VSPREFIX, pageUrl, sessionId, DateTime.Now.Ticks);
  16:  
  17:             string cachePath = Page.MapPath(CACHEFOLDER);
  18:             if (!Directory.Exists(cachePath))
  19:                 Directory.CreateDirectory(cachePath);
  20:             cacheFile = Path.Combine(cachePath, BuildFileName());
  21:         }
  22:         else    // get our vs key from the page, re use it, and the cache file (pulled from page.cache)
  23:         {
  24:             vsKey = Page.Request.Form[VSKEY];
  25:             if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
  26:             cacheFile = Page.Cache[vsKey] as string;
  27:             if (string.IsNullOrEmpty(cacheFile)) throw new ViewStateException();
  28:         }
  29:  
  30:         IStateFormatter frmt = StateFormatter;
  31:         string state = frmt.Serialize(new Pair(ViewState, ControlState));
  32:         using (StreamWriter sw = File.CreateText(cacheFile))
  33:             sw.Write(state);
  34:  
  35:         Page.Cache.Add(vsKey, cacheFile, null, DateTime.Now.AddMinutes(Page.Session.Timeout),
  36:                        Cache.NoSlidingExpiration, CacheItemPriority.Low, ViewStateCacheRemoveCallback);
  37:         Page.ClientScript.RegisterHiddenField(VSKEY, vsKey);
  38:     }
  39: }

 

So, with this small change in the Save() method, our server side view state persister truly supports accurate retrieval of a pages view state, no matter what the user is doing on their end with whichever browser configuration they may be running.

Download Sample Project (10.3k)