Persisting View State to the File System
 
Published: 14 Jun 2004
Unedited - Community Contributed
Abstract
This is a run up article to Scott Mitchell's article at MSDN. This article will provide a working example of persisting view state onto the file system with the usage of global unique identifiers; whilst giving a reason why Scott's solution is likely to fail and why this one is a fail-safe solution.
by Justin Lovell
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 46406/ 87

Introduction

[Download Source Code]

How would one start an article based on top of an excellent article from a worthy author? The point of that question is that Scott Mitchell is a fantastic author who recently got an excellent article published at MSDN about the ins and outs of view state.

It is quite a lengthy article - I think that it is at least 10 000 words. I was lucky enough to review that article (as mentioned in the credits of Scott's article) a month ago and I added a few suggestions in.

One of my suggestions that I put forward was regarding the part of where Scott demonstrates that the view state may be saved to the server's disk; the intention being to reduce the download bandwidth that the web browser will have to do and possibly the additional upload of the same information to the server when a post back occurs.

The thought was excellent, I must admit. However, there is a major downfall to his code which could result in ASP.NET throwing an exception stating that view state is corrupted and/or invalid. The reason for the error being thrown is to avoid security exploits which Scott also discussed in his article. The exact reason (or should I say reproduction) of the exception being thrown is a bit complex to explain for the time being but I will explain on the next page of this article.

Exception message

Figure 1: A screen shot of the exception that Scott's code could raise.

The suggestion that I put forward was to avoid the above issue. Due to the extremely long length of the article, Scott could not fit my suggestion in his article (although he did give a hint of my solution but I think it was too brief)… and that is what this article will discuss - using hidden fields and global unique identifiers combination.

Note: I will be referencing to Scott's article at regular intervals so I would recommend that you read his article before continuing reading this one.

Where Scott’s Code Will Fail On Some Occasions

[Download Source Code]

The best way for demonstrating where Scott's code will fail is in a practical example. For the complete source code for the practical example in which I will demonstrate the downfall can be downloaded with the attached source code. The specific file that demonstrates the downfall is conventionally called "ScottsProblem.aspx"

Scott already explained his code in his article in immense detail; hence, I will not regurgitate what he has already written. However, I will explain the code that allows the exception to be thrown. The first code listing to step up to the plate follows (without Scott's code but it is included in the example):

public class ScottsProblem : PersistViewStateToFileSystem {
   protected System.Web.UI.WebControls.PlaceHolder ControlHolder;</font>


<font face="Verdana" size="2">   protected override void OnLoad(EventArgs e) {
      if (Request.QueryString["Control"] != null) {
         ControlHolder.Controls.Add(LoadControl(String.Format(
            "UserControl{0}.ascx", Request.QueryString["Control"])));
      }</font>


<font face="Verdana" size="2">      base.OnLoad(e);
   }
}

That code listing is the code behind for the ScottsProblem page. The ASP.NET page code looks like so (I left out the common code pieces):

<form id="Form1" method="post" runat="server">
   <div>
      <a href="ScottsProblem.aspx?control=A">Show User Control A</a><br/>
      <a href="ScottsProblem.aspx?control=B">Show User Control B</a>
   </div>
   <asp:PlaceHolder id="ControlHolder" runat="server"></asp:PlaceHolder>
</form>

You have guessed it correctly! The above code is a simple demonstration of two user controls being dynamically added to the page based on the query string. That query string variable name is 'Control'. Essentially, the two hyperlinks are there only to facilitate the testing purposes.

There is also one thing that I would like to add: the two user controls are named UserControlA.ascx and UserControlB.ascx respectively. The only thing that they have in common is that their control tree has similar naming. For example, the ID's of the controls inside both user controls looks like the following:

|- Ctl1
|- Ctl2

Because view state saves the data according to the ID of the controls and not by it's control type (ie. DataGrid or TextBox), ASP.NET will throw an exception when it detects that the control structure is completely different between the two post backs.

With that said, time for the exact reproduction of the ASP.NET exception being thrown with Scott's code.

  1. Open a new browser window and browse to (assuming that you downloaded my source code and made a virtual directory named "ViewStateToFS" to the root of the source code):

    http://localhost/ViewStateToFS/ScottsProblem.aspx
  2. From there, instruct the browser to open UserControlB in a new window. Please note that this is an essential step because:

    a) We are replicating a casual user's browsing habits.
    b) Opening a new instance of a browser to show UserControlB will result in the two "browser windows" taking on two different session ID's.
  3. On the first browser window, make the browser browse to the page that will output UserControlA.
  4. And the part that I enjoy (because I have a thing with atomic weapons going off), click the post back button on second windows (the one displaying UserControlB). And then you will find that you are going face to face to with the exception which I said ASP.NET will raise.

    Exception message

