Introduction
View State is a mechanism employed by ASP.NET web pages to persist the state of the page itself, individual controls, objects, and data that are housed on that particular ASP.NET web page. View State is a double edged sword in that using it properly allows the developer to build full and robust web applications that seem to overcome the stateless nature of the web.
As the complexity of our web applications grow, more and more controls get added to the screen, more and more data needs to be persisted... View State grows. There's no way to avoid it. Even if we are diligent in our efforts in only making sure that those things that need View State are the only ones with it turned on, View State can grow to several kilobytes in size. It's not uncommon to have View State sizes that run into the 50K to 100K range on custom intranet/extranet applications.
Background
Scott Mitchell wrote an excellent article on View State some years ago that was published on MSDN. I won't go into the details on View State that he does (since he's already done an excellent job), but I will mention a few things:
- To much of a good thing... By default, every control that you add to an ASP.NET web page has View State enabled. Every button, label, grid, drop down.. everything. That means, every control on your page makes the size of the View State grow.
- View State is persisted in the HTML markup. That's right boys and girls. Every control on your page that makes use of View State makes that View State bigger.. makes your overall page size bigger as well.
- View State travels both ways. Since View State is used by ASP.NET, and ASP.NET only exists on the server, the View State that was sent down to the browser has to be sent back to the server for it to be used. Yes, that includes partial page updates with AJAX as well.
- View State is ONLY used on the server. I know I said this in #3, but it bears repeating here... the only place that View State is ever used is on the server, by the ASP.NET runtime...
Requirements
- The View State needs to be persisted on the server.
- The View State persistence mechanism needs to be identified by a specific user session.
- The persisted View State artifact must not be allowed to remain forever.
- The persisted View State should be able to be enabled and disabled on a page by page bases.
- Different persistence mechanisms should be able to be used.
- Page development and structure should not be modified.
Control Adapters to the Rescue
Since the ASP.NET Page
object, at its core, is a control (granted.. it is *the* control.. but a control nonetheless), we're able to modify its behavior with a simple control adapter. Typically, when speaking about control adapters, the development community at large thinks about CSS Control Adapters.
MSDN defines control adapters thusly: Control adapters are components that override certain Control
class methods and events in its execution lifecycle to allow browser or markup-specific handling. The .NET Framework maps a single derived control adapter to a Control
object for each client request.
I discovered this rather excellent article by Robert Boedigheimer on using Server Side View State in ASP.NET using the SessionPageStatePersister
. I knew then that this was the road I wanted to go down... although using the SessionPageStatePersister
would run into the same problems with finite memory resources.. so that as a brush stroke solution wouldn't do...
My solution actually contains two parts. The first part is the control adapter PageStateAdapter
, and the second is the custom persistence mechanism CachePageStatePersister
.
PageStateAdapter
To solve my requirements #4 and #5, I decided to go with a simple attribute scheme, where basically, the developer can decide to use a different View State persistence scheme simply by putting an attribute at the top of the page's class definition. The attribute class and supporting enum are both defined in the PageStateAdapter
class.
public enum StateStorageTypes { Default, Cache, Session, InPage }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class PageViewStateStorageAttribute : Attribute
{
private readonly StateStorageTypes storageType = StateStorageTypes.Default;
public PageViewStateStorageAttribute(StateStorageTypes stateStorageType)
{
storageType = stateStorageType;
}
internal StateStorageTypes StorageType
{
get { return storageType; }
}
}
Now, all the PageStateAdapter
has to do is implement the virtual method GetStatePersister
.
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;
default:
psp = new CachePageStatePersister(Page);
break;
}
return psp;
}
If a developer wishes to override the (now) default View State persistence method of CachePageStatePersister
, they can do so by applying a simple attribute to the page class declaration.
[PageStateAdapter.PageViewStateStorage(PageStateAdapter.StateStorageTypes.InPage)]
public partial class ViewStateInPage : System.Web.UI.Page
CachePageStatePersister
The CachePageStatePersister
inherits from PageStatePersister
. So, all it has to do is implement the two virtual methods Load()
and Save()
.
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)
{
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
{
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);
}
}
In our Save
method, you can see we're doing a number of things.. first is generating a unique View State key (if this is a new page request) based on the page that's being requested, the user's session ID and the time represented in ticks. Then, we're serializing both the View State and the control state to a physical file (in this case, we're storing the file in the ~/App_Data/Cache directory). After the file is created, we store the View State key and the path to the file in the Page.Cache
, and save the View State key to a hidden field in the page.
If the page is requested from a POST
verb, then we know that we've already got a unique key for this page instance. We extract that key from the page, and the file path from the cache and re use those settings to persist the view state and control state
So, we are mindful of finite server resources by only storing the file path in cache, we are mindful of download page size by only storing the unique key in the page (instead of the entire View State), and by using the Page.Cache
object to tie everything together, we're giving our persisted View State files a life time. Notice on the last bit of the Page.Cache.Add()
method, we're defining ViewStateCacheRemoveCallback
as our callback method when an item is removed from cache.
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);
}
When the cache makes the callback, it passes on what that cache object contained, which we know is the file path to the persisted View State object. All we have to do when we receive the callback is to delete the physical file.
The Load
method basically works in reverse of the Save
method..
public override void Load()
{
if (!Page.IsPostBack) return;
string vsKey = Page.Request.Form[VSKEY];
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;
}
Some points of interest here are... if the page is not working as a post back, we don't want to load our View State from persistence. This solves the problem of a user loading stale data. The Load()
method gets its View State key from the page, then gets the path to the persisted View State file from the Page.Cache
using that View State key. The file is then read, de-serialized into a Pair
object, then ViewState
and ControlState
are loaded from the Pair
object.
The Browsers File
The last point of order is to wire up the PageStateAdapter
for use with a simple .browser file located in App_Browsers:
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.Page" adapterType="PageStateAdapter" />
</controlAdapters>
</browser>
</browsers>
Conclusion
View State, while a powerful tool, can very quickly become the demise of your users in experiencing your website to the fullest. By making use of built-in ASP.NET technologies like Cache and Control Adapters, we can effectively and accurately persist View State on the server instead of streaming it down to the user. Using the outlined PageStateAdapter
has enabled our developers to make full use of our existing ASP.NET skill set, including our heavy use of ASP.NET AJAX, without having to re-design our entire page framework; it is truly a drop in solution. Our PageStateAdapter
method could be very easily modified to persist View State files to a common location for a multi-homed web environment, and the Page.Cache
replaced with a common mechanism in that same environment. We've been using this technique in our training environment under both real world load and extreme test load, with very impressive results.
In our code base, we've also added to the global Application_Start
and Application_End
events to delete all the .cache files that might be present. This takes care of any artifacts that might be around when the server reboots, or that might have been missed in the cache remove callback.
Points of Interest
When we first started down the path of persisting View State on the server, we were stuffing the entire thing into Page.Cache
. It worked fine on our workstations and our development server. It even worked find on our training server.. until that training server went under load. 100 users and three hours later, the sever came crashing down, falling flat on its face after running out of memory. Point of Interest: Page.Cache
.. is in memory.
History