1. 引言

正如在前两个任务中所看到的,我们使用视图(ViewState)保存自定义控件属性,ViewState实际上是一个StateBag对象,开发人员使用键/值的方法向视图中保存或读取设置,最终发送给用户的HTML页面中会包含一个隐藏域,该隐藏域中保存了经过序列化后的值。如果过分使用视图的话,会导致页面急剧增大,虽然现在网络带宽已经不是限制条件,但这仍然是一个不太好的设计,因此开发人员有时会禁用视图状态。

对于自定义控件来说,如果禁用视图状态可能导致控件不能够正常工作,读者可以使用第一次任务里开发的星级控件,禁用页面的视图状态(设置Page指令EnableViewState属性为false)并在在页面的加载(Page_Load)事件里输入如下代码以设置分数:

protected void Page_Load(object sender, EventArgs e)
{
     if (!IsPostBack)
         star.Score = 4;
}

并在ASPX页面上放置一个服务器端按钮以引起回发事件,预览该页面并点击按钮,会发生什么情况?由于提交按钮引起回发IsPostBack属性为true,即不会执行分数设置操作,并且因为禁用了视图状态,在页面生命周期里运行时无法从视图状态中恢复分数,所以会导致没有分数:

2. 分析

在.NET1.X版本里,视图状态被作为一个整体,要不允许要么禁止,对于控件开发人员来说非常不方便,与控件相关的数据放置在ViewState里,一旦被禁用后,可能就会出问题。比较幸运的是,对于这种情况,微软及时做出了反应。在ASP.NET2.0里,出现了一个新的概念——控件状态。

控件状态实际上是一种特殊的视图状态,它仍然保存在客户端的隐藏域中,但是它并不会受视图状态启用/禁用的影响,也就是说,即使将ViewState禁用,运行时仍然能正确的恢复在控件状态中保存的数据。

为了使用控件状态,仅仅需要做额外的几个工作:

  1. 向页面注册使用控件状态
  2. 在控件状态保存事件(Control类的SaveControlState方法)中保存相关数据
  3. 在控件状态读取事件(Control类的LoadControlState方法)中读取保存的数据

需要说明的一点是,正因为控件状态始终都会发送到客户端,所以将大量数据保存到控件状态中显然不是一件太好的事件,始终应该只保存影响控件使用的关键的核心的数据。

为了能够使页面视图状态被禁止后控件仍然能够正常使用,将星级控件的得分保存在控件状态中。

3. 实现

为了组织文件方便,创建一个新的使用控件状态的类。

1. 在自定义控件解决方案的ControlLibrary类库中添加StateStar类,像第一次任务那样定义属性和创建控件层次并呈现:

using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;
  namespace ControlLibrary
{
     public class StateStar : WebControl
     {
         private int _score;
          [DefaultValue(0)]
         public int Score
         {
             get
             {
                 return _score;
             }
             set
             {
                 _score = value;
             }
         }
          public string Comment
         {
             get
             {
                 object obj = ViewState["Comment"];
                 return obj == null ? string.Empty : Convert.ToString(obj);
             }
             set
             {
                 ViewState["Comment"] = value;
             }
         }
          protected override void CreateChildControls()
         {
             base.CreateChildControls();
              CreateControlHierarchy();
         }
          protected virtual void CreateControlHierarchy()
         {
             Table table = new Table();
             TableRow row = new TableRow();
             table.Rows.Add(row);
             TableCell comment = new TableCell();
             CreateComment(comment);
             row.Cells.Add(comment);
              TableCell stars = new TableCell();
              CreateStars(stars);
              row.Cells.Add(stars);
              this.Controls.Add(table);
         }
          /// <summary>
         /// 向单元格中创建注释标签
         /// </summary>
         /// <param name="cell">单元格对象</param>
         private void CreateComment(TableCell cell)
         {
             Label lbl = new Label();
             lbl.Text = Comment;
              cell.Controls.Add(lbl);
         }
          /// <summary>
         /// 向单元格中创建星形图案
         /// </summary>
         /// <param name="cell"></param>
         private void CreateStars(TableCell cell)
         {
             string starPath =
              Page.ClientScript.GetWebResourceUrl(this.GetType(), 
             "ControlLibrary.Image.stars.gif");
              Panel panBg = new Panel();
             panBg.Style.Add(HtmlTextWriterStyle.Width, "80px");
             panBg.Style.Add(HtmlTextWriterStyle.Height, "16px");
             panBg.Style.Add(HtmlTextWriterStyle.TextAlign, "left");
             panBg.Style.Add(HtmlTextWriterStyle.Overflow, "hidden");
             panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath);
             panBg.Style.Add("background-position", "0px -32px");
             panBg.Style.Add("background-repeat", "repeat-x");
              cell.Controls.Add(panBg);
              Panel panCur = new Panel();
             string width = Score * 16 + "px";
             panCur.Style.Add(HtmlTextWriterStyle.Width, width);
             panCur.Style.Add(HtmlTextWriterStyle.Height, "16px");
             panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage,starPath );
             panCur.Style.Add("background-position", "0px 0px");
             panCur.Style.Add("background-repeat", "repeat-x");
              panBg.Controls.Add(panCur);
         }
          protected override void Render(HtmlTextWriter writer)
         {
             PrepareControlForReader();
              base.Render(writer);
         }
     }
}

