Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Creating a Persistent Panel Web Control

4.86/5 (15 votes)
1 Mar 2010CPOL8 min read 1   730  
Extending ASP.NET's Built in Panel Control to create Sticky control values between page visits

1.0 Introduction

"The search page looks great," said my customer, "but after I click on a search result and go to another page, I want my search page to come back sorted and filtered in the same way as I left it."

Regardless of what the requirements say, your users are frequently going to expect pages in your web site to retain their state in between visits. This is often called making a page "sticky." This article will demonstrate how to create a PersistentPanelControl that makes this task trivial.

2.0 Using the Code

If you are not interested in the construction of the web control itself, then you can simply download the package and incorporate the DLL into any page by writing just three lines of code.

XML
<%@ Register TagPrefix="util" Namespace="PersistentPanel" Assembly="PersistentPanel">

<util:PersistentPanelControl runat="server" id="persistentPanel">
     <!-- Drop any Controls to be persisted here -->
</util:PersistentPanelControl>

The PersistentPanelControl will remember the state of any server controls placed inside its body on any post-back event, and will restore the state of those controls when you return.

The default behavior of the control is to save all control state data to the Session, but you can override this behavior by implementing the IControlPersistenceProvider interface and specifying the new implementation in your site's web.config file.

XML
<appSettings>
  <!--
      Web.config setting to specify the type and assembly of the
  Control Persistence Provider
      If this setting does not exist, the control will default to
  SessionControlPersistenceProvider
  -->
  <add key="ControlPersistenceProviderType"
  value="PersistentPanel.SessionControlPersistenceProvider,PersistentPanel"/>
</appSettings>

3.0 Building the Control

3.1 High Level Design

3.2.1 The Base Class

Many beginner and even intermediate ASP.NET developers are intimidated by the thought of building their own web control. The prospect of creating HTML rendering methods, child control maintenance logic, and visual designer code will often discourage a developer from writing a useful web control, and instead lead him to write the functionality that should be encapsulated by a control into the code-behind of the web page itself. One way to avoid the overhead of writing a web control from scratch (that is, from the WebControl and CompositeControl base classes) is to derive the control from an existing ASP.NET web control, and let the base control handle most of the ASP.NET plumbing, the developer to write only the functionality that is unique to your custom control.

Upon a little reflection, we realize that the only new requirement in our new control is to save the state of all the child controls between page requests. All of the logic required to manage a set of child controls, render them to the page, display the control and its children to the Visual Studio Designer, and integrate with the ASP.NET event model is already included in ASP.NET's built in System.Web.UI.WebControls.Panel class! Thus, inheriting from Panel allows us to write a control that focuses almost exclusively on the business logic of saving and restoring control state between requests.

3.2.2 Control Flow

We need to answer one question before proceeding further in our design: when should the control save the state of its child controls, and when should it restore that state? Among all possible scenarios, the most common usage will be when a user wants a page to remember the values he entered when he takes some action such as searching or filtering, and to remember it when returning to the page. We can reasonably distinguish these two scenarios using ASP.NET's post-back model. The control will save the state of all its children when the request is a post-back, and restore them to their saved state when the request is not a post back.

3.3.3 Using the Provider Pattern for Persistence

Another question that we need to answer before creating our component is where should our control persist its state? Unfortunately, this question is not straight forward, and may provide different answers based on different use cases. Some pages may only need to remember state for the current session, and can store that state in the Session object. Some may not be able to use the Session object, or may need to store the data between sessions. In this case, we may want to use browser cookies to store data that can persist between sessions. Still other applications may need to persist data to a database in order to allow users to keep their settings even when logging on from other computers.

In order to accommodate the potential needs of future users, the control uses the provider model for persisting the actual state information via the IControlPersistenceProvider interface.

C#
/// <summary>
/// The public interface that must be
/// implemented to persist control state
/// </summary>
public interface IControlPersistenceProvider
{
    void SaveControlState(Control parent, Dictionary<string,object> controlState);
    Dictionary<string,object> RestrieveControlState(Control parent);
}

Good design and usability tenets demand that all component controls must be able to run out of the box with little or no configuration. Thus, our control will ship with a default implementation that saves state to the Session object - a simple implementation that will satisfy the majority of cases.

3.2 Populating the Control State

The real business of the PersistentPanelControl is encapsulated in the method to populate a dictionary containing the current values of all child controls (the control state), and the method to restore those control values from that dictionary. Because child controls can be nested within one another, these methods are not as simple as looping through all of the PersistentPanelControl's child controls and remembering their state. Both methods must recursively populate each child's children in order to save the state of the entire control tree.

