This class asides from being a collection and providing us with the perfect base for our collections, it also manages viewstate automagically for us. In the past if your collection was inheriting CollectionBase, now you can instead inherit StateManagedCollection. But first why do we need a collection ? I mean we could always just expose a property of type ArrayList of objects ?
But then an ArrayList overgoes a lot of un-necessary boxing and unboxing, this is a useless overhead. But so what, asp.net 2.0 now supports generics and we can use the generic list ? The overhead is gone and we have a nice collection exposed, so why go through the trouble of creating a custom collection ? Especially when a generic List is giving us a nice easy, effortless collection Vs writing a strongly typed wrapper collection ourselves. extract from msdn docs :
It is to your advantage to use the type-specific implementation of the List class instead of using the ArrayList class or writing a strongly typed wrapper collection yourself. The reason is your implementation must do what the .NET Framework does for you already, and the common language runtime can share Microsoft intermediate language code and metadata, which your implementation cannot.Now keeping this note in mind, consider that not always the List is what we want and we will need to resort to a custom collection we write ourselves. A good example is when we want the items in our list to participate in viewstate. I was unable to get this type of functionality.
The list does not implement IStateManager, so it does not manage saving viewstate by itself. In order to save the items in our list, in viewstate, when using a generic list, we are going to have to make sure that our items contained in the list do not in turn contain complex types. If they do, then we need to make sure that those complex types are decorated with the serialize() attribute.
That way their values can be serialized into viewstate. This is not usually in your control when you have a complex type like the Style class. Since this class has not been marked as serializable, you cannot just inherit it and mark your class as serializable and then voila. This will not work.
Serialization cannot be added to a class after it has been compiled. Also not to mention that using this approach of marking our class as serializable will result in pretty bad performance, especially in bloated data and overhead of serialization which takes quite a long time.
So for complex types like these, since we cant serialize them, we also cant have them participate in our controls viewstate. Remember complex types must manage their own viewstate by implementing IStateManager. If however the properties types that we are exposing in our Item are basic types and/or complex types that we have control over, this is possible.
However it looks pretty dirty imho. The reason I say dirty is becuase we can not control at the property level how items are adding to viewstate, meaning that we will be storing all items in viewstate whether their values have changed or not. This is not very convenient for me and results in a beautiful bloated viewstate which i prefer to avoid.
These advantages are however there when writing your own custom collections class and its not all that hard. Here in this post, i want to demonstrate how to create your own custom collection while allowing it to participate in viewstate for FREE*, that is with very little effort coming from your part by using a new class in the .net framework -->> StateManagedCollection.
In addition creating our custom collection also allows us to nest collections within collections, within collections, which i have found very useful. This, i am not demonstrating here but once you know how to create a custom collection, nesting collections is very simple and a no brainer so i dont want to waste time here with that.
I am not going to walk you through the code, because its very simple, basic code example. I have left comments inline, some useless, some are good notes and understanding them is essential, so make sure you dont miss them.
Note that since we will be using a CollectionEditor, so that you can get a beautiful dialog for adding items to your collection at designtime in visual studio 2005, then you need to reference one managed dll : System.Design. this dll is not added into a web application project by default, so you have to reference it yourself.
The output of this custom control are the 4 items we added to our collection, each item represented with a color. There is no purpose for this control other than to demonstrate collections, so functionality wise its pretty dumb. I tried to keep it as simple as possible. Posting back by clicking the button in the webform should cause the control to load these values from viewstate, and its perfect to see how this is all working nicely and here is the rendered output :
//MyControlItem.cs using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; namespace CustomControl { /// <summary> /// Summary description for MyControlItem /// </summary> public class MyControlItem : IStateManager { public MyControlItem() { } public string MyProperty { get { object o = ViewState["MyPropertyValue"]; return (o == null) ? string.Empty : o.ToString(); } set { ViewState["MyPropertyValue"] = value; } } /* Only need to call TrackViewState on the complex type (style) itself. The style class or rather the complex type is always responsible for maintaining its own viewstate. Our job is to simply call LoadViewState, SaveViewState and TrackViewState methods exposed by our complex types. To see that, look at the LoadViewState, SaveViewState and TrackViewState methods in this class. Since this is a complex type too it implements IStateManager interface. Our Style class also implements IStateManager like we are doing here.. */ private Style myControlStyleValue = null; [DesignerSerializationVisibility(DesignerSerializationVisibility.Content), PersistenceMode(PersistenceMode.InnerProperty), NotifyParentProperty(true)] public Style MyControlStyle { get { if (myControlStyleValue == null) { myControlStyleValue = new Style(); if (isTrackingViewStateValue) { ((IStateManager)(myControlStyleValue)).TrackViewState(); } } return myControlStyleValue; } } /* Store all our properties in the statebag exposed by this property. All properties that are not complex types can be stored by our custom implementation of the statebag. */ private StateBag viewStateValue; private StateBag ViewState { get { if (viewStateValue == null) { viewStateValue = new StateBag(false); if (isTrackingViewStateValue) { ((IStateManager)viewStateValue).TrackViewState(); } } return viewStateValue; } } private bool isTrackingViewStateValue; bool IStateManager.IsTrackingViewState { get { return isTrackingViewStateValue; } } object IStateManager.SaveViewState() { return this.SaveViewState(); } void IStateManager.LoadViewState(object savedState) { this.LoadViewState(savedState); } internal void LoadViewState(object savedState) { object[] states = (object[])savedState; if (states != null) { ((IStateManager)ViewState).LoadViewState(states[0]); ((IStateManager)MyControlStyle).LoadViewState(states[1]); } } internal object SaveViewState() { object[] states = new object[2]; states[0] = (viewStateValue != null) ? ((IStateManager)viewStateValue).SaveViewState() : null; states[1] = (myControlStyleValue != null) ? ((IStateManager)myControlStyleValue).SaveViewState() : null; return states; } void IStateManager.TrackViewState() { isTrackingViewStateValue = true; if (viewStateValue != null) ((IStateManager)viewStateValue).TrackViewState(); if (myControlStyleValue != null) ((IStateManager)myControlStyleValue).TrackViewState(); } /* This is important. Without this call, state wont be maintained. This method is being called by our collections SetDirtyObject method, which in turn is called internally by StateManagedCollection's SaveViewState method, Add, and Insert methods. The SetDirtyObject allows you to save viewstate information at the element level. In this method call of SetDirty we call set dirty individually only on the properties that manage state themselves. So its the only way that this class will know its items have changed and it needs to save it to viewstate. Forget to do this and viewstate wont be maintained. */ internal void SetDirty() { viewStateValue.SetDirty(true); if (myControlStyleValue != null) myControlStyleValue.SetDirty(); } } } //MyControlCollection.cs using System; using System.Web.UI; using System.Collections; namespace CustomControl { /// <summary> /// Summary description for MyControlCollection /// </summary> public class MyControlCollection : StateManagedCollection { public MyControlCollection() { } private static readonly Type[] knownTypes; static MyControlCollection() { MyControlCollection.knownTypes = new Type[] { typeof(MyControlItem) }; } /*Note how we cast to IList, this is because our base class implements IList and adds our elements to an IList.*/ public MyControlItem this[int index] { get { return (MyControlItem)((IList)this)[index]; } set { ((IList)this)[index] = value; } } // Add an item to the collection public int Add(MyControlItem value) { return ((IList)this).Add(value); } // Get the index of the value, passed in as parameter public int IndexOf(MyControlItem value) { return ((IList)this).IndexOf(value); } //Insert an item in the collection by index public void Insert(int index, MyControlItem value) { ((IList)this).Insert(index, value); } /*Copies the elements of the StateManagedCollection collection to an array, starting at a particular array index*/ public void CopyTo(MyControlItem[] toolBarSetArray, int index) { base.CopyTo(toolBarSetArray, index); } /* Overide and create an instance of the class that implements IStateManager. This class is ofcourse our item that we are adding to our collection. In this case the class is MyControlItem. its used internally by the base class (StateManagedCollection) when saving the item in viewstate. If by chance your class type can vary, then you can check the index and create a new class based on the index value. the index will vary based on the order of the types you specified in GetKnownTypes. For a good concreate example look up StateManagedCollection class on msdn. For this example, we have only one type, and the index is always zero, so there is no need to check the index before returning a new instance of our type*/ protected override object CreateKnownType(int index) { return new MyControlItem(); } /*This method should return an array of IStateManager types that the StateManagedCollection collection can contain. For a good concreate example look up StateManagedCollection class on msdn.*/ protected override Type[] GetKnownTypes() { return MyControlCollection.knownTypes; } // Remove an item from the collection public void Remove(MyControlItem value) { ((IList)this).Remove(value); } //Remove an item by index from the collection public void RemoveAt(int index) { ((IList)this).RemoveAt(index); } //Does our collection contain the following item in the collection ? public bool Contains(MyControlItem value) { // If value is not of type MyControlItem, this will return false. return ((IList)this).Contains(value); } // Validate and allow types that your collection will contain protected override void OnValidate(Object value) { if (value.GetType() != typeof(MyControlItem)) throw new ArgumentException( "value must be of type MyControlItem.", "value"); } /*instructs an object contained by the collection to record its entire state to view state, rather than recording only change information*/ protected override void SetDirtyObject(object o) { if (o is MyControlItem) { ((MyControlItem)o).SetDirty(); } } } } //MyControl.cs using System; using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; namespace CustomControl { /// <summary> /// Summary description for MyControl /// </summary> public class MyControl : WebControl { public MyControl() : base("div") { // // TODO: Add constructor logic here // } private bool isLoadedFromViewState = true; private MyControlCollection myControlCollectionValue = null; [Category("Behavior"), Description("A collection of ToolBarItem's "), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), Editor(typeof(System.ComponentModel.Design.CollectionEditor), typeof(System.Drawing.Design.UITypeEditor)), PersistenceMode(PersistenceMode.InnerProperty)] public MyControlCollection MyControlCollection { get { if (myControlCollectionValue == null) myControlCollectionValue = new MyControlCollection(); if (IsTrackingViewState) { ((IStateManager)(myControlCollectionValue)).TrackViewState(); } return myControlCollectionValue; } } protected override void LoadViewState(object state) { if (state != null) { object[] states = (object[])state; base.LoadViewState(states[0]); ((IStateManager)MyControlCollection).LoadViewState(states[1]); } } protected override object SaveViewState() { object[] states = new object[2]; states[0] = base.SaveViewState(); states[1] = (myControlCollectionValue != null) ? ((IStateManager)myControlCollectionValue).SaveViewState() : null; return states; } protected override void TrackViewState() { base.TrackViewState(); if (myControlCollectionValue != null) ((IStateManager)myControlCollectionValue).TrackViewState(); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); /* just create some defaults and add them to our collection. Since we are adding new items to our collection at runtime, viewstate should kick in during postback and rebuild the values from there. So only set some defaults if our collection is empty. Meaning this is the first time we are adding items to our collection :-) This is perfect to test if viewstate on my collection is working. */ if (this.MyControlCollection.Count < 1) { isLoadedFromViewState = false; string[] defaults = new string[] { "blue", "green", "yellow", "beige" }; foreach (string d in defaults) { MyControlItem mci = new MyControlItem(); mci.MyProperty = d; mci.MyControlStyle.BackColor = System.Drawing.Color.FromName(d); this.MyControlCollection.Add(mci); } } } protected override void CreateChildControls() { base.CreateChildControls(); if (this.MyControlCollection.Count > 0) { this.Controls.Add(new LiteralControl( string.Format( "<h1>This collection was loaded from viewstate ? {0}</h1>", isLoadedFromViewState.ToString()))); foreach (MyControlItem mci in this.MyControlCollection) { Label l = new Label(); l.Text = mci.MyProperty; l.ControlStyle.CopyFrom(mci.MyControlStyle); this.Controls.Add(l); } } Button b = new Button(); b.Text = "Lets postback, c'mon click me, please please click me :P"; this.Controls.Add(b);// dummy postback to check viewstate } } }
<%@ Page Language="C#" %> <%@ Register TagPrefix="ctl" Namespace="CustomControl" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <ctl:MyControl ID="MyControl1" runat="server"></ctl:MyControl> </div> </form> </body> </html>
Wo, I had been searching for a long time for this. Great job man. This is EXACTLY what I needed. You saved my job.
ReplyDeleteYour more than welcome. Thanks for comment :-)
ReplyDeleteJust ran into this example, very well done.
ReplyDelete