65.9K
CodeProject 正在变化。 阅读更多。
Home

创建持久化面板 Web 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (13投票s)

2010年3月1日

CPOL

8分钟阅读

viewsIcon

35423

downloadIcon

749

扩展 ASP.NET 内置的 Panel 控件以创建在页面之间保持的 Sticky 控件值

1.0 简介

“搜索页面看起来很棒,”我的客户说,“但是在我点击搜索结果并跳转到另一页之后,我希望我的搜索页面能够以我离开时的相同方式进行排序和过滤。”

无论需求如何规定,您的用户通常会期望您网站上的页面在访问之间保持其状态。这通常被称为使页面“Sticky”。本文将演示如何创建一个 PersistentPanelControl,使此任务变得微不足道。

2.0 使用代码

如果您对 Web 控件本身的构建不感兴趣,那么您只需下载软件包,然后编写三行代码即可将 DLL 整合到任何页面中。

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

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

PersistentPanelControl 将在任何回发事件中记住其内部服务器控件的状态,并在您返回时恢复这些控件的状态。

该控件的默认行为是将所有控件状态数据保存到 Session,但您可以通过实现 IControlPersistenceProvider 接口并在您网站的 *web.config* 文件中指定新的实现来覆盖此行为。

  <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 构建控件

3.1 整体设计

3.2.1 基类

许多初学者甚至中级 ASP.NET 开发人员都会对构建自己的 Web 控件感到畏惧。创建 HTML 渲染方法、子控件维护逻辑和可视化设计器代码的前景,常常会使开发人员放弃编写有用的 Web 控件,转而将本应由控件封装的功能写入 Web 页面的代码隐藏文件中。避免从头开始编写 Web 控件(即从 WebControlCompositeControl 基类开始)的开销的一种方法是,从现有的 ASP.NET Web 控件派生该控件,并让基控件处理大多数 ASP.NET 基础结构,开发人员只需编写您的自定义控件特有的功能。

经过一番思考,我们意识到我们的新控件中唯一的新需求是保存所有子控件在页面请求之间的状态。管理一组子控件、将它们渲染到页面、在 Visual Studio 设计器中显示控件及其子项以及与 ASP.NET 事件模型集成所需的所有逻辑已经包含在 ASP.NET 内置的 System.Web.UI.WebControls.Panel 类中!因此,继承自 Panel 允许我们编写一个几乎完全专注于在请求之间保存和恢复控件状态的业务逻辑的控件。

3.2.2 控件流程

在进一步进行设计之前,我们需要回答一个问题:控件应该何时 *保存* 其子控件的状态,又该何时 *恢复* 该状态?在所有可能的情况下,最常见的用法是当用户希望页面记住他们在执行搜索或过滤等操作时输入的那些值,并在返回页面时记住它们。我们可以使用 ASP.NET 的回发模型合理地区分这两种情况。当请求是回发时,控件将保存其所有子项的状态,并在请求不是回发时将它们恢复到已保存的状态。

3.3.3 使用 Provider 模式进行持久化

在创建我们的组件之前,我们还需要回答另一个问题:我们的控件应该在哪里持久化其状态?不幸的是,这个问题并不直接,并且可能根据不同的用例提供不同的答案。某些页面可能只需要记住当前会话的状态,并将该状态存储在 Session 对象中。有些可能无法使用 Session 对象,或者可能需要在会话之间存储数据。在这种情况下,我们可能希望使用浏览器 cookie 来存储可在会话之间持久化的数据。还有一些应用程序可能需要将数据持久化到数据库,以便用户即使从其他计算机登录也能保留其设置。

为了满足未来用户的潜在需求,该控件使用 Provider 模式,通过 IControlPersistenceProvider 接口来持久化实际的状态信息。

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

良好的设计和可用性原则要求所有组件控件都必须能够开箱即用,只需很少或无需配置。因此,我们的控件将附带一个默认实现,该实现将状态保存到 Session 对象中 - 这是一个简单的实现,可以满足大多数情况。

3.2 填充控件状态