Stay tuned because on the next page, I will explain why the exception is being thrown.

Why Does Scott’s Code Fail?

[Download Source Code]

The ironic thing is: Scott already discussed the reason why that exception will be thrown… indirectly. The LosFormatter is clever enough to know when the view state was tampered with. You might ask - but we never tampered with the view state!? Indirectly, yes we did. If you had to step through the logic of the saving and loading of the view state from the medium (file system), you will quickly find that both of the browser windows that were opened shares the same session ID and page path; therefore, sharing the same view state file (where all the view state is saved to).

Essentially, with Scott's code, the last to write to the view state file is the one which that the view state file will be guaranteed to work in future post backs. With that said, the connection of the tampering has to do with the logic that the last post back may make the legitimism of the pages rendered earlier corrupt (from the latest page request in timeline).

A sharp reader out there might say - but you used the query string for different 'pages' (presentation of different user controls). Surely you can just vary each view state file by query string as well? To be honest, that was only an example that I presented… there are many other pages that do not vary their query string at all but the same page may want to persist two completely different view state values.

It is time for me to crack my knuckles for my solution that follows on the next page.

My Solution to the Problem

[Download Source Code]

Scott hinted in his article of what I proposed to him - to use global unique identifiers (commonly known as GUID) to differentiate between multiple instances of the page. For those who do not know of what a GUID is, it is a randomly drawn number that is 128 bits in length. It is almost guaranteed that any GUID that is computed by the computer will be not only unique to that machine but also guaranteed that it would be unique throughout the world.

Essentially, my solution follows the simple logic of:

  1. On every page request, a GUID will automatically generate.
  2. That GUID will then be used to name the view state file.
  3. For keeping records for the next post back, the GUID is saved to a hidden field because we want to keep reference to the view state (file) which was used for that request.
  4. When post back occurs, the GUID is read from the hidden field that was assigned in step three.
  5. From there on, a new GUID is then created and the cycle repeats itself from step one until the user does not post back by moving onto another page or closing the browser window.

With the logic explained in point form above; here is my version of my view state persister. The file name is called "PersistViewStateToFileSystem2.cs" in the downloadable source. Here is the code of my version of the PersistViewStateToFileSystem class with self-explanatory comments:

public class PersistViewStateToFileSystem : Page {
   // the extension is to protect users from sniffing in on view state via a simple
   // HTTP request
   private const string FilePathFormat = "~/PersistedViewState/{0}.vs.resource";
   private const string ViewStateHiddenFieldName = "__ViewStateGuid";</font>


<font face="Verdana" size="2">   // creates a new instance of a GUID for the current request
   private Guid pViewStateFilePath = Guid.NewGuid();</font>


<font face="Verdana" size="2">   /// <summary>
   /// The path for this page's view state information (GUID based).
   /// </summary>
   public string ViewStateFilePath {
      get {
         return MapPath(String.Format(FilePathFormat, pViewStateFilePath.ToString()));
      }
   }</font>


<font face="Verdana" size="2">   /// <summary>
   /// Saves the view state to the Web server file system.
   /// </summary>
   protected override void SavePageStateToPersistenceMedium(object viewState) {
      // serialize the view state into a base-64 encoded string
      LosFormatter los = new LosFormatter();
      StringWriter writer = new StringWriter();</font>


<font face="Verdana" size="2">      // save the view state to disk
      los.Serialize(writer, viewState);
      StreamWriter sw = File.CreateText(ViewStateFilePath);
      sw.Write(writer.ToString());
      sw.Close();</font>


<font face="Verdana" size="2">      // saves the view state GUID to a hidden field
      Page.RegisterHiddenField(ViewStateHiddenFieldName, ViewStateFilePath);
   }</font>


<font face="Verdana" size="2">   /// <summary>
   /// Loads the page's view state from the Web server's file system.
   /// </summary>
   protected override object LoadPageStateFromPersistenceMedium() {
      string vsString = Request.Form[ViewStateHiddenFieldName];</font>


<font face="Verdana" size="2">      if (!File.Exists(vsString))
         throw new Exception("The Viewstate file " + vsString + " is missing!!!");
      else {
         // instantiates the formatter and opens the file
         LosFormatter los = new LosFormatter();
         StreamReader sr = File.OpenText(vsString);
         string viewStateString = sr.ReadToEnd();</font>


<font face="Verdana" size="2">         // close file and deserialize the view state
         sr.Close();
         return los.Deserialize(viewStateString);
      }
   }
}

Other Problems That My Solution Combats

[Download Code]

We have successfully stopped a lot of other minor issues as well. All of them coincide with the main problem that Scott's code runs into. To list some minor issues that we have deterred:

  • Users can browse back and forth in their browser's history without worrying about concurrency issues. In example, browsing back three times pages back into the history and able to hit resubmit with the correct information / page display being rendered because all of the data will be on the same timeline. In other words, it behaves as if the view state was saved to a hidden field and resubmitted up via HTTP POST (as by default).
  • It allows your application to be dial-up friendly - just in case the connection is disrupted in some way (you can NEVER EVER predict them). Also, you never know when some ISP's will experience technical problems… in other words, if I had to put a code name to this feature, it would allow network latency to take place without any concurrency issues.
  • The user may have multiple browsers open for one page. A perfect example of the advantage is a "quote of the day" page or forums will do just as well.

Technically speaking, that is what Scott's code lacked - keeping track of concurrency. As soon as you do not keep record of each change to the view state, then you will run into concurrency problems from the web browser.

The reason being, in Scott's code, a user might want to go back one step in his browser's history. Because that page was a post back, the web browser would ask if it could do a post back. And because the user will want to see the page, it will post back. However, it will only post back the HTTP POST headers -- not the view state! The view state would compromise the values from the previous request that the user made (which the view state is newer compared to the HTTP POST headers). For that reason, that is why a GUID is used in a hidden field in my solution: ensuring that the same version of the view state is used in combination of the HTTP POST headers.

Scott's and My Problem

[Download Source Code]

There is one problem that both Scott's code and my code run into: garbage collection! Over time, your view state might take a hundred megabytes of disk space and you will have to devise a way on how to clean it up. There are a few solutions on how to schedule the clean-up of the view state files but most of them have one thing in common.

The one thing in common is the clean up code. The code is rather simple and I have commented the code as well in a self explanatory fashion:

public sealed class CleanUpViewStateFiles {
   private const int DaysToLeaveFiles = 2;</font>


<font face="Verdana" size="2">   /// <summary>
   /// Cleans up all the files that are found in a given directory.
   /// </summary>
   /// <param name="path">
   /// The absolute file path to the directory which contains the view state files.
   /// </param>
   public void CleanUp(string path) {
      TimeSpan timeSpan = new TimeSpan(DaysToLeaveFiles, 0, 0, 0);

      if (!path.EndsWith(@"\"))
         path += @"\";

      foreach (string filePath in Directory.GetFiles(path)) {
         FileInfo file = new FileInfo(path + filePath);

         // if the difference between now and the last access time is greater than the time span
         // delete the file.
         if (DateTime.Now.Subtract(file.LastAccessTime) >= timeSpan)
            file.Delete();
      }
   }</font>


<font face="Verdana" size="2">   private CleanUpViewStateFiles() {}
}

I think it is a good period of keeping view state files for two days. Busier sites might want to reduce the amount the amount of time that the view state file stays on disk because those disks can get filled rather quickly.

I mentioned that there are a couple solutions to the clean-up. One of them is to implement a simple windows service that runs the common code every thirty minutes. Obviously, most of us do not have that type of control over the server (web or file server).

The other solution is, which I prefer, is to run one thread in the background. I do have some code on some "practice/honing sites" that runs a thread in the background that does the odd site maintenance (like keeping the ASP.NET application compiled so there are no delays). For an example of how to implement the ASP.NET-keep-alive, you may read Paul Wilson's article. And while you are at it, you may integrate the clean-up code into the keep-alive.

I have one tip: do not use the Global.asax approach -- HttpModule's will work just as well. In fact, even better because you will not be restricted by single inheritance. A sample of an HttpModule has been included in the downloadable source code - it is located in the CleanUpHttpModule.cs file. To set up the clean up module, this is what  you configuration file should look something like this (I recommend that you put the clean up code and module in a seperate assembly):

<configuration>
   <system.web>
      <httpModules>
          <add name="ViewStateCleanUp"
             type="ViewStateToFS.CleanUpHttpModule, ViewStateToFS.dll"/>
      </httpModules>
    </system.web>
</configuration>

Conclusion

[Download Source Code]

In this article, I highlighted that Scott Mitchell's code may cause problems for a good percentage of users. I demonstrated the circumstances that Scott's solution will fail and explained why it happens (although the error message is also quite detailed).

I then showed you my solution to persist view state on the server's hard disk. In my solution, view state is controlled via a combination of hidden fields and global unique identifiers. I also mentioned the other small benefits that my solution has over Scott's... all of the benefits came from one aspect that my solution introduced: concurrency control.

As far as the credits go, I would have to give it back to Scott Mitchell. I think he deserves most of the credit due to his initial code; it inspired me to write this article. And as Scott loves to say:

Happy Programming!



User Comments

Title: Will fail for ASP.NET 2.0   
Name: TomP
Date: 12/11/2007 9:13:31 AM
Comment:
We used this for ASP.NET 2.0 and found that you will lose your control state. A good article exists here: http://blog.arctus.co.uk/articles/2007/04/23/advanced-asp-net-storing-viewstate-in-a-database
Title: Problem with Microsoft Ajax   
Name: AnupT
Date: 6/13/2007 12:17:38 AM
Comment:
Excellent article and i was able to successfully use this in a project.

But I ran in to problem in another project that uses Microsoft Ajax 1.0.

I have a page which uses updatepanel control. In this updatepanel, i have 2 dropdownlist with autopostback enabled. For some weird reason the second dropdownlist is not maintaing the selected value. As soon as i change the 2nd dropdownlist, its value changes back to first item in the list.

This problem is happening for all the page that has 2 or more dropdownlist.
Title: Some problems occurs during long sessions   
Name: Raul
Date: 1/16/2006 12:22:06 PM
Comment:
\
Title: Httpmodule problem   
Name: Ian
Date: 5/25/2005 12:47:33 PM
Comment:
Great article.
I did run into one problem with the HttpModule. I kept getting the error "Server operation is not available in this context". I think it was because the the httpmodule is created in the Application_OnStart and at that point the context does not contain a value for Server.MapPath. Anyway, I got around this by using System.AppDomain.CurrentDomain.BaseDirectory instead.

Good stuff though
Title: Cache   
Name: Wesley
Date: 8/7/2004 5:58:02 AM
Comment:
Justin,

I thought it was all about minimizing site traffic due to the sometimes very large viewstate. Just a little misunderstanding. But...

Your mathwork showed me somthing else as wel. 50 x 8 x 15 = 6000KB an hour! If we're talking about shared hosting most hosts give you a harddisk space of 50MB to 100MB which means that in the best case(not counting object allready there) we got 16.6 hours of viewstate left. So it's not very usefull for the average.

I would like to thank you both(scott and you) an awfull lot for explaning viewstate in such an extended way. I managed to solve my viewstate problems on the composite control mentioned in the url.

Cheers,

Wes
Title: Postback problem still exists   
Name: Justin Lovell
Date: 8/7/2004 5:57:14 AM
Comment:
Richard,

As I have already said to, the problem that you are experiencing is due to the fact that my HTML code that I posted is directing you to Scott's example page.

I have upload the new source code with the fixed HTML
Title: re: Cache   
Name: Justin Lovell
Date: 8/6/2004 5:43:53 PM
Comment:
Hi Wesley,

You can use the cache object to store the view state. However, it totally depends on what type of site you are running. The main issue that was addressed by persisting the view state to the file system is that an user can fill a form and then leave it to stand over night before he submits his changes.

If you are using the cache object, you will then have to counter that time-restriction by placing the cache time to a couple of hours and sometimes as long as a day. If your site gets an average of 50 users who all visit eight pages on average, and assuming that the average view state size is 15KB, then do the maths: 50 x 8 x 15 = 6000KB of memory (~6MB). That is a low traffic site... now let's multiply the amount of sites doing exactly that (shared hosting for low traffic sites) to about 40: that is ~240MG just for just holding view state into memory.

That is a bit inefficient. However, it becomes even more dramatized if you have to run off medium trafic site. And to make it worst: you can't scale your application to a web farm.

It is all about keeping your options open... and your memory for better use :-)
Title: Cache   
Name: Wesley
Date: 8/6/2004 2:57:28 PM
Comment:
As I am quit new on programming in .Net I tend to read a lot just to learn. So don't blame me if this is a stupid idea

One thing I truly don't understand: Why not cache the viewstate by the name of the SessionID+Page and the time a session exists? Something like: cache.insert(SessionId+Page, Viewstate,,DateTime.Now.AddMinutes(SessionTTL))

In that case your sure to get the viewstate corresponding to the current session, it auto cleans up the mess and it's a lot quicker I guess... You just have to make sure the cache isn't cleared.

Cheers,
Wes
Title: Postback problem still exists   
Name: Richard Lemmon
Date: 8/6/2004 2:41:38 PM
Comment:
I followed your article and was able to duplicate the same error that was being caused by Scott's code.

Try the following:

1.) http://localhost/ViewStateToFS/MySolution.aspx
2.) Click on the "Show User Control A"
3.) Select Hello world
4.) right-click on "Show user Control B" and select Open in new window
5.) In the new window click Postback
6.) Return to the initial window & click Postback

same error.

Regards,

Rich Lemmon
rich@lindenstreet.com
Title: A couple of very small points on an otherwise excellent article   
Name: Scott Galloway
Date: 8/6/2004 11:37:13 AM
Comment:
Great article, I do have a couple of small points though - firstly, as written your code writes the filepath into the HTML source of the generated page; this is not really a great idea as path disclosure is generally to be avoided in web apps. I made a simple modification to allow just the GUID portion to be stored in the hidden field - which is also smaller :-). Second point is to do with the HttpModule you use for cleanup, I tend to use a different approach here, using a Timer object stored as a static object - it has the nice side effect of avoiding using a separate module but it does have the disadvantage of requiring a code change.

Product Spotlight
Product Spotlight 





Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2014 ASPAlliance.com  |  Page Processed at 9/2/2014 7:42:37 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search