1. 引言

在上个任务里,通过继承WebControl类创建了一个简单的星级控件,并且也可以设置字体边框等相关样式,但是需求马上又来了,如果我们想加入自定义的样式,例如希望文本可以自由显示到图案左边或下边,如下图所示,对于这样的要求怎么办?

2. 分析

看到上方的图形,很容易想到为第一个任务里的Star类添加一个属性,标识是在左面还是下面显示文本,这的确能够解决现有的问题,但如果过一阵子希望能够设置标签文本的颜色怎么办,很容易,再添加一个属性,具有敏锐眼光的读者一定想到,这不是解决问题的最好方法,因为可能会陆续提出新的样式的要求,那么比较合理的解决方法是单独添加一个样式属性,该属性持有一个样式集合,通过修改这个样式集合可以随时添加新的外观特征,最后再呈现控件的时候只需要使用第一个单元格应用该样式就可以了。

在.NET中,System.Web.UI.WebControls.Style类封装了Web服务器外观的属性,例如可以设置背景色、前景色或边框等,TableStyle类和TableItemStyle类均扩展了Style类,从它们的名字就可以想到TableStyle类用于设置表格控样的样式,而TableItemStyle类用于设置TableRow或TableCell的样式,它们之间的关系如图所示:

 

对于本次任务来说,我们要设置某个单元格的样式,并且添加了自定义特性,因此选择继承TableItemStyle类即可。

确定为Star类添加一个扩展了TableItemStyle类的属性后,样式的问题解决了,接下来要考虑的就是如何保存样式如性,像上次任务里直接保存到ViewState中吗,还是有更好的选择。

很显然,ViewState不能直接保存某个对象,需要进行序列化操作后才可以进行,默认情况下使用的是BinaryFormatter作为对象的序列化器,该类会把对象所有的成员全部序列化,而不管是公有的或私有的,这样做的话可能导致效率不是很高,为了自己控制属性的保存,需要继承IStateManager接口,该接口定义任何类为支持服务器控件的视图状态管理而必须实现的属性和方法,其定义的成员有:

成员描述
IsTrackingViewState属性 获取一个值,批示服务器控件是否正在跟踪其视图状态更改
LoadViewState方法 加载服务器控件以前保存的视图状态
SaveViewState方法 将服务器控件的视图状态保存到Object
TrackViewState方法 指示服务器控跟踪其视图状态更改

根据以上分析,为了实现需求需要定义一个继承自TableItemStyle并实现IStateManager接口的样式类TextItemStyle,在该类中增加DisplayTextAtBottom属性定义是否在底部显示注册文本,并在上次任务中开发的Star类里增加类型为TextItemStyle的属性以设置样式。

3. 实现

1. 在上次任务解决方案的ControlLibrary类库中添加TextItemStyle类,该类继承TableItemStyle类并实现了IStateManager接口:

public class TextItemStyle:TableItemStyle,IStateManager
{
}

2. 在该类中定义布尔类型的DisplayTextAtBottom属性,使用一个私有变量保存该属性设置;当该属性修改时需要通知Star类中的样式属性以应用新的样式,所以需要使用NotifyParentProperty属性修饰:

private bool _displayTextAtBottom;
  [NotifyParentProperty(true)]
public bool DisplayTextAtBottom
{
     get
     {
         return _displayTextAtBottom;
     }
     set
     {
         _displayTextAtBottom = value;
     }
}

3. 实现IStateManager中的IsTrackingViewState属性和TrackViewState方法,由于在这两个成员里不需要额外的动作,所以只需要简单的访问父类成员即可:

bool IStateManager.IsTrackingViewState
{
     get
     {
         return base.IsTrackingViewState;
     }
}
  void IStateManager.TrackViewState()
{
     base.TrackViewState();
}

4. 实现IStateManager接口SaveViewState方法以保存自定义属性:

object IStateManager.SaveViewState()
{
     Pair p = new Pair();
     p.First=base.SaveViewState();
     p.Second = _displayTextAtBottom;
      return p;
}
这里使用了System.Web.UI.Pair类用于保存两个相关的对象,与此类似的还是System.Web.UI.Triplet类,用于保存三个相关的对象。需要保存更多的数据时,可以考虑定义一个数组。

5. 实现IStateManager接口LoadViewState方法,从保存的视图数据中恢复必要的设置:

void IStateManager.LoadViewState(object savedState)
{
     if (savedState != null)
     {
         Pair p = (Pair)savedState;
          base.LoadViewState(p.First);
          _displayTextAtBottom = Convert.ToBoolean(p.Second);
     }
}
可以看到TextItemStyle中对接口的实现是通过接口名称.成员名称实现的,即所谓的显式继承,采用这种继承方向,不能直接通过类调用接口方法,而必须将类转换为接口类型后方可调用。

6. 在Start类中添加样式属性,并调用接口方法进行视图操作:

private TextItemStyle _textStyle;
  [PersistenceMode(PersistenceMode.InnerProperty)]
public TextItemStyle TextStyle
{
     get
     {
         if (_textStyle == null)
             _textStyle = new TextItemStyle();
          if (IsTrackingViewState)
             ((IStateManager)_textStyle).TrackViewState();
          return _textStyle;
     }
     set
     {
         _textStyle = value;
     }
}

该属性使用PersitenceMode标识,该类只是通知Visual Studio 2005,使用aspx源文件中的一个嵌入标记来永久存储该风格的内容。

7. 修改CreateControlHierarchy方法,根据样式属性设置决定是否创建新行:

protected virtual void CreateControlHierarchy()
{
     Table table = new Table();
     TableRow row = new TableRow();
     table.Rows.Add(row);
      TableCell stars = new TableCell();
     CreateStars(stars);
      TableCell comment = new TableCell();
     CreateComment(comment);
      if (TextStyle.DisplayTextAtBottom)
     {
         row.Cells.Add(stars);
          TableRow text = new TableRow();
         text.Cells.Add(comment);
         table.Rows.Add(text);
     }
     else
     { 
         row.Cells.Add(comment);
          row.Cells.Add(stars);
     }
      this.Controls.Add(table);
}
8. 最后修改呈现方法PrepareControlForRender,将样式应用到文字单元格上:
private void PrepareControlForReader()
{
     if (this.Controls.Count < 1)
         return;
      Table table = (Table)this.Controls[0];
      table.CellSpacing = 0;
     table.CellPadding = 0;
      TableCell cell = null;
      if (TextStyle.DisplayTextAtBottom)
     {
         cell = table.Rows[1].Cells[0];
     }
     else
     {
         cell = table.Rows[0].Cells[0];
     }
      cell.ApplyStyle(TextStyle);
}

9. 在网站中声明并定义控件:

<cc:StyleStar ID="star" runat="server" Score="4" Comment="Windows XP"> 
     <TextStyle ForeColor="Red" DisplayTextAtBottom="true" /> 
</cc:StyleStar>

浏览运行结果。

虽然我们在TextItemStyle类中定义了保存和读取视图状态的方法,但是在回发时能够正常工作吗,尝试在页面的PageLoad方法里设置样式的背景色为红色:

if (!IsPostBack)
     star.TextStyle.BackColor=System.Drawing.Color.Red;

接下来在页面中添加一个服务器端按钮,浏览页面并点击提交按钮,会出现怎样的结果?可以看到红色背景丢失了,这是由于虽然我们定义了样式类属性保存的方法,但它还没有真正的参与到页面视图读写过程中,为此,需要重写Start类的SaveViewState和LoadViewState方法,指定如何将数据保存到视图状态中,以及如何从视图状态中恢复。

protected override object SaveViewState()
{
     Pair p = new Pair();
     p.First=base.SaveViewState();
     p.Second = ((IStateManager)TextStyle).SaveViewState();
      return p;
}
  protected override void LoadViewState(object savedState)
{
     if (savedState != null)
     {
         Pair p = (Pair)savedState;
          base.LoadViewState(p.First);
         ((IStateManager)TextStyle).LoadViewState(p.Second);
     }
}

编译解决方案后再次预览页面,并点击提交按钮,可以看到.NET已经帮助我们正确的从视图状态中恢复数据了。

4. 总结

本次任务中为星形控件增加了自定义样式,并自定义视图操作状态以更高效的保存和读取相关数据。在定义属性时候使用了NotifyParentProperty特性和PersistenceMode特性分别用来在属性发生更改时通知父属性和将属性使用嵌入标记来保存。可能您会突然想到,如果用户将页面视图状态禁止后会产生什么样的结果,某些属性还能正确设置吗,下一次任务里我们将讨论这个问题。


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

前言

第一天 简单的星级控件 

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

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

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

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

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

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

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

第九天 自定义GridView

第十天 实现分页功能的DataList


全部源码下载

本系列文章PDF版本下载