PersistentPanelControl 的实际业务封装在填充包含所有子控件当前值(控件状态)的字典的方法中,以及从该字典恢复这些控件值的方法中。由于子控件可以嵌套在彼此内部,因此这些方法不像循环遍历 PersistentPanelControl 的所有子控件并记住它们的状态那么简单。两种方法都必须递归地填充每个子项的子项,以便保存整个控件树的状态。

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

如您所见,填充方法是递归的。对该方法的第一次调用将使用 PersistentPanelControl 实例本身和一个空的控件状态 Dictionary

将实际控件的状态保存到 Dictionary 的过程取决于我们要保存的控件类型。例如,对于 ListBox 控件,状态应包含所有选定的值作为 string 数组,而对于单值 ListControl 对象,则只包含单个选定的值 string

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

对于更复杂的控件,必须编写更复杂的逻辑来准确地保存状态。例如,GridView 控件的持久化机制必须同时保存排序表达式、排序方向和页索引。

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

最后,许多控件实现了 ITextControl 接口。将 ITextControl 检查放在列表的末尾是一种保存几种控件文本的方法,包括 TextBoxLabelButton,它们不匹配更具体的控件类型。

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

3.3 从控件状态恢复

将子控件恢复到其原始状态只是填充状态对象的逆过程。一个需要注意的点是,虽然大多数控件的状态都以控件 ID 作为键保存在 Dictionary 中,但像 GridView 这样的复杂对象会使用控件 ID 和前缀的组合,以多个键的形式保存其状态。

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 初始化持久化 Provider

该组件的整体设计要求一种可插入的方式来持久化控件的实际状态。该控件将在一个只读属性中封装此逻辑,该属性首先检查 *web.config* 文件中的 <appSettings> 部分,看是否已命名 Provider,如果未指定类型,则默认为 SessionControlPersistenceProvider。由于实现设置在 *web.config* 中,因此该控件可以安全地将 Provider 的实例保存到应用程序状态中。

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

默认实现,将控件状态保存到 Session 对象

/// <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 ASP.NET 页面生命周期中的执行顺序

理解 ASP.NET 页面生命周期 对于 Web 控件开发人员至关重要。控件在该生命周期中保存和恢复其子项状态对于正常运行至关重要。如果控件在其子项的事件处理程序触发之前保存其状态,那么它将错过那些处理程序中发生的任何更新。如果控件过早恢复状态,那么控件本身可能尚未初始化。

对于 PersistentPanelControl,保存控件状态的合适时机是在回发请求的 OnUnload 事件期间。此事件在所有控件渲染完毕后触发,并且控件在其生命周期内其值保证不会改变。恢复事件将在 OnInit 方法期间触发,该方法在所有子控件初始化后立即发生。

4.0 演示应用程序

该控件随附一个演示 Web 页面,位于 TestWebApp 项目中。该项目包含一个单独的 *Default.aspx* 页面,其中包含一个 PersistentPanelControl。面板包含各种类型的子项供用户尝试,包括 GridView。该 GridView 从 AdventureWorks2008 数据库 填充产品列表,该数据库是运行演示的必需项。

5.0 未来增强功能

我们刚刚介绍了一个非常基础的 PersistentPanelControl 实现。一个完整的实现将包括:

  • 支持更多类型的子控件
  • 一个属性,用于将命名子控件排除在持久化列表之外
  • 一个属性,允许用户逐个实例地覆盖 IControlPersistenceProvider 的默认实现
  • 描述性异常消息
  • 已发布的事件,允许开发人员在填充和恢复控件状态之前和之后编写代码。
  • IControlPersistenceProvider 的实现,用于将控件状态保存到数据库、浏览器 cookie 或其他数据存储方法。

6.0 总结

创建自定义 WebControl 最初可能令人望而生畏,但通过扩展现有控件,它们可以很容易地创建。下次当您需要在 Web 页面中进行一些“管道”代码时,问问自己是否可以将该逻辑封装在自定义 Web 控件中。PersistentPanelControl 是一个很好的例子,说明如何利用现有的 ASP.NET 控件来创建完全封装、可重用、通用的 WebControl,以使页面的状态保持“Sticky”。在您的应用程序需要跨页面存储页面状态,但永久用户配置文件对象不适用时,它在许多情况下都很有用。

© . All rights reserved.