Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Updating the ASP.NET PreserveProperty Control


:P
On this page:

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

}

 


The Voices of Reason


 

Peter Bromberg
January 07, 2006

# re: Updating the ASP.NET PreserveProperty Control

Nice work Rick! BTW, earlier work I've done on storage of viewstate indicated that Cache storage (vs Hidden or Session) was actually the most performant storage. You could add a PropertyStorageModes.Cache option by using the SessionID as a unique key, in which case Session could be set to ReadOnly if desired. You can see the tests here:
http://www.eggheadcafe.com/articles/20040613.asp

cowgaR
January 07, 2006

# re: Updating the ASP.NET PreserveProperty Control

One thumb up for server-side cache storage mode, I just read Mr. Bromberg article (thank you for the tests, rare these days) and found 500% improvement, but that is only when using viewstate all the time...

all in all still useful

cowgaR
January 07, 2006

# re: Updating the ASP.NET PreserveProperty Control

regarding server-side viewstate storing mechanism, I just found out interesting Mr. Boedigheimer's (Peter mentioned him in article) blog-post about ASP.NET 2.0 PageStateAdapter class:

http://aspadvice.com/blogs/robertb/archive/2005/11/16/13835.aspx


Rick Strahl
January 07, 2006

# re: Updating the ASP.NET PreserveProperty Control

Peter good stuff. A couple of thoughts on your article are that UserHostAddress + DateTime() is probably slightly unreliable since Proxies can cause problems with same idsand potential overlap. A GUID hashed value maybe?

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?

Rick Strahl
January 07, 2006

# re: Updating the ASP.NET PreserveProperty Control

So in light of plain Session issues how about storing in Cache on a per Page basis?

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;
}

Peter Bromberg
January 08, 2006

# re: Updating the ASP.NET PreserveProperty Control

Rick,
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.

JR
January 10, 2006

# re: Updating the ASP.NET PreserveProperty Control

Hello all,

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?

Marc Brooks
January 17, 2006

# re: Updating the ASP.NET PreserveProperty Control

Rick,
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.

Marc Brooks
January 17, 2006

# re: Updating the ASP.NET PreserveProperty Control

Rick,
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).

Rick Strahl's Web Log
September 28, 2006

# Implementing an ASP.NET PreserveProperty Control - Rick Strahl's Web Log

A couple of days ago I posted about having PreserveProperty functionality in ASP.NET. Today I'll look at implementing a custom control that implements this functionality. The control allows saving and restoring of property values automatically and declaratively without ViewState being enabled. This article discusses the control functionality and implementation in some detail. If you're new to Server Control development this article covers a few important topics such as exposing collections as ch

Peter Bromberg's UnBlog
October 17, 2006

# Peter Bromberg's UnBlog: Default ASP.NET Web Administration with SQL Server 2005, and a PreserveProperty Control


RussianGeek
October 23, 2006

# re: Updating the ASP.NET PreserveProperty Control

Hi!

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

Rui Jarimba
June 08, 2009

# re: Updating the ASP.NET PreserveProperty Control

Hi Rick,

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;
}      

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024