A few days ago I posted a note about a feature I’d like to see in ASP.NET: Control.PreserveProperty() which would allow the ability to tell ASP.NET to actively manage properties that I specify by storing and retrieving their content automatically.
Sounds like ViewState right? But ViewState is not declarative and not easily controlled. If you turn off ViewState (like I do whenever I can) you immediately loose the ability to track any values at all in ViewState. Turn it on and you end up picking up all ViewState of all controls that don’t have ViewState explicitly off. In other words it’s an all or nothing approach.
I ran into this a couple of days ago, when I was dealing with a somewhat complex DataGrid that has lots of actions firing off it. The DataGrid is set up with ViewState off and there are many actions, like Delete, set flags etc. that are accessible as options on each row. And there’s Paging. But without ViewState it gets real messy trying to get the DataGrid keep track of the CurrentPageIndex properly. Wouldn’t it be nice if you could do something like this:
this.gdInventoryList.PreserveProperty("CurrentPageIndex");
and be done with it. No ViewState, just a single value that you know of persisted. Same for something like say a color you persist on a button or label:
this.lblMessage.PreserveProperty("ForeColor")
Rather than having ViewState track EVERYTHING on the page that you really don’t care about you tell the Page or the control what you do care about and let ASP.NET persist and restore the property. Unfortunately it’s not easy to do this on the control level because ASP.NET provides the Control base class and without multiple inheritance support in C#/VB there’s no way to hook in this functionality (actually C# 3.0 will allow extender methods that might provide the ability to extend a type, but it looks like it will be limited within an assembly). Currently the only way to implement this at the control level is to implement that functionality on subclassed control for every control. Yuk.
Bertrand LeRoy mentioned that you can easily build a separate control for this, and so I spent some time today putting it together. Still – it would be a wonderful addition to ASP.NET at the control level to make it much easier for applications and even more so for Control Developers to persist values more easily without having to worry about various StateBag containers and the somewhat messy code that goes along with checking for the existence of values before reading them etc. This is what ControlState should have been.
Since it’s the holidays and I have some spare time (or I don’t have a life, whichever way you want to look at it <g>) I spent a few hours today building a control provides the PreserveProperty functionality. You can download the control with source code from:
http://www.west-wind.com/files/tools/PreservePropertyControl.zip
How to use the control
The PreservePropertyControl is an ASP.NET 2.0 custom server control but you can backfit it fairly easily to 1.1. The main 2.0 feature used is Generics for the Property collection and you’d have to implement the collection yourself by inheriting from CollectionBase in order to get the designer support.
As Bertrand suggested nicely, I implemented it as a server control that can be defined on the page declaratively. For example you can do something 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>
This is nice because you can drop the control on a page and use the designer to add the PreservedProperty items – the designer will pop up the default collection editor for entering the proeprties to preserve. Then again there’s little need to have anything on the design surface so you can also do the same thing in code like this:
protected PreservePropertyControl Persister = null;
protected void Page_Load(object sender, EventArgs e)
{
Persister = new PreservePropertyControl();
Persister.ID = "Persister";
this.Controls.Add(Persister);
this.Persister.PreserveProperty(this.btnSubmit, "ForeColor");
this.Persister.PreserveProperty(this, "CustomerPk");
this.Persister.PreserveProperty(this.dgInventoryList,"CurrentPageIndex");
}
You can also store things like objects, so this can potentially replace ViewState as long as you have a property on either a control or the page. The nice thing about this approach is you set up the PreserveProperty call once and after that you can reference the property and always have it up to date across same page posts just like ViewState, but with ViewState actually off. In addition, you can persist things that wouldn’t auto-persist with ViewState, like properties on the Page itself. I actually do this frequently for things like IDs that identify the record I’m editing for example or to signify a certain state page (adding, editing etc.).
You should note though that this persistence mechanism is a little more verbose than ViewState. While you can persist much less data than auto-ViewState does in most cases, comparable persist values are going to be larger in this implementation because this control must store the control Id and property along with the value. How light the impact is depends on how much you persist – the big advantage is that you get to control exactly what gets persisted.
How it works
The implementation of the control is pretty simple. It has a PreservedProperties collection that holds the Control ID (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 control Id 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! Yay…
To get the collection to work as 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 to get the child control values to be added as PreservedProperty objects from the declarative script definition.
[ParseChildren(true)]
[PersistChildren(false)]
public class PreservePropertyControl : Control
{
/// <summary>
/// Collection of all the preserved properties that are to
/// be preserved/restored. Collection hold, ControlId, Property
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public List<PreservedProperty> PreservedProperties
{
get
{
return _PreservedProperties;
}
}
List<PreservedProperty> _PreservedProperties = new List<PreservedProperty>();
/// <summary>
/// Required to be able to properly PreservedProperty Collection
/// </summary>
/// <param name="obj"></param>
protected override void AddParsedSubObject(object obj)
{
if (obj is PreservedProperty)
this.PreservedProperties.Add( obj as PreservedProperty );
}
… more class stuff
}
Most of the work in the class is handled by a couple of methods called RenderOutput() and RestoreOutput. RenderOutput() basically generates the encode text to be persisted and then embeds a hidden form var. RestoreOutput() takes the output generated, reads the encoded text and then parses it back into the control properties. These methods are then hooked up to OnPreRender() and OnInit() respectively to fire automatically as part of the page cycle.
Creating the encoded output
RenderOutput() essentially runs through the collection of PreservedProperties that were added during the course of page Execution – usually in the Page_Load or as part of the declarative script. It reads each property value and tries to persist it into a string. The string is then encoded in a very simplistic format (for the moment). I suppose there are better ways to do this but I wanted to keep things simple at least in this first rev so each property is encoded like this:
`|ControlId~|PropertyName~|StringValue|`
The entire collection is then strung together and finally base64 encoded. For simple values this is straight forward - the value is simply turned into a string and stored. For complex types, the value is serialized via Binary Serialization into a byte[] array and converted to an ASCII string that gets embedded instead.
Here’s what the encoding looks like:
/// <summary>
/// Creates the Hidden Text Field that contains the encoded PreserveProperty
/// state for all the preserved properties and generates it into the page
/// as a hidden form variable ( Name of control prefixed with __)
/// </summary>
/// <exception>
/// Fires an ApplicationException if the property cannot be persisted.
/// </exception>
public void RenderOutput()
{
StringBuilder sb = new StringBuilder();
foreach(PreservedProperty Property in this.PreservedProperties)
{
Control Ctl = Property.ControlInstance;
if (Ctl == null)
{
Ctl = this.Page.FindControl(Property.ControlId);
if (Ctl == null)
continue;
}
// *** Try to retrieve the property or field
object Value = null;
try
{
Value = Ctl.GetType().InvokeMember(Property.Property,
BindingFlags.GetField | BindingFlags.GetProperty |
BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.IgnoreCase, null, Ctl, null);
}
catch
{
throw new ApplicationException("PreserveProperty() couldn't read property " +
Property.ControlId + " " + Property.Property);
}
Type ValueType = Value.GetType();
// *** Helper manages the conversion to string
// *** Simple type or ISerializable on - otherwise ApplicationException is thrown
string Return = this.TypedValueToString(Value,CultureInfo.InvariantCulture);
// *** If we made it here and we're null the value is not set
if (Return == null)
continue;
sb.Append("`|" + Property.ControlId + "~|" +
Property.Property + "~|" + Return + "|`");
}
string FullOutput = sb.ToString();
if (this.Base64Encode)
FullOutput = Convert.ToBase64String( Encoding.UTF8.GetBytes(FullOutput) );
this.Page.ClientScript.RegisterHiddenField("__" + this.UniqueID,FullOutput);
}
/// <summary>
/// Converts a type to string if possible. This method supports an optional culture generically on any value.
/// It calls the ToString() method on common types and uses a type converter on all other objects
/// if available
/// </summary>
/// <param name="RawValue">The Value or Object to convert to a string</param>
/// <param name="Culture">Culture for numeric and DateTime values</param>
/// <returns>string</returns>
public string TypedValueToString(object RawValue, CultureInfo Culture)
{
Type ValueType = RawValue.GetType();
string Return = null;
if (ValueType == typeof(string))
Return = RawValue.ToString();
else if (ValueType == typeof(int) || ValueType == typeof(decimal) ||
ValueType == typeof(double) || ValueType == typeof(float))
Return = string.Format(Culture.NumberFormat, "{0}", RawValue);
else if (ValueType == typeof(DateTime))
Return = string.Format(Culture.DateTimeFormat, "{0}", RawValue);
else if (ValueType == typeof(bool))
Return = RawValue.ToString();
else if (ValueType == typeof(byte))
Return = RawValue.ToString();
else if (ValueType.IsEnum)
Return = RawValue.ToString();
else
{
if (ValueType.IsSerializable)
{
byte[] BufferResult = null;
if (!wwUtils.SerializeObject(RawValue, out BufferResult))
return "";
Return = Encoding.ASCII.GetString(BufferResult);
//Return = Convert.ToBase64String(BufferResult);
}
else
{
throw new ApplicationException("Can't preserve property of type " +
ValueType.Name);
//TypeConverter converter = TypeDescriptor.GetConverter(ValueType);
//if (converter != null && converter.CanConvertTo(typeof(string)))
// Return = converter.ConvertToString(null, Culture, RawValue);
//else
// return null;
}
}
return Return;
}
The code uses Reflection to retrieve the control’s property value dynamically based on the string representation stored in the PreservedProperty item. Once the value is retrieved, it’s converted to a string using the TypedValueToString() helper method.
You can see that the code checks for simple types and then for ISerializable and if that doesn’t work it throws an exception back. This is similar to ViewState. Like ViewState, the control persists simple values and anything that supports ISerializable. I played around a bit with trying to check for type converters first, since TypeConverter output tends to be much less verbose and more efficient than serialization. But unfortunately many things output incomplete TypeConverter data. For example, the .Style or .Font properties have type converters but they don’t actually work for two way persistence (in fact the style converter only appears to do a .ToString() which is useless). So for now I used it’s simple types and ISerializable – it works well, but is not optimal for performance or encoded size.
If anybody has any ideas on how to get better type parsing support I’d love to hear about it. But it looks ViewState has these same limitations and ViewState too persists using Serialization. This control’s encoded output tends to be more verbose than the same values persisted to ViewState primarily because it persists to string (other than the serialized objects) and it has to store the control and property names.
Anyway, RenderOutput is self contained – it takes the generated string of all the properties and writes a hidden form var out to the page:
this.Page.ClientScript.RegisterHiddenField("__" + this.UniqueID,FullOutput);
This code is hooked up to the OnPreRender() method:
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (this.Enabled)
this.RenderOutput();
}
OnPreRender happens late in the cycle just before the page actually renders to output. I want to do this as late as possible to make sure the last known value is persisted.
Restoring the property values
Once the value’s in the page it gets rendered into the page when it displays. Now when the page is posted back again – after the user clicks a button or other dynamic Postback link – this encoded data gets posted back to the server.
On the inbound end the control hooks into OnInit() to read and assign the value.
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (this.Enabled)
this.RestoreOutput();
}
OnInit is very early in the cycle and so the PreserveProperty restore action is the lowest in the totem pole of state mechanisms. If ViewState is enabled or a PostBack value exists those will overwrite the value that was preserved. The idea is that this mechanism won’t change any existing behaviors in ASP.NET. RestoreOutput() then parses the inbound string and tries to get the control and property/field and then assign the value to it.
The full format of the base64 decoded string looks something like this:
`|btnSubmit~|ForeColor~|_????___QSystem.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a___System.Drawing.Color__name_value
knownColor_state_ ___
_|``|lblMessage2~|BackColor~|_????___QSystem.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a___System.Drawing.Color__name_value
knownColor_state_ ___
_|``|__Page~|Test~|Hello|``|__Page~|Value~|2|``|__Page~|Address~|_????___HApp_Code.zbs41ciw, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null___AddressClass__Street_City_Zip_Phones_Access SomeValue_____PhoneNumberClass_______32 Kaiea___Paia___96779 _
_121.1112___PhoneNumberClass__Phone_Fax_email_______123-213-1231___123-123-1231_ _rickstrahl@hotmail.com_|`
There are a few different things persisted here. ForeColor (which turns into a serialized object), a simple value and a custom object. Notice that this serialization format isn’t very tight. There’s a lot of extra content – for one even to persist a simple integer value you have to write out the actual value, plus the name of the control and the property. The ID is the UniqueID by the way so that the control can be found regardless of the containership level.
The question marks and underscores in the text above are binary characters that don’t print and part of the ASCII text from the serialization. Although it looks funky they do work just fine holding the binary data values. Notice that many things that are non-obvious serialize - like Colors for example. So ForeColor and BackColor serialize into fairly large object signatures.
RestoreOutput() then parses through this string by splitting the string first into the individual preserved properties and then each of the components of each property (ID, Property and Value). Finally it takes the value and tries to reassign it to the control by reading the control’s original value and type, converting the string value to that type and then assigning it to the property/field. Here’s what RestoreOuput() looks like:
protected void RestoreOutput()
{
if (!this.Page.IsPostBack)
return;
string RawBuffer = HttpContext.Current.Request.Form["__" + this.UniqueID];
if (string.IsNullOrEmpty(RawBuffer))
return;
if (this.Base64Encode)
RawBuffer = Encoding.UTF8.GetString(Convert.FromBase64String(RawBuffer));
string[] PropertyStrings = RawBuffer.Split(new string[1] { "`|" }, StringSplitOptions.RemoveEmptyEntries);
foreach(string TPropertyString in PropertyStrings)
{
// *** Strip off |`
string PropertyString = TPropertyString.Substring(0, TPropertyString.Length - 2);
string[] Tokens = PropertyString.Split(new string[1] {"~|"},
StringSplitOptions.None);
string ControlId = Tokens[0];
string Property = Tokens[1];
string StringValue = Tokens[2];
Control Ctl = this.Page.FindControl(ControlId);
if (Ctl == null)
continue;
// *** Get existing Value
object CtlValue = wwUtils.GetPropertyCom(Ctl,Property);
object Value = this.StringToTypedValue(StringValue, CtlValue.GetType(),CultureInfo.InvariantCulture);
if (Value == null)
continue;
wwUtils.SetPropertyCom(Ctl, Property, Value);
}
}
public object StringToTypedValue(string SourceString, Type TargetType, CultureInfo Culture)
{
object Result = null;
if (TargetType == typeof(string))
Result = SourceString;
else if (TargetType == typeof(int))
Result = int.Parse(SourceString, System.Globalization.NumberStyles.Integer, Culture.NumberFormat);
else if (TargetType == typeof(byte))
Result = Convert.ToByte(SourceString);
else if (TargetType == typeof(decimal))
Result = Decimal.Parse(SourceString, System.Globalization.NumberStyles.Any, Culture.NumberFormat);
else if (TargetType == typeof(double))
Result = Double.Parse(SourceString, System.Globalization.NumberStyles.Any, Culture.NumberFormat);
else if (TargetType == typeof(bool))
{
if (SourceString.ToLower() == "true" || SourceString.ToLower() == "on" || SourceString == "1")
Result = true;
else
Result = false;
}
else if (TargetType == typeof(DateTime))
Result = Convert.ToDateTime(SourceString, Culture.DateTimeFormat);
else if (TargetType.IsEnum)
Result = Enum.Parse(TargetType, SourceString);
else
{
if (TargetType.IsSerializable)
{
if (SourceString == "" || SourceString == "")
return null;
byte[] BufferResult = Encoding.ASCII.GetBytes(SourceString);
//byte[] BufferResult = Convert.FromBase64String(SourceString);
Result = wwUtils.DeSerializeObject(BufferResult, TargetType);
}
else
{
System.ComponentModel.TypeConverter converter = System.ComponentModel.TypeDescriptor.GetConverter(TargetType);
if (converter != null && converter.CanConvertTo(typeof(string) ) )
try
{
return converter.ConvertFromString(null, Culture, SourceString);
}
catch
{ ; }
throw (new ApplicationException("Type Conversion failed for " +
TargetType.Name + "\r\n" + SourceString) );
}
}
return Result;
}
The code uses a couple of helper methods for GetProperty and SetProperty which are basically Reflection Type.InvokeMember calls, but otherwise this code is the reverse of what ReturnOutput() does. This code grabs the current value of the Control property to figure out what type to convert back to. It then converts the value and if successful assigns it back to the control.
There you have it. Since I’m still sitting on a bunch of 1.1 ASP.NET code that has moved to ASP.NET 2.0 I went through my current app I’m working on and added the control to about 10 pages that have DataGrids and persisted the ActivePageIndex and a couple of other custom settings that have been problematic. I was able to rip out a bunch of scattered code out of these pages that dealt with properly keeping track of these values previously. Very cool…
I’ve posted the code and a small sample at:
http://www.west-wind.com/files/tools/PreservePropertyControl.zip
You can check it out and play with this for yourself. If you come up with some useful changes please let me know by posting here…
Enjoy…