ASP.NET provides a couple of page-level state persistence mechanisms in ViewState and the new ControlState.
While both mechanisms work, they both have some limitations in that they are not deterministic for the application developer-ViewState can be turned off and can be very bulky, and ControlState can only be set from within a control implementation. In this article I’ll show another, more flexible state mechanism using a PreservePropertyControl that allows automatic persistence and restoration of field values automatically without requiring ViewState.
ASP.NET 1.x provides ViewState and ASP.NET 2.0 adds ControlState. Both mechanisms provide the ability to persist page-specific state across a single page postback by embedding the state data into the page as a hidden form variable called __VIEWSTATE. Both work for specific scenarios but I also think that both have serious limitations that make them inappropriate in some scenarios and more difficult to use than they have to be.
ViewState is an all or nothing mechanism. Once enabled it tracks all changes to controls.
Wouldn’t it be nice to have a built-in mechanism to declaratively store a property value of a control or the page and have it automatically restored when the page is posted back?
Sounds a lot like ViewState, right? But ViewState is not declarative and not easily controlled. If you turn off ViewState, as I do whenever I can, you immediately loose the ability to track any values in ViewState at all. Controls stop tracking, and you can no longer use the ViewState collection to store values from your application code. Turn ViewState on and you end up picking up all changed values of all controls that have changed non-postback values and have ViewState enabled. In other words it’s an all or nothing approach when often times you only want to persist one or two values of a page.
I ran into this a couple of days ago when I was dealing with a complex DataGrid. The DataGrid is set up with ViewState off and there are many actions, like Delete, Update Status, etc., that fire postbacks from each row. There’s also paging and sorting. Without ViewState it gets real messy trying to get the DataGrid to keep track of the CurrentPageIndex properly.
Wouldn’t it be nice if you could do something like this?
this.gdGrid.PreserveProperty("CurrentPageIndex")
And you’re done? You don’t need to use ViewState. You can choose to have a single value persisted instead of all the DataGrid data. The same would go for persisting properties of any other control like the ForeColor of a button:
this.btnSubmit.PreserveProperty("ForeColor")
This would work without ViewState enabled and you’d stick this code at the beginning of Page_Load() or other code that runs on any request. ASP.NET would then automatically write out the value at the end of the request and restore it when the page loads again on a postback. Now that’s a page-level state mechanism that’s easy to use and flexible.
Controls could internally use the same mechanism. Controls could persist their state to this same statebag deterministically similar to the way ControlState does in ASP.NET 2.0. ControlState is new in ASP.NET 2.0 and I’ll talk more about it later in this article.
Unfortunately, this approach of using a method on the Control class assumes a change to the Control class by adding a PreserveProperty method, which would then be available on all controls automatically. Changes to the Control class is only an option if you are Microsoft, as you can’t extend Control and have other stock controls inherit from a modified Control class.
The next best solution is to build an external control that you can drop onto a page or add to a control to provide this same functionality. In this article, I’ll show you how to build such an extender control.
Introducing the PreservePropertyControl
The PreservePropertyControl is an ASP.NET 2.0 custom server control. The downloadable code for this article also contains an ASP.NET 1.1 version of the source courtesy of Peter Bromberg who ported it back to 1.1. I used ASP.NET 2.0 mainly to take advantage of generics for the persisted properties rather than implementing a custom collection. .NET 2.0 makes it a cinch to create strongly typed collections with generics. You can then use the designer to edit these collections using the default Collection editor. Another 2.0 feature, ControlState, provides the default storage mechanism for the PreserveProperty control. ControlState makes it easy to persist an internal structure without having to worry about encoding the structure for embedding into the page.
ControlState is accessible only inside of a control and not on the Page level.
I implemented the control as a server control that can be defined on the page declaratively and has designer support. For example you can declare the control like this:
<ww:PreservePropertyControl ID="Persister"
runat="server">
<PreservedProperties>
<ww:PreservedProperty
ID="PreservedProperty1" runat="server"
ControlId="btnSubmit"
Property="ForeColor" />
<ww:PreservedProperty
ID="PreservedProperty2" runat="server"
ControlId="__Page"
Property="CustomerPk" />
</PreservedProperties>
</ww:PreservePropertyControl>
You can do this with script markup inside of your ASP.NET page or use the designer and the default collection editor to enter the preserved properties with Visual Studio. To persist a value, you specify the control’s UniqueID and the property or field name to persist. You can persist control values as well as values on the Page object as shown with the __Page ControlId above.
Of course, you can also use code to accomplish the same thing.
protected PreservePropertyControl Persister=null;
protected void Page_Load(object sender,
EventArgs e)
{
this.Persister=new PreservePropertyControl();
this.Persister.ID = "Persister";
this.Controls.Add(Persister);
this.Persister.PreserveProperty(
this.btnSubmit, "ForeColor");
this.Persister.PreserveProperty(
this, "CustomerPk");
}
When using code, it’s more efficient to pass an actual control reference rather than a string ID-the control caches the control reference and uses it later in the page cycle to write the values into its storage container.
Note that you get a lot of flexibility with this mechanism and you can basically store anything in this state container as long as you can reference it through a control instance. Because the Page object is also a control you can attach a Protected property for anything you want persisted. It’s quite convenient, for example, to add a Pk reference to a page so you can keep track of your current edit context without requiring ViewState or a hidden variable.
You can even store entire objects. Like ViewState, you can store any serializable object in the PreservePropertyControl. The nice thing about this approach is you set up the PreserveProperty() call once and after that you can just reference the property. So if you persist the CustomerPk as shown above you can simply reference the CustomerPk property on the page and always get the correct value back without having to explicitly access a state mechanism. You can just say this.CustomerPk and the value will be up to date!
This is even easier than ViewState because it eliminates the need to talk to the state container at all. You don’t need to access a statebag, double-check for null values, etc. The control manages that for you.
Control developers can also use the control internally to map their own properties by using a private copy of the control.
public class CustomControl : Control
{
PreservePropertyControl Persister = null;
protected string value = null;
protected override void OnLoad(EventArgs e)
{
this.Persister =
new PreservePropertyControl();
this.Persister.ID = "__" + this.ID;
this.Persister.StorageMode =
PropertyStorageModes.HiddenVariable;
this.Persister.PreserveProperty(
this,"value");
this.Controls.Add(this.Persister);
base.OnLoad(e);
}
}
Once this is in place the control developer doesn’t have to keep persisting values into any special store like ViewState["value"] or a custom container, but can simply reference the property as usual, all without ViewState being active.
You can check out the control and a small sample page that demonstrates the functionality from the source code provided with this article from here:
www.west-wind.com/files/tools/PreservePropertyControl.zip
The .zip file contains both the 2.0 and 1.1 versions.
How it Works
The implementation of the control is pretty simple. It has a PreservedProperties collection that holds the ControlID (or instance if available) and the property name. When you call the PreserveProperty() on the control or declaratively define the control on a page, each ControlId and Property name is stored in the collection. The collection is a Generic List<PreservedProperty>. I love generics for this stuff-no more creating custom collections for child controls!
To get the collection to work as a designable collection you have to use a few custom attributes on the Control class as well as the collection property. You also need to implement AddParsedSubObject (or use the DefaultProperty attribute if you only have a single collection) to get the child control values to be added as PreservedProperty objects from the declarative script definition. Listing 1 shows the class and collection definition.
The code deals with storing the PreservedProperties in a simple strongly-typed List collection. The control uses a custom class to hold the control’s ID, an instance reference if available, and the ID for the control. The PreservedProperties collection is a temporary holding container for the property identification and the values are not actually stored in it until later in the request cycle.
Encoding and Decoding the Persisted Data
The actual hooks for encoding and decoding depend on the StorageMode which includes ControlState, HiddenVariable, SessionVariable and CachePerPage. All but ControlState require explicit hooks to fire for the encoding and decoding to occur. In these modes, hook calls are made in OnInit() and OnPreRender() of the control. OnInit() fires very early in the page cycle so that the preserved value can be retrieved prior to any other state retrieval. Specifically, the property value is set before ViewState assigns its value or before a postback value is assigned. This makes preserved properties have the lowest priority in value assignment, which is the intended behavior.
The PreserveProperty control lets you work with properties and fields without having to worry about the persistence mechanism.
When the value is stored it’s done in the As part of the OnPreRender() of the PreservePropertyControl.
ControlState Persistance in ASP.NET 2.0
ControlState is a new feature of ASP.NET 2.0. It’s meant as a control internal state implementation used to store essential data between page postbacks. It uses ViewState for storage, but unlike stock ViewState it is not affected by the setting of EnableViewState and always writes out its values.
You can easily work with ControlState by implementing two methods on the Page class: SaveControlState and LoadControlState. These methods work by returning and retrieving an object value that ASP.NET internally persists. These objects must either be simple types, serializable or implement a TypeConverter. As with ViewState, the rules are determined through the .NET LosFormatter class which is an optimized serializer ASP.NET uses for encoding ViewState strings. You can find ControlState in the System.Web.UI namespace.
In order to enable ControlState you have to tell the Page to use it with:
this.Page.RegisterRequiresControlState(this);
which is typically called in the OnInit() of the control.
You then override SaveControlState() to return an object that contains the state. If you have more than one value to persist, a typical object is a Hashtable with all the persisted values in it. When the page posts back, LoadControlState is called with an object parameter that essentially restores that object saved in SaveControlState. It’s a very easy mechanism that deals with all the encoding details. All you do is return an object and off you go. Listing 2 shows the SaveControlState() and LoadControlState() methods for managing ControlState.
SaveControlState() walks through it’s PreservedProperties collection and starts by finding each control instance. If you used code to add properties, then you likely passed in a control instance reference, but when using declarative script a string reference is stored, and you’ll need to use FindControl to actually locate the control. Once a control reference is available the value for the property is retrieved with Reflection. Note the flags to retrieve both Fields and Properties and both public and non-public members. Also note that you cannot preserve Private members on a Page object.
The next step is to add the value to a Hashtable with the key being a concatenation of the unique ControlID and the property name. Hashtables are efficient at storing key value pairs and persist in a lightweight fashion. The SaveControlState method then returns this Hashtable for persistence.
Loading the data on a postback works this process in reverse. LoadControlState() receives the Hashtable as an input and then runs through the collection, retrieving each key and value. The code in LoadControlState() splits the key back into its ControlID and property name components and FindControl is then used to get an instance, followed by Reflection to set the control’s value.
This is a very straightforward process and using ControlState takes very little code to implement this solution.
Other Persistence Mechanisms
ControlState works fine and is probably the most reliable way to store the persisted data, but if you’re using ASP.NET 1.1 or if you don’t like ViewState persistence (even if permanent) you might want to use a different storage mechanism. The PreservePropertyControl actually supports four persistence modes:
- ControlState
- HiddenVariable
- SessionVariable
- CachePerPage
All but ControlState require custom hooks into the page pipeline and a little extra work to serialize the data. When I created the original control for this I used my own serialization mechanism which was less efficient and required quite a bit more code. After a few suggestions from my blog someone suggested that I use the LosFormatter. The LosFormatter is a little known string serializer in the System.Web.UI namespace that you can use to serialize objects. The format used by this serializer is more light-weight than the full binary formatter as it encodes common types more efficiently, often bypassing the expensive full .NET serialization mechanism. Listing 3 shows the relevant code from the control that demonstrates the non-ControlState storage mechanisms.
For all but the ControlState persistence, both OnInit() and OnPreRender() methods are hooked. OnInit() calls LoadStateFromLosStorage() and OnPreRender() calls SaveStateToLosStorage(). These methods delegate the actual Hashtable generation and parsing to SaveControlState() and LoadControlState() shown above. SaveStateToLosStorage() then calls the LosFormatter to create a string representation of this Hashtable. Each of the different storage mechanisms then stores this string in the appropriate storage mechanism.
With HiddenFormVariable the data is stored like this:
this.Page.ClientScript.RegisterHiddenField(
"__" + this.UniqueID, Serialized);
and read with:
RawBuffer = HttpContext.Current.Request.Form[
"__" + this.UniqueID];
Once the raw buffer is retrieved it’s deserialized back into a Hashtable and then passed to LoadControlState() which reassigns the values to the controls.
Session variable storage is a little different and requires a little explanation. Session storage stores the preserved property state in a Session variable that is reused for all pages. This means every page gets the same Session variable instance and there’s one variable per user.
Using Session can be a great boon because if you store the persisted data in the Session object, you’re not shipping it over the wire, which means smaller pages. Session storage also tends to be much faster than ViewState encoding of any kind if using InProc storage because no serialization occurs.
You’ll want to use caution with this approach! If you open two windows in the same browser session or use Frames pages that have the same Session active simultaneously, you’ll run into problems with getting the wrong state restored. In the included demo page you can test this by running the page, setting a color, and submitting then pressing Ctrl-N (or Ctrl-T if you have tabs) to create a new browser window in the same browser session. Open the same page, select a different color, and click Show. Now go back to the first page and click the postback button-you should see the color from the second instance in the first instance which is clearly incorrect. The first instance picked up the persisted data of the second.
This might not be a problem in some internal applications where there’s no reason to run multiple browser windows or frames, but even so, be very careful with this option. You could solve this problem by generating a unique per-page ID for a Session variable, but this would then potentially clutter up Session state with a lot of persisted memory. Session variable storage should only be considered if you work on pages that have a large amount of persisted data and want to avoid sending that data over the wire for each postback.
Similarly, you can use the Cache object. Unlike the Session approach, the Cache approach writes to the session assigning a new GUID for each page. So every new page for every user creates a new cache entry.
As with Session, be aware of the trade offs: Using the Cache object you will generate a large number of cache entries and on a really busy site with many pages. I think this approach is probably not realistic. Also note that the Cache approach will not work in Web farm environments as Cache doesn’t persist across machines.
Both Cache and Session offer more light-weight pages and better performance for state storage though, so you might want to experiment with the options.
Both ControlState and HiddenFormVariable approaches are stored in the page itself as POST data so they don’t suffer from these issues. Out of the two, ControlState is the more reliable solution simply because ControlState has its own events that fire as part of the page pipeline and don’t require hooking existing events where timing issues with other controls can potentially occur. I provided a HiddenFormVariable implementation primarily to support ASP.NET 1.x.
Preserve the World
Using this control in recent applications I’ve found a lot of uses for it where I had previously not even considered state storage. When you can control exactly what gets persisted it’s easy to keep persisted state down and still get a flexible page that provides true property access to the persisted values. I tend to run my pages with ViewState off altogether and so having a control that can persist a few items declaratively can be very helpful. All in all this control makes life easier in many ways.
The most common scenarios for me are managing List state settings like SelectedValue properties from list controls, paging, and sorting settings in grids and lists. Storing page-level state like a current record Id also works very well. The beauty is that it becomes much more natural to interact with any preserved properties because you simply reference the properties without any concern for the persistence mechanism-the process is totally transparent once you’ve added it to the PreservePropertyControl. The control basically lets you turn ASP.NET’s promiscuous ViewState policy upside down: Instead of storing everything in ViewState automatically you only store what you explicitly need to store. This can drastically reduce the amount of ViewState in your pages. Check it out and see if you can’t trim your page size considerably using this control.
I hope you’ll find this control useful and that this article has given you a few insights into the various page-level state persistence mechanisms. Go ahead-do your thing for the environment and preserve… in real life and now in your Web code!
If you have any comments, questions or suggestions regarding this article, you can post them at www.west-wind.com/wwthreads/?default.asp?forum=White+Papers.