Child Collections in Asp.Net Custom Controls
I have been developing custom web controls for many years now and I am just now getting comfortable with many of the advanced features afforded by the rich System.Web.UI.WebControls namespace. One feature I have implemented many different times over the years is having a web control which contains a list of sub items. For example, the Asp.Net GridView's collection of Columns or the DropDownList's collection of ListItems.
<asp:GridView ID="grid" runat="server">
<Columns>
<asp:BoundField />
<asp:ButtonField />
</Columns>
</asp:GridView>
There is so many things going on it a control like this that just seem like magic. How are the child items persisted in ViewState? How does the control know how to parse the child items? What if the whole world was a power ranger?
StateManagedItem
I have created a couple of classes that really simplify the process of creating lists of child items. The first of which is the class StateManagedItem. This class is the base class for all the objects which you wish to use as "Child Items" (like grid view columns or list items).
In order to manage state in Asp.Net, you have to implement the IStateManager interface. This interface allows objects to participate in the process of saving and retrieving things from ViewState. In order to generalize the whole process, I needed on other method to set an item as "Dirty" so I created the following interface:
public interface IStateManagedItem : IStateManager
{
void SetDirty();
}
So now we have the base interface for all state managed items. So here is the what the base class definition looks like:
public abstract class StateManagedItem : IStateManagedItem
This class provides it's own ViewState for items to save their state in which essentially allows "Child Items" to save their state just like custom web controls do. To illustrate this concept, lets create an example class called Appointment which will hold information about a calendar appointment.
public class Appointment : StateManagedItem
The definition for this class is very simple. Here is how the properties are implemented:
public DateTime? Start
{
get
{
object o = ViewState["Start"];
return o == null ? null : (DateTime?)o;
}
set { ViewState["Start"] = value; }
}
If you are familiar with using ViewState to store web control properties, this syntax should look fairly familiar. That that is all there is to our child item.
StateManagedCollection
The next helper class is a base class for the collection of items. In our example, we need a collection of Appointments. For this purpose, I have created a StateManagedCollection
public abstract class StateManagedCollection<T> : IList<T>, ICollection<T>,
IEnumerable<T>, IStateManager, IList, ICollection
where T : class, IStateManagedItem, new()
If you are not familiar with generics and generic constraints, this is basically a list class which requires the child items to be a class (not a struct), derive from IStateManagedItem, and to have a default constructor.
So to create our list of Appointments, all we need to do is the following:
public class AppointmentCollection : StateManagedCollection<Appointment>
{
}
That is it as far as the AppointmentCollection class goes. It doesn't require any more code.
Sample Control
So lets finish the example by creating a custom control which displays a list of Appointments. First we will derive from Composite Control:
[ParseChildren(true), PersistChildren(false)]
public class AppointmentControl : CompositeControl
Note the ParseChildren and PersistChildren attributes, these are required to tell Asp.Net how to parse the markup inbetween the start and end tags of your control. This is part of the "Magic" I mentioned earlier.
Next, make a member of the class which is an AppointmentCollection:
private AppointmentCollection appointments = null;
And a public property to access that collection:
[
Mergable(false),
PersistenceMode(PersistenceMode.InnerProperty),
]
public AppointmentCollection Appointments
{
get
{
if (appointments == null)
{
appointments = new AppointmentCollection();
if (base.IsTrackingViewState)
{
appointments.TrackViewState();
}
}
return appointments;
}
}
This property also must have a couple of attributes - Mergable and PersistenceMode. These basically tell Asp.Net and Visual Studio how to handle and persist this property.
To use this control, first place the proper includ directive on your page:
<%@ Register TagPrefix="web" Namespace="Web.Example.Appointment" %>
Now we can declare our control markup as:
<web:AppointmentControl id="appointmentList" runat="server">
<Appointments>
<web:Appointment Start="1/6/2007 8:00 AM" End="1/6/2007 11:30 AM"
Description="Breakfast" />
<web:Appointment Start="5/12/2007 9:30 AM" End="5/12/2007 9:45 AM"
Description="Meeting" />
<web:Appointment Start="5/12/2007 10:00 AM" End="5/12/2007 10:30 AM"
Description="Break" />
</Appointments>
</web:AppointmentControl>
I won't go into the render code (you can download the source code to see all the gory details) but here is how the control renders:
You have 3 appointment(s)
Breakfast - (01/06/2007 08:00:00 - 01/06/2007 11:30:00)
Meeting - (05/12/2007 09:30:00 - 05/12/2007 09:45:00)
Break - (05/12/2007 10:00:00 - 05/12/2007 10:30:00)
You can download the full source code here. Hope this helps dispel some of the "Magic" of Asp.Net controls.