1. 引言

前几次任务里我们开发的星级控件只能显示一个条目的评分,在现实生活中,经常会遇到需要向用户展示一系列数据的评分状态,例如下图所示:

本次任务里,我们将一起开发这样一个控件。

2. 分析

通过上图可以看到,该自定义控件是一系列数据评分等级的列表,很显然需要作为一个数据绑定控件来实现才可以灵活的显示多条数据,并且在该列表上方显示了标题和当前的日期,为了允许用户灵活的定义标题和二级标题(当前日期),有必要引入模版的概念,由用户编辑模版,最终按照模版内容显示。如此看来再使用DataBoundControl作为自定义控件的基类就不太适合了,因为我们要在该控件中包含多个子控件,那么我们应该选择哪个类作为基类?

回忆一下第六天的任务中数据绑定控件的类关系图,其中有一个继承自DataBoudControl类的CompositeDataBoundControl类,该类是.NET Framework 2.0中新增的一个类,用作绑定到数据源中的数据服务器控件的基类,该定义定义如下:

public abstract class CompositeDataBoundControl : DataBoundControl, INamingContainer

可以看到,该在只是在继承DataBoundControl类的基础上实现了INamingContainer,这意味着该类所包含的子控件都会生成唯一的ID属性。

但是CompositeDataBoundControl是如何实现数据绑定的呢,换句话说,如果某一个页面包含了一个复杂数据绑定控件,在某一个服务器端控件引起回发后,如何确保数据绑定控件能正确的被填充呢?按照设计,ASP.NET中的数据绑定组合控件只能从数据绑定中获取数据,并且不会缓存任何绑定的数据,因此,需要提供一个特殊的方法来处理回发事件。

再次回顾一下DataBoundControl这个类,我们来分析一下数据是如何被显示出来的。在DataBoundControl类上重载了BaseDataBoundControl类上定义的PerformSelect方法,该方法如下所示:

protected override void PerformSelect()
{
     if (this.DataSourceID.Length == 0)
     {
         this.OnDataBinding(EventArgs.Empty);
     }
     DataSourceView data = this.GetData();
     this._arguments = this.CreateDataSourceSelectArguments();
     this._ignoreDataSourceViewChanged = true;
     base.RequiresDataBinding = false;
     this.MarkAsDataBound();
     data.Select(this._arguments, new
     DataSourceViewSelectCallback(this.OnDataSourceViewSelectCallback));
}

该方法的关键在于不论数据源是通过DataSource属性指定还是通过DataSourceID属性指定,最终都是通过数据源视图对象(DataSourceView)来获取数据,在获取数据源视图之后调用了该对象的Select方法,以获取可枚举的数据集合(不论数据源是以何种方式提供),该方法还接收多个输入参数和回调函数。

到现在为止,我们已经可以通过为控件设置数据源数据并且通过数据源视图找到要读取的数据,那么接下来要考虑的是,如果访问这个可绑定的数据集合以将其显示到页面上?

答案是在数据源视图上处理Select语句操作结果的回调函数最后会调用一个可重载的受保护方法PerformDataBinding方法,该方法定义如下:

protected internal virtual void PerformDataBinding (
     IEnumerable data
)

PerformDataBinding方法接收一个IEnumerable类型的对象以访问返回的数据列表,我们可以利用该对象迭代访问数据并根据需要创建控件的内部结构最终呈现给用户。

对于数据绑定组合控件,和独自处理数据绑定的控件不同,它不将数据保存到视图中,而是交由各个子控件处理,此时,就不能通过简单的重写CreateChildControls方法实现了,通过.NET Reflection工具观察CompositeDataBindControl类上该方法的实现:

protected internal override void CreateChildControls()
{
     this.Controls.Clear();
     object obj2 = this.ViewState["_!ItemCount"];
     if ((obj2 == null) && base.RequiresDataBinding)
     {
         this.EnsureDataBound();
     }
     if ((obj2 != null) && (((int)obj2) != -1))
     {
         DummyDataSource dataSource = new DummyDataSource((int)obj2);
         this.CreateChildControls(dataSource, false);
         base.ClearChildViewState();
     }
}

CreateChildControls方法有两种工作模式:绑定模式和非绑定模式。

在绑定模式下,将会正常创建控件树,具体来说会在执行PerformDataBinding方法中调用重载的CreateChildControls方法,以手动的实现数据控件层次的创建;

