Creating a Persistent Panel Web Control






4.86/5 (13 votes)
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.
<%@ 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.
<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.
/// <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.
/// <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 string
s, but only a single selected value string
for single value ListControl
objects.
//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:
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.
//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.
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.
/// <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:
/// <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.