Updating the ASP.NET PreserveProperty Control
So, after all the great suggestions that were made regarding the PreserveProperty Control last week I sat down and played around with a few of the suggestions made in comments. The most significant one was creating a more efficient storage mechanism then what was shown in the original post.
As I posted in the comments I added Peter Bromberg’s code he posted in the comments to the LosFormatter which reduced the size of the custom control state quite a bit and made the code a lot simpler for parsing complex objects.
Bertrand LeRoy also made a cool suggestion about using ControlState instead of managing the persistence on my own. I hadn’t really spent much time looking at how control state is implemented so I had to some quick reading up on Fredrik Normén's Blog.
ControlState is pretty easy to work with simply by implementing two methods on the Page class: SaveControlState and LoadControlState. Save basically needs to return an object that contains the state and ASP.NET conveniently parcels up that state and stores it in ViewState. It stores it even if ViewState is disabled on a page which is an important consideration for viewstate. When the page posts back LoadControlState then is called with an object parameter that essentially restores that object saved in SaveControlState. It’s a very easy mechanism and does away with all the messy encoding code I had been using before. This reduced the code of the class drastically.
First try didn’t go so well. I ended up using a generic Dictionary<string,object> list and while that worked just fine – control state properly encoded the object – the output generated was huge. The problem is that a dictionary object is basically a bunch of sub objects of key value pairs that ended up generating a huge list of items. Ooops, that wasn’t much of an improvement.
So, I went back to using a good old Hashtable which persists quite nicely into ViewState and reducing my sample output by 30% - Cool! Simpler code and a good sized reduction in the data is great.
One thing I don’t like is that ControlState stores its value in the ViewState hidden variable. I like the idea of storing this state separately, so I also added a StorageMode property which allows selecting how the data is actually stored. The options are ControlState, HiddenVariable and SessionVariable.
Both Hidden and Session Variable use the LosFormatter to simply turn the Hashtable into a string and store that either in a hidden form var or a Session variable. It turns out the output generated is just about the same as it is for ControlState, minues the preamble for the ControlState in ViewState.
The Session mechanism does basically the same thing opting to store the resulting string from the LosFormatter in a Session Variable. Using Session Variables is probably not a good idea in most situations because it stores a single value in the session object. The problem is that if you have two pages open within the same browser instances (say two tabs or two windows or even two frames) you’re going to get session cross talk. Still, in some applications that may not be an issue as it is highly unlikely two apps are run.
One thing to note is that, as with ViewState and stock ControlState you will get generic errors if you try to store objects that the LosFormatter can handle. Basically complex objects that don’t support Serialization or aren’t flagged for custom TypeConverters. This is a bummer because some common things you might want to preserve like say the Style object can’t be serialized.
Here’s the code for the class and you can also download this from:
http://www.west-wind.com/files/tools/PreservePropertyControl.zip
[ParseChildren(true)]
[PersistChildren(false)]
public class PreservePropertyControl : Control
{
private const char CTLID_PROPERTY_SEPERATOR = '|';
/// <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>
/// Internal object used for persisting to control state. Control stores all preserved
/// property values into this dictionary which in turn persists the values into viewstate
/// </summary>
protected Hashtable SerialzedProperties = new Hashtable();
/// <summary>
/// Determines the storage mode for the control
/// </summary>
[Description("Determines how the Property data is persisted.")]
public PropertyStorageModes StorageMode
{
get
{
return _StorageMode;
}
set
{
_StorageMode = value;
}
}
private PropertyStorageModes _StorageMode = PropertyStorageModes.ControlState;
/// <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);
}
/// <summary>
/// Determines whether the control preserves and restores values
/// </summary>
[Description("Determines whether the control preserves and restores values"),DefaultValue(true)]
public bool Enabled
{
get
{
return _Enabled;
}
set
{
_Enabled = value;
}
}
private bool _Enabled = true;
/// <summary>
/// Adds a control to the collection. At this point only the
/// control and property are stored.
/// </summary>
/// <param name="WebControl"></param>
/// <param name="Property"></param>
/// <returns></returns>
public bool PreserveProperty(Control WebControl, string Property)
{
PreservedProperty pp = new PreservedProperty();
pp.ControlId = WebControl.UniqueID;
pp.ControlInstance = WebControl;
pp.Property = Property;
this.PreservedProperties.Add(pp);
return true;
}
/// <summary>
/// Adds a control to the collection. At this point only the
/// control and property are stored.
/// </summary>
/// <param name="ControlId"></param>
/// <param name="Property"></param>
/// <returns></returns>
public bool PreserveProperty(string ControlId, string Property)
{
Control ctl = this.Page.FindControl(ControlId);
if (ctl == null)
throw new ApplicationException("Can't persist control: " + ControlId + "." + Property);
return this.PreserveProperty(ctl, Property);
}
/// <summary>
/// Read in data of preserved properties in OnInit
/// </summary>
/// <param name="e"></param>
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (this.Enabled)
{
if (this.StorageMode == PropertyStorageModes.ControlState)
this.Page.RegisterRequiresControlState(this);
else
this.LoadStateFromLosStorage();
}
}
/// <summary>
/// Write out data for preserved properties in OnPreRender
/// </summary>
/// <param name="e"></param>
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (this.Enabled && StorageMode != PropertyStorageModes.ControlState)
this.SaveStateToLosStorage();
}
/// <summary>
/// Saves the preserved Properties into a Hashtabe where the key is
/// a string containing the ControlID and Property name
/// </summary>
/// <returns></returns>
protected override object SaveControlState()
{
foreach (PreservedProperty Property in this.PreservedProperties)
{
// *** Try to get a control instance
Control Ctl = Property.ControlInstance;
if (Ctl == null)
{
// *** Nope - user stored a string or declarative
Ctl = this.Page.FindControl(Property.ControlId);
if (Ctl == null)
continue;
}
string Key = Ctl.UniqueID + CTLID_PROPERTY_SEPERATOR + Property.Property;
// *** If the property was already added skip over it
// *** Values are read now so duplicates are always the same
if (this.SerialzedProperties.Contains(Key))
continue;
// *** Try to retrieve the property
object Value = null;
try
{
// *** Use Reflection to get the value out
// *** Note: InvokeMember is easier here since
// we support both fields and properties
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);
}
// *** Store into our hashtable to persist later
this.SerialzedProperties.Add(Key, Value);
}
// *** store the hashtable in control state (or return it
return this.SerialzedProperties;
}
/// <summary>
/// Overridden to store a HashTable of preserved properties.
/// Key: CtlID + "|" + Property
/// Value: Value of the control
/// </summary>
/// <param name="savedState"></param>
protected override void LoadControlState(object savedState)
{
Hashtable Properties = (Hashtable)savedState;
IDictionaryEnumerator Enum = Properties.GetEnumerator();
while (Enum.MoveNext())
{
string Key = (string)Enum.Key;
string[] Tokens = Key.Split(CTLID_PROPERTY_SEPERATOR);
string ControlId = Tokens[0];
string Property = Tokens[1];
Control Ctl = this.Page.FindControl(ControlId);
if (Ctl == null)
continue;
Ctl.GetType().InvokeMember(Property,
BindingFlags.SetField | BindingFlags.SetProperty |
BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.IgnoreCase, null, Ctl,new object[1] { Enum.Value } );
}
}
private void SaveStateToLosStorage()
{
string Serialized = LosSerializeObject( this.SaveControlState() );
if (this.StorageMode == PropertyStorageModes.HiddenVariable)
this.Page.ClientScript.RegisterHiddenField("__" + this.UniqueID, Serialized);
else // if (this.StorageMode == PropertyStorageModes.SessionVariable)
HttpContext.Current.Session["__" + this.UniqueID] = Serialized;
}
private void LoadStateFromLosStorage()
{
string RawBuffer = null;
if (this.StorageMode == PropertyStorageModes.HiddenVariable)
{
RawBuffer = HttpContext.Current.Request.Form["__" + this.UniqueID];
if (RawBuffer == null)
return;
}
else
{
RawBuffer = HttpContext.Current.Session["__" + this.UniqueID] as string;
if (RawBuffer == null)
return;
}
// *** Retrieve the persisted HashTable and pass to LoadControlState
// *** to handle the assignment of property values
this.LoadControlState(LosDeserializeObject(RawBuffer));
}
private string LosSerializeObject(object obj)
{
LosFormatter output = new LosFormatter();
StringWriter writer = new StringWriter();
output.Serialize(writer, obj);
return writer.ToString();
}
private object LosDeserializeObject(string inputString)
{
LosFormatter input = new LosFormatter();
return input.Deserialize(inputString);
}
protected override void Render(HtmlTextWriter writer)
{
if (this.DesignMode)
writer.Write("[ *** wwPersister: " + this.ID + " *** ]");
base.Render(writer);
}
}
/// <summary>
/// Determines how preserved properties are stored on the page
/// </summary>
public enum PropertyStorageModes
{
ControlState,
HiddenVariable,
SessionVariable
}
Other Posts you might also like
- Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application
- Resolving Paths To Server Relative Paths in .NET Code
- Map Physical Paths with an HttpContext.MapPath() Extension Method in ASP.NET
- Back to Basics: Rewriting a URL in ASP.NET Core
- Getting the Client IP Address in ASP.NET Core
The Voices of Reason
# re: Updating the ASP.NET PreserveProperty Control
all in all still useful
# re: Updating the ASP.NET PreserveProperty Control
http://aspadvice.com/blogs/robertb/archive/2005/11/16/13835.aspx
# re: Updating the ASP.NET PreserveProperty Control
The other issue there is that now you're going to generate potentially a LOT of data into the Cache of the application, with each page access causing a new Cache entry which doesn't time out for quite some time (20 minutes by default). I'm thinking this can easily hammer a moderately busy site. When you ran your WAST tests, did you by any chance run it for a while and watch memory usage?
# re: Updating the ASP.NET PreserveProperty Control
Save:
else if (this.StorageMode == PropertyStorageModes.CachePerPage)
{
if (this.PreservePropertyKey == null)
this.PreservePropertyKey = Guid.NewGuid().ToString().GetHashCode().ToString("x");
HttpContext.Current.Cache[this.PreservePropertyKey] = Serialized;
this.Page.ClientScript.RegisterHiddenField("__PreservePropertyKey", this.PreservePropertyKey);
}
Load:
else if (this.StorageMode == PropertyStorageModes.CachePerPage)
{
this.PreservePropertyKey = HttpContext.Current.Request.Form["__PreservePropertyKey"];
if (this.PreservePropertyKey == null)
return;
RawBuffer = HttpContext.Current.Cache[this.PreservePropertyKey] as string;
}
# re: Updating the ASP.NET PreserveProperty Control
Your solution for Per-user-per-page Cache storage is elegant and works well. In the original "Testing" article I pointed to, the objective was very narrow, mainly to see if there were big differences between storing ViewState data in Session, Cache, etc. and it showed a significant-enough advantage with Cache that it warranted further study. That's where I stopped the tests, to answer your other question.
# re: Updating the ASP.NET PreserveProperty Control
Please don't hate me (too much) for posting this question here.
The question that I have is more of a usage question. The PreservePropertyControl is something that appears to be very useful for an application that I am working on. In this application, I have a DropDownList control that is bound to a custom object. Right now, I have ViewState for this control turned on. If I turn ViewState off, I have to rebind the data to the control on every postback. I bind the data to the control in the page's Load event. If I use the PreservePropertyControl to save the SelectedIndex property of the DropDownList, it is my understanding that the SelectedIndex property will be restored before the page's Load event and will be wiped out when the data is rebound to the control during the Load event. Is this understanding correct? Is there something that I can do to remedy this behavior?
# re: Updating the ASP.NET PreserveProperty Control
I think you should use the entire Guid.NewGuid().ToString() as the key, not just the GetHashCode() value as it is distinctly possible that two Guid(s) can hash to the same value, which would cause one user's Cache entry to be lost. It's only a couple more characters on the form value.
# re: Updating the ASP.NET PreserveProperty Control
Having played with this a tiny bit today, looks like you should use ObjectStateFormatter.Serialize(Stream, Object) constructor directly when the state storage is ControlState, Cache or Session (since they can squirrel away the binary array at much lower cost), and use the LosFormatter or the ObjectStateFormatter.Serialize(Object) constructor when state storage is HiddenVariable (since that gives you a Base64 string).
# re: Updating the ASP.NET PreserveProperty Control
Maybe you can explain what is the difference between ObjectStateFormatter and LosFormatter? What should I use if I want to override SavePageStateToPersistenceMedium / LoadPageStateFromPersistenceMedium methods?
I use .NET 2.0.
Thank you in advance,
Igor Olikh
# re: Updating the ASP.NET PreserveProperty Control
thank you so much for this, I've been trying to find an easy and fast way to do this in my user controls.
However, I've found a limitation in your class... I wanted to drag your control to my own user controls, and specify the property to persist.
It would be simpler to do it only once inside the user control, not on every page my control is used.
In the design code of my user controls, I want to be able to use something like this (without specifying the ControlId property of the PreservePropertyControl)
<%@ Control Language="C#" AutoEventWireup="true" EnableViewState="true" CodeFile="TemplateSelect.ascx.cs" Inherits="TemplateSelect" %> <%@ Register Assembly="Westwind.Web.Controls.PreservePropertyControl" Namespace="Westwind.Web.Controls" TagPrefix="cc1" %> <!-- page code here --> <cc1:PreservePropertyControl ID="PreservedProperties" StorageMode="ControlState" runat="server"> <PreservedProperties> <cc1:PreservedProperty ID="PreserveTemplates" runat="server" Property="templates"></cc1:PreservedProperty> </PreservedProperties> </cc1:PreservePropertyControl>
What I want to say is that there are some cases in which I don't want to specify the <b>ControlId</b> to use,
I want to use the control that contains the PreservePropertyControl
I order to achieve this, I've modified the function SaveControlState.
If there's no ControlInstance or ControlId specified, it will use the Parent Control
protected override object SaveControlState() { foreach (PreservedProperty Property in this.PreservedProperties) { // *** Try to get a control instance Control Ctl = Property.ControlInstance; if (Ctl == null) { // *** Nope - user stored a string or declarative if(!string.IsNullOrEmpty(Property.ControlId)) Ctl = this.Page.FindControl(Property.ControlId); if (Ctl == null) { // ADDED CODE - if no controlId is specified, use parent control Ctl = this.Parent; if (Ctl == null) continue; } } string Key = Ctl.UniqueID + CTLID_PROPERTY_SEPERATOR + Property.Property; // *** If the property was already added skip over it // *** Values are read now so duplicates are always the same if (this.SerializedProperties.Contains(Key)) continue; // *** Try to retrieve the property object Value = null; try { // *** Use Reflection to get the value out // *** Note: InvokeMember is easier here since // we support both fields and properties 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); } // *** Store into our hashtable to persist later this.SerializedProperties.Add(Key, Value); } // *** store the hashtable in control state (or return it return this.SerializedProperties; }
# re: Updating the ASP.NET PreserveProperty Control
http://www.eggheadcafe.com/articles/20040613.asp