不要被这一大堆没有注释的代码吓倒,这只是最简单的星级控件的实现,并且为了方便某些读者不需要查询以前的资料就可以快速编辑一个控件。

2. 为了使用控件状态,向页面进行注册,而且由于在回发事件的过程中,控件状态的注册无法在请求之间进行传递,因此使用控件状态的自定义服务器控件必须对每个请求进行注册,所以重写OnInit方法:

protected override void OnInit(EventArgs e)
{
     base.OnInit(e);
      Page.RegisterRequiresControlState(this);
}

3. 重写SaveControlState方法,该方法保存自页回发到服务器后发生的任何服务器控件状态更改,该方法的返回值标识了服务器控件的当前状态,因此只需要将星级控件的得分保存到返回的object中即可(为了保持一致,仍然需要调用父类的实现):

protected override object SaveControlState()
{
     object[] o = new object[2];
      o[0]=base.SaveControlState();
     o[1] = _score;
      return o;
}

4. 重写LoadControlState方法,该方法从保存的控件状态中恢复数据,它有一个参数表示保存的数据,该参数是object类型,需要根据保存时使用的类型进行正确的转换:

protected override void LoadControlState(object savedState)
{
     if (savedState != null)
     {
         object[] o = (object[])savedState;
          base.LoadControlState(o[0]);
         _score = Convert.ToInt32(o[1]);
     } 
}

5. 完成以后步骤后在Web网站中创建一个ASPX页面声明并定义StateStar控件,将视图禁用并添加一个引起回发的服务器端按钮,像一开始那样在页面加载事件里设置得分,再次预览页面并点击按钮,可以看到,得分仍然能够正确的显示出来:

4. 总结

在本次任务里,我们使用控件状态保存了自定义控件的得分,使得在页面视图被禁用时自定义控件仍能正常用行。为了使用控件状态,调用了Page.RegisterRequiresControlState方法,并重调了SaveControlState和LoadControlState方法,最后需要再次说明的是控件状态只应该用来保存关键的数字。

到今天为止,我们已经掌握了开发简单的自定义控件的方法,并且围绕一个星级控件讨论了它的组织、呈现、自定义样式、控件状态和如何使用特性进行定义。在下一次任务里,我们将一起开发一个具有一些挑战性的复杂控件。


ASP.NET自定义控件系列文章

前言

第一天 简单的星级控件 

第二天 带有自定义样式的星级控件

第三天 使用控件状态的星级控件

第四天 折叠面板自定义控件

第五天 可以评分的星级控件

第六天 可以绑定数据源的星级控件

第七天 开发具有丰富特性的列表控件

第八天 显示多个条目星级评分的数据绑定控件

第九天 自定义GridView

第十天 实现分页功能的DataList


全部源码下载

本系列文章PDF版本下载