protected internal override void PerformDataBinding(IEnumerable data)
{
     base.PerformDataBinding(data);
     this.Controls.Clear();
     base.ClearChildViewState();
     this.TrackViewState();
     int num = this.CreateChildControls(data, true);
     base.ChildControlsCreated = true;
     this.ViewState["_!ItemCount"] = num;
}
而在非绑定模式中,将从CreateChildControls方法中调用带有两个参数的重载:
DummyDataSource dataSource = new DummyDataSource((int) obj2);
this.CreateChildControls(dataSource, false);
base.ClearChildViewState();

在这种情况下,传递给CreateChildControls方法的第二个参数为false,这表示不会向控件层次添加任何数据。ASP.NET回发机制确保每一个子控件正确的从视图中恢复自己的值。

接下来观察在CompositeDataBindControl类上定义的CreateChildControls方法的重载:

protected abstract int CreateChildControls (
     IEnumerable dataSource,
     bool dataBinding
)

该方法的参数说明如下:

属性描述

Attribute

在最终的标记中,该属性永久存储为一个已编码的HTML特性
EncodedInnerDefaultProperty 该属性存储为该控件的内部文本。该属性的值是编码的HTML,只能将字符串赋给该属性
InnerDefaultProperty 永久存储为控件中的内部文本的属性并且是元素的默认属性,只有一个属性能够指定为默认属性
InnerProperty 该属性永久存储为控件中的一个嵌入标记,这是使用模版和风格的复杂对象常用的一个属性。

模版属性示例如下所示:

private ITemplate _titleTemplate;
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(TitleTemplateContainer))]
public ITemplate TitleTemplate
{
     get
     {
         return _titleTemplate;
     }
     set
     {
         _titleTemplate = value;
     }
}

3. 实现

3.1 在解决方案ControlLibrary类库中创建BarChartItem类用于表示控件项:

using System;
using System.Collections.ObjectModel;
using System.Web.UI;
using System.Web.UI.WebControls;
  public class BarChartItem:TableRow
{
     public BarChartItem(BarChartItemType itemType)
     {
         ItemType = itemType;
     }
      public BarChartItemType ItemType
     {
         get;
         set;
     }
      public object DataItem
     {
         get;
         set;
     }
}

3.2 在BarChartItem类中使用了BarChartItemType枚举以标识当前项的类型(类似于GridView中的标题、交替行等),编写该枚举实现:

public enum BarChartItemType
{
     Title, //标题
     SubTitle, //二级标题
     Item, //数据项
     Footer //页脚
}

3.3 定义该控件项的集合类BarChartItemCollection:

public class BarChartItemCollection : Collection<BarChartItem>
{
}
3.4 为了能够在创建项和绑定数据后暴露出相应的事件中使用户能够访问数据项信息,编写自定义事件参数类:
public class BarChartItemEventArgs : EventArgs
{
     private BarChartItem _item;
      public BarChartItemEventArgs(BarChartItem item)
     {
         _item = item;
     }
      public BarChartItem Item
     {
         get
         {
             return _item;
         }
     }
}

3.5 定义模版容器类

public class TitleTemplateContainer : WebControl, INamingContainer
{
     private BarChart _parent;
      public TitleTemplateContainer(BarChart parent)
     {
         _parent = parent;
     }
      public string Title
     {
         get
         {
             return _parent.Title;
         }
     }
      public string SubTitle
     {
         get
         {
             return _parent.SubTitle;
         }
     }
      public BarChart BarChart
     {
         get
         {
             return _parent;
         }
     }
}

3.6 在ControlLibrary类库中创建BarChar类并定义私有变量,该类继承自CompositeDataBoundControl类:

public class BarChart : CompositeDataBoundControl
{
     public event EventHandler<BarChartItemEventArgs> BarChartItemCreated;
     public event EventHandler<BarChartItemEventArgs> BarChartItemDataBound;
     private BarChartItemCollection _items;
     private ITemplate _titleTemplate;
     private TitleTemplateContainer _titleTemplateContainer;
     private TableItemStyle _titleStyle;
     private TableItemStyle _subtitleStyle;
     private TableItemStyle _labelStyle;
}

3.7 定义相关属性:

public string DataTextField
{
     get
     {
         object o = ViewState["DataTextField"];
         return o == null ? string.Empty : (string)o;
     }
     set
     {
         ViewState["DataTextField"] = value;
     }
}
  public string DataValueField
{
     get
     {
         object o = ViewState["DataValueField"];
         return o == null ? string.Empty : (string)o;
     }
     set
     {
         ViewState["DataValueField"] = value;
     }
}
  public BarChartItemCollection Items
{
     get
     {
         if (_items == null)
         {
             _items = new BarChartItemCollection();
         }
         return _items;
     }
}
  [Browsable(false)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(TitleTemplateContainer))]
public ITemplate TitleTemplate
{
     get
     {
         return _titleTemplate;
     }
     set
     {
         _titleTemplate = value;
     }
}
  public string Title
{
     get
     {
         object o = ViewState["Title"];
         return o == null ? string.Empty : (string)o;
     }
     set
     {
         ViewState["Title"] = value;
     }
}
  public string SubTitle
{
     get
     {
         object o = ViewState["SubTitle"];
         return o == null ? string.Empty : (string)o;
     }
     set
     {
         ViewState["SubTitle"] = value;
     }
}
  [PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[Description("标题样式")]
public virtual TableItemStyle TitleStyle
{
     get
     {
         if (_titleStyle == null)
         {
             _titleStyle = new TableItemStyle();
         }
          if (IsTrackingViewState)
             ((IStateManager)_titleStyle).TrackViewState();
          return _titleStyle;
     }
}
  [PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[Description("二级标题样式")]
public virtual TableItemStyle SubTitleStyle
{
     get
     {
         if (_subtitleStyle == null)
         {
             _subtitleStyle = new TableItemStyle();
         }
          if (IsTrackingViewState)
             ((IStateManager)_subtitleStyle).TrackViewState();
          return _subtitleStyle;
     }
}
  [PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[Description("标签样式")]
public virtual TableItemStyle LabelStyle
{
     get
     {
         if (_labelStyle == null)
         {
             _labelStyle = new TableItemStyle();
             // Can initialize the style HERE 
         }
          if (IsTrackingViewState)
             ((IStateManager)_labelStyle).TrackViewState();
          return _labelStyle;
     }
}

3.8 定义事件响应方法:

protected virtual void OnBarChartCreated(BarChartItemEventArgs e)
{
     if (BarChartItemCreated != null)
         BarChartItemCreated(this, e);
}
  protected virtual void OnBarChartDataBound(BarChartItemEventArgs e)
{
     if (BarChartItemDataBound != null)
         BarChartItemDataBound(this, e);
}

3.9 编写创建子控件层次方法:

protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
     return CreateControlHierarchy(dataSource, dataBinding);
}
  private int CreateControlHierarchy(IEnumerable dataSource, bool dataBinding)
{
     if (dataSource == null)
     {
         RenderEmptyControl();
         return 0;
     }
      Table t = new Table();
     Controls.Add(t);
      //创建标题
     CreateTitle(t);
      //创建二级标题
     CreateSubTitle(t);
      //创建数据项
     int totalItems = CreateAllItems(t, dataSource, dataBinding);
      return totalItems;
}

3.10 编写创建控件方法:

private void RenderEmptyControl()
{
     LiteralControl lbl = new LiteralControl("??????");
     Controls.Add(lbl);
}
  private void CreateTitle(Table t)
{
     BarChartItem item = new BarChartItem(BarChartItemType.Title);
     t.Rows.Add(item);
      TableCell cell = new TableCell();
     item.Cells.Add(cell);
      cell.ColumnSpan = 2;
      if (TitleTemplate != null)
     {
         _titleTemplateContainer = new TitleTemplateContainer(this);
         TitleTemplate.InstantiateIn(_titleTemplateContainer);
         cell.Controls.Add(_titleTemplateContainer);
     }
     else
     {
         cell.Text = Title;
     }
      item.DataBind();
}
  private void CreateSubTitle(Table t)
{
     BarChartItem item = new BarChartItem(BarChartItemType.SubTitle);
      t.Rows.Add(item);
     TableCell cell = new TableCell();
     item.Cells.Add(cell);
      cell.ColumnSpan = 2;
     cell.Text = SubTitle;
}
  private int CreateAllItems(Table t, IEnumerable data, bool useDataSource)
{
     int itemCount = 0;
      Items.Clear();
      foreach (object o in data)
     {
         BarChartItemType itemType = BarChartItemType.Item;
         BarChartItem item = CreateBarChartItem(t, itemType, o, useDataSource);
          _items.Add(item);
          itemCount++;
     }
      return itemCount;
}
  private BarChartItem CreateBarChartItem(Table t, BarChartItemType itemType, object dataItem, bool useDataSource)
{
     BarChartItem item = new BarChartItem(itemType);
      TableCell labelCell = CreateLabelCell(item);
     TableCell valueCell = CreateScoreCell(item);
      BarChartItemEventArgs argsCreated = new BarChartItemEventArgs(item);
      OnBarChartCreated(argsCreated);
      t.Rows.Add(item);
      if (useDataSource)
     {
         item.DataItem = dataItem;
          BindLabelCell(labelCell, dataItem);
         BindValueCell(valueCell, dataItem);
          BarChartItemEventArgs argsData = new BarChartItemEventArgs(item);
          OnBarChartDataBound(argsData);
     }
      return item;
}
  private TableCell CreateLabelCell(BarChartItem item)
{
     TableCell cell = new TableCell();
          item.Cells.Add(cell);
      return cell;
}
  private TableCell CreateScoreCell(BarChartItem item)
{
     TableCell cell = new TableCell();
     item.Cells.Add(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(); 
          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);
           return cell;
}
  private void BindLabelCell(TableCell cell, object dataItem)
{
     if (!string.IsNullOrEmpty(DataTextField))
     {
         string txt = Convert.ToString(DataBinder.GetPropertyValue(dataItem, DataTextField));
         cell.Text = txt;
     }
}
  /// <summary>
/// ????????????
/// </summary>
/// <param name="cell"></param>
/// <param name="dataItem"></param>
private void BindValueCell(TableCell cell, object dataItem)
{
     Panel panCur = (Panel)cell.Controls[0].Controls[0];
     object o = null;
      if (!String.IsNullOrEmpty(DataValueField))
         o = DataBinder.GetPropertyValue(dataItem, DataValueField);
     else
         return;
     int score = Convert.ToInt32(o);
      string width = score * 16 + "px";
     panCur.Style.Add(HtmlTextWriterStyle.Width, width);
}
  protected override void Render(HtmlTextWriter writer)
{
     PrepareControlForRender();
     base.Render(writer);
}
  public void PrepareControlForRender()
{
     if (Controls.Count != 1)
         return;
      Table t = (Table)Controls[0];
     t.CopyBaseAttributes(this);
     if (ControlStyleCreated)
         t.ApplyStyle(ControlStyle);
      t.Rows[0].Cells[0].MergeStyle(TitleStyle);
      t.Rows[1].Cells[0].MergeStyle(SubTitleStyle);
      // Apply style to the labels that render team names
     for (int i = 2; i < Items.Count; i++)
     {
         // Style team labels
         t.Rows[i].Cells[0].ApplyStyle(LabelStyle);
     }
}

3.10 在页面中声明并定义自定义控件,预览结果。

<cc:BarChart ID="bar" runat="server" Font-Size="12px" Title="????????">
     <TitleTemplate>
         <%# Container.Title %>
             <small>(<%# DateTime.Now.ToString() %>)</small>
     </TitleTemplate>
</cc:BarChart>

3.11 编写后置代码,预览结果:

protected void Page_Load(object sender, EventArgs e)
{
     if (!IsPostBack)
         this.BindData();
}
  private void BindData()
{
     DataTable table = new DataTable();
       DataColumn col = new DataColumn("Comment", typeof(string));
     table.Columns.Add(col);
     col = new DataColumn("Score", typeof(int));
     table.Columns.Add(col);
      Random ran=new Random();
      for (int i = 0; i < 10; i++)
     {
         DataRow row = table.NewRow();
         int num= ran.Next(0, 6);
         row[0] = "数据 "+i+" , "+num+" 星";
          row[1] = num;
          table.Rows.Add(row);
     }
          table.AcceptChanges();
      bar.DataSource = table;
     bar.DataTextField = "Comment";
     bar.DataValueField = "Score";
      bar.DataBind();
}

4. 总结

复杂绑据绑定控件可以通过扩展CompositeDataBindControl类实现,该类有一个特殊的CreateChildControls方法实现数据的绑定和恢复,在下一次任务里,我们将尝试扩展DataList和GridView以实现自定义分页控件。


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

前言

第一天 简单的星级控件 

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

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

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

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

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

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

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

第九天 自定义GridView

第十天 实现分页功能的DataList


全部源码下载

本系列文章PDF版本下载