C#
/// <summary>
/// Saves all control values to a serialized dictionary. The method recurses through all 
/// levels of children, saving non literal control values
/// </summary>
protected void PopulateControlState
	(Control parent, Dictionary<string,object> controlState)
{
    //loop through each control and add value to the dictionary
    foreach (Control ctrl in parent.Controls)
    {
        //filter out literal controls because it can add many
        //unnecessary entries for immutable controls to the dictionary
        if (ctrl.ID != null
             && !(ctrl is LiteralControl)
             && !(ctrl is Literal))
        {                    
        //BEGIN SNIP
        // code that populate's the control's state removed for clarity
        //END SNIP
        
        //recursively populate control state with each child's children
        PopulateControlState(ctrl, controlState);
    }
}

As you can see, the population method is recursive. The first call to this method will use the PersistentPanelControl instance itself and an empty control state Dictionary.

The process of saving an actual control's state to the Dictionary depends on the type of control that we are saving. For example, the state should contain all selected values of a ListBox control as an array of strings, but only a single selected value string for single value ListControl objects.

C#
//List Boxes are potentially multi-select, so be prepared to store a delimited list of 
//selected values
if (ctrl is ListBox)
{
    ListBox lst = (ListBox)ctrl;
    List<string> itemValues = new List<string>(lst.Items.Count);
    foreach (ListItem item in lst.Items)
    {
        if (item.Selected)
        {
	    itemValues.Add(item.Value);
        }
    }
    controlState[lst.ID] = itemValues.ToArray();
}

//other list controls can have only one selected value
else if (ctrl is ListControl)
{
    controlState[ctrl.ID] = ((ListControl)ctrl).SelectedValue;
}

For more complex controls, more complex logic must be written to accurately save state. For example, the persistence mechanism for GridView controls must save both the sort expression, sort direction, and page index:

C#
else if (ctrl is GridView)
{
    GridView gv = (GridView)ctrl;
    //add sorting to the list with the header SORT 
    if (gv.AllowSorting)
    {
        controlState[ctrl.ID + SORT_INDEX_SUFFIX] = gv.SortExpression;
        controlState[ctrl.ID + SORT_DIRECTION_SUFFIX] = gv.SortDirection.ToString();
    }

    //add paging to the list with the header PAGE
    //don't forget to add a delimiter in between if necessary
    if (gv.AllowPaging)
    {
        controlState[ctrl.ID + PAGE_NUMBER_SUFFIX] = gv.PageIndex;
    }
}

Finally, many controls implement the ITextControl interface. Placing the ITextControl check at the end of the list is a way to save the text of several types of controls including TextBox, Label, and Button that do not match a more specific control type.

C#
//default catch for text controls
else if (ctrl is ITextControl)
{
    controlState[ctrl.ID] = ((ITextControl)ctrl).Text;
}

3.3 Restoring from the Control State

Restoring the child controls to their original state is simply the inverse of populating the state object. One caveat is that while most controls have their state saved in the Dictionary under their control ID, complex objects like GridView have their state saved under multiple keys using the combination of the control's ID and a prefix.

C#
if (ctrl.ID != null && controlState.ContainsKey(ctrl.ID))
{
    object controlValue = controlState[ctrl.ID];

    if (ctrl is ListBox)
    {
         string[] selectedValues = controlValue as string[];
         if (selectedValues != null)
         {
             foreach (ListItem li in ((ListBox)ctrl).Items)
             {
                 li.Selected = selectedValues.Contains<string>(li.Value);
             }
         }
    }
    else if (ctrl is ListControl)
    {
        ((ListControl)ctrl).SelectedValue = controlValue as string;
    }
    else if (ctrl is ITextControl)
    {
        ((ITextControl)ctrl).Text = controlValue as string;
    }
}
else if (ctrl is GridView)
{
    GridView gv = (GridView)ctrl;

    if (controlState.ContainsKey(gv.ID + SORT_INDEX_SUFFIX))
    {
        gv.AllowSorting = true;
        SortDirection direction = (SortDirection)Enum.Parse(typeof(SortDirection), 
        controlState[gv.ID + SORT_DIRECTION_SUFFIX] as string);
 
        gv.Sort(controlState[gv.ID + SORT_INDEX_SUFFIX] as string, direction);
    }

    // Restore page number AFTER sort or else sort
    // function will reset page number
    if (controlState.ContainsKey(gv.ID + PAGE_NUMBER_SUFFIX))
    {
        gv.AllowPaging = true;
        gv.PageIndex = (int)controlState[gv.ID + PAGE_NUMBER_SUFFIX];
    }
}

3.4 Initializing the Persistence Provider

The high level design of the component calls for a pluggable way to persist the actual state of the control. The control will encapsulate this logic in a Read-Only property that first checks the web.config file's <appSettings> section to see if a provider has been named, and defaults to the SessionControlPersistenceProvider if no type is specified. Because the implementation is set in web.config, the control can safely save the instance of the provider to the application state.

C#
/// <summary>
/// private variable to hold the instance of the persistence provider
/// </summary>
private IControlPersistenceProvider _persistenceProvider = null;

/// <summary>
/// Readonly copy of the persistence provider. 
/// Default value is SessionControlPersistenceProvider, but this
/// value can be overridden by specifying another type 
/// in the "ControlPersistenceProviderType" application setting
/// </summary>
public IControlPersistenceProvider PersistenceProvider
{
    get
    {
        if (_persistenceProvider == null)
        {
            if (Page.Application[ApplicationStateKey] == null)
            {
                string providerType = ConfigurationManager.AppSettings
				["ControlPersistenceProviderType"];
                if (providerType == null)
                {
                    _persistenceProvider = new SessionControlPersistenceProvider();
                }
                else
                {
                    ConstructorInfo ctor = 
			Type.GetType(providerType).GetConstructor(new Type[0]);
                    _persistenceProvider = 
			(IControlPersistenceProvider)ctor.Invoke(new object[0]);

                    Page.Application[ApplicationStateKey] = _persistenceProvider;
                }
            }
            else
            {
                 _persistenceProvider = 
		(IControlPersistenceProvider)Page.Application[ApplicationStateKey];
            }
        }
        return _persistenceProvider;
    }
}

Default implementation, saving control state to the Session object:

C#
/// <summary>
/// Trivial implementation of the persistence provider to 
/// store control values in the original Dictionary form
/// to the session
/// <summary>
public class SessionControlPersistenceProvider : IControlPersistenceProvider
{
    public void SaveControlState
	(Control parent, Dictionary<string, object> controlState)
    {
        parent.Page.Session[CreateSessionKey(parent)] = controlState;
    }

    public Dictionary<string, object> RestrieveControlState(Control parent)
    {
        return parent.Page.Session[CreateSessionKey(parent)] 
				as Dictionary<string, object>;
    }

    private string CreateSessionKey(Control parent)
    {
        return parent.Page.ToString() + parent.UniqueID;
    }
}

3.5 Order of Execution in the ASP.NET Page Life Cycle

Understanding the ASP.NET Page Life Cycle is of the greatest importance for web control developers. When the control saves and restores the state of its children in this life cycle is critical to proper functionality. If the control saves its state before the event handlers of its children have fired, then it will miss any updates that happen within those handlers. If the control restores the state too early, then the controls themselves may not yet have been initialized.

For the PersistentPanelControl, the opportune time to save the control state is during the OnUnload event of post-back requests. This event is fired after all controls have already been rendered, and their values are guaranteed not to change within the control's life cycle. The restore event will be fired during the OnInit method, which occurs immediately after all child controls are initialized.

4.0 Demo Application

This control ships with a demonstration web page in the TestWebApp project. This project contains a single Default.aspx page that contains a PersistentPanelControl. The panel has children of various types for users to experiment with, including a GridView. The grid view populates a list of products from the AdventureWorks2008 database, which is required for the demo to run.

5.0 Future Enhancements

We have just covered a very basic implementation of the PersistentPanelControl. A full implementation would include:

  • Support for more types of child controls
  • A property to exclude named child controls from the persistence list
  • A property to allow users to override the default implementation of IControlPersistenceProvider on an instance by instance basis
  • Descriptive exception messages
  • Published events that will allow developers to write code before and after populating and restoring control state.
  • Implementations of the IControlPersistenceProvider to save control state to a database, browser cookie, or other data storage method.

6.0 Summary

Creating a custom WebControl can be intimidating at first, but by extending existing controls they can be quite easy to create. The next time you need some "plumbing" code in your web page, ask yourself if you could encapsulate that logic in a custom web control. The PersistentPanelControl is a good example of how you can leverage an existing ASP.NET control to create a fully encapsulated, reusable, generic WebControl to make a page's state sticky. It is useful in many situations where your application needs to store a page's state across pages, but a permanent user profile object is not appropriate.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)