![]() |
Web Development »
ASP.NET »
Howto
Intermediate
License: The Microsoft Public License (Ms-PL)
Efficient Server-Side View State PersistenceBy datacopIncreasing the performance of your ASP.NET website by reducing the download footprint of your pages. |
C# (C#2.0, C#3.0), XML, ASP.NET, Architect, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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:
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.
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
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;
// create a unique cache file and key based on this user's
// session and page instance (time)
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());
}
// get our vs key from the page, re use it, and the cache
// file (pulled from page.cache)
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;
// 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;
}
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 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>
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.
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.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 8 Aug 2008 Editor: Sean Ewington |
Copyright 2008 by datacop Everything else Copyright © CodeProject, 1999-2010 Web17 | Advertise on the Code Project |