1. 引言

以上几个任务里,我们创建了一些简单但很实用的自定义控件,但是它们只能按照固定的设置进行呈现,缺少一些关键的特征——数据绑定和有时为了更灵活的控制以支持模版设置。在ASP.NET数据绑定控件分为三种:

  • 简单数据绑定:简单数据绑定将一个对象与某个控件的属性绑定在一起。数据源只是绑定单个数据项,而不是绑定一个数据项列表。简单数据绑定使用数据绑定表达式完成,数据绑定表达式是用<%#...%>封装的任何可执行代码。
  • 列表控件:列表控件是通过一个固定不变的用户界面显示一个数据项列表的控件。常见的列表控件包含RadioButtonList控件、CheckBoxList控件和ASP.NET2.0中新引入的BulletedList控件。
  • 复杂数据绑定:复杂数据绑定控件通常是显示一组数据项的组合控件,它们有着灵活的呈现机制,例如GridView控件就是一个复杂数据绑定控件。

为了使星级控件在使用时能够通过某个数据源显示数据,需要使该控件拥有数据绑定的能力,使用时数据绑定方法代码看起来可能如下所示:

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);
      DataRow row = table.NewRow();
     row[0] = "Vista";
     row[1] = 3;
      table.Rows.Add(row);
      table.AcceptChanges();
      star.DataSource = table;
     star.DataTextField = "Comment";
     star.DataValueField = "Score";
     star.DataBind();
}

2. 分析

在开始列表控件之前再来讨论一下简单数据绑定,前几次开发的星级控件就是一个简单数据绑定控件,我们直接可以为他的某个属性使用数据绑定表达式赋值,例如在StartTest.aspx中编写如下代码使用自定义控件:

<cc:Star ID="star" runat="server" Score="4" Comment="<%#DateTime.Now%>" Font-Size="12px" />

在页面中预览——很不巧,当前日期并没有显示在页面上,这是由于在页面中定义的任何数据绑定表达式,只有在调用DataBound方法之后才会进行计算。我们有多种选择,既可以调用页面对象(Page)的DataBind方法,也可以调用具体控件上的DataBind方法,实际上,如果调用页面对象上的DataBind方法,它将递归的调用页面中定义的所有控件上的DataBind方法。这就意味着,如果页面上使用了多个数据绑定表达式,最好还是通过调用Page.DataBind方法执行数据绑定。

如果想了解在数据绑定时底层究竟执行了什么操作,可以打开ASP.NET的调试功能,并改变临时文件目录,修改web.config中complcation配置节如下所示:

<compilation debug="true" tempDirectory="c:\web">
     …… ……
</complation>

以上配置启动了调试并将临时目录设置为C:\Web(如果不设置tempDirectory属性临时文件将放置在默认%Windows%\Microsoft.NET\Framework\%Version%\Temporary ASP.NET Files目录下)。在浏览器预览StarTest.aspx页面后可以在临时目录中发现StarTest两个分部类定义,其中一个定义了后置代码,另一个用于生成ASPX页面。在生成ASPX页面的类文件中可以找到@__BuildControlstar方法(实际上还有一系列的@__BuildControlXXX方法,XXX对应于控件的ID),在该方法中包含如下代码:

@__ctrl.DataBinding += new System.EventHandler(this.@__DataBindingstar);
相应的,可以在此文件中找到@__DataBindingstar方法,该方法如下所示:
public void @__DataBindingstar(object sender, System.EventArgs e) {
     ControlLibrary.Star dataBindingExpressionBuilderTarget;
     System.Web.UI.Page Container;
     dataBindingExpressionBuilderTarget = ((ControlLibrary.Star)(sender));
     Container = ((System.Web.UI.Page)(dataBindingExpressionBuilderTarget.BindingContainer));
          #line 13 "E:\Documents and Settings\holywolf\My Documents\Books\????\??????????\ControlSolution\Web\StarTest.aspx"
     dataBindingExpressionBuilderTarget.Comment = System.Convert.ToString(DateTime.Now, System.Globalization.CultureInfo.CurrentCulture);
          #line default
     #line hidden
}

如果数据绑定表达式不匹配期望的类型,则通常会得到一个编译错误,然而,如果期望的类型是string,则解析器通过Convert.ToString方法实现标准转换。

接下来讨论如何使星级控件拥有数据绑定的能力,首先我们来观查ASP.NET2.0中数据绑定类层次结构:

可以看到,所有类都继承自BaseDataBoundControl类,该类是ASP.NET2.0数据绑定控件的基类。BasedataBoundControl类上定义了如何进行数据绑定和如何验证所绑定的数据的方式,并且该类上还定义了两个数据源属性:用于可枚举数据的DataSource属性和用于数据源控件的DataSourceID属性。

DataSource属性接受一个实现了IEnumerable接口(例如集合)或IListSource接口(例如DataTable)的对象,设置该属性后,必须调用该控件或页面的DataBind方法才会真正的将数据填充的控件中。

DataBoundControl类继承自BaseDataBoundControl类,该类中提供了一个重要的方法PerformDataBinding方法,在编写派生自DataBoundControl类的自定义控件时,需要编写此方法实现以加载数据,该方法定义如下:

protected virtual void PerformDataBinding(IEnumerable data)
{
     …… ……
}

在创建自定义数据绑定控件时,一般需要处理以下内容:

  • 定义相关的DataXXXField指定绑定到数据源映射
  • 添加数据项属性并手动管理视图状态以获得更高的效率
  • 重载PerformDataBinding方法从数据源中读取数据并缓存到数据项中
  • 根据数据项中保存的值正确的呈现控件

根据以上分析,可以在修改原有星级控件,使之继承自DataBoundControl以获得从数据源读取数据的能力。

3. 实现

3.1 向解决方案中ControlLibrary类库中添加StarDataItem类作为保存数据的数据项,为了能够管理视图状态,该类实现了IStateManager接口:

using System;
using System.Web.UI;
  namespace ControlLibrary
{
     public class StarDataItem:IStateManager
     {
     }
}

3.2 在该类中添加得分和注释属性并添加构造函数:

public string Comment
{
     get;
     set;
}
  public int Score
{
     get;
     set;
}
  public StarDataItem()
{
     Comment = string.Empty;
     Score = 0;
}
  public StarDataItem(string comment, int score)
{
     Comment = comment;
     Score = score;
}

3.3 实现IStateManager接口相关方法以管理视图状态:

private bool _mark;
  public bool IsTrackingViewState
{
     get
     {
         return _mark;
     }
}
  public object SaveViewState()
{
     Pair p = new Pair(Comment, Score);
      return p;
}
  public void LoadViewState(object savedState)
{
     if (savedState != null)
     {
         Pair p = (Pair)savedState;
          Comment = Convert.ToString(p.First);
         Score = Convert.ToInt32(p.Second);
     }
}
  public void TrackViewState()
{
     _mark = true;
}

3.4 添加DataBoundStar类,该类继承了DataBoundControl类:

using System;
using System.Collections;
using System.Web.UI;
using System.Web.UI.WebControls;
  namespace ControlLibrary
{
     public class DataBoundStar : DataBoundControl
     {
     }
}

3.5 在该类中定义数据项属性(StarDataItem类型):

private StarDataItem _data;
  public StarDataItem DataItem
{
     get
     {
         if (_data == null)
             _data = new StarDataItem();
          if (IsTrackingViewState)
             _data.TrackViewState();
          return _data;
     }
}

3.6 为类增加DataTextField和DataValueField属性定义与数据源的映射:

public virtual string DataValueField
{
     get
     {
         object o = ViewState["DataValueField"];
         return o == null ? string.Empty : (string)o;
     }
     set
     {
         ViewState["DataValueField"] = value;
     }
}
  public virtual string DataTextField
{
     get
     {
         object o = ViewState["DataTextField"];
          return o == null ? string.Empty : (string)o;
     }
     set
     {
         ViewState["DataTextField"] = value;
     }
}

3.7 重写PerformDataBinding方法,根据映射从数据源中读取数据填充到数据项中:

protected override void PerformDataBinding(IEnumerable data)
{
     if (data == null)
         return;
      IEnumerator e = data.GetEnumerator();
     e.MoveNext();
      if (!string.IsNullOrEmpty(DataTextField))
         DataItem.Comment = 
Convert.ToString(DataBinder.GetPropertyValue(e.Current, DataTextField));
      if (!string.IsNullOrEmpty(DataValueField))
         DataItem.Score = 
Convert.ToInt32(DataBinder.GetPropertyValue(e.Current, DataValueField));
}

该方法接收IEnumerable类型的参数用于迭带访问数据源中的数据,在读取数据源中的数据时使用了DataBinder类,该类上有一个实用的GetPropertyValue方法,用于根据属性反射的读取数据源中的值,此处我们使用该方法并传递了DataTextField和DataValueField以读取注释和评分。

3.8 重写CreateChildControls方法,调用CreateControlHierarchy方法以创建子控件层次:

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

3.9 如上所示在创建子控件时调用了CreateComment和CreateStars方法,用于创建注释和星形图案(仍然使用了资源文件),并且相关数据均由DataItem属性读取,以下是这两个方法的实现:

private void CreateComment(TableCell cell)
{
     Label lbl = new Label();
     lbl.Text = DataItem.Comment;
      cell.Controls.Add(lbl);
}
  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.Position, "relative");
     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 = DataItem.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);
}

3.10 重写SaveViewState和LoadViewState方法将DataItem数据项保存到视图状态中并能够正确的恢复:

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

3.11 最后重写Render方法呈现控件:

protected override void Render(HtmlTextWriter writer)
{
     PrepareControlForReader();
      base.Render(writer);
}
  private void PrepareControlForReader()
{
     if (this.Controls.Count < 1)
         return;
      Table table = (Table)this.Controls[0];
      table.CellSpacing = 0;
     table.CellPadding = 0;
}

3.12 在Web网站中添加页面,声明并定义自定义控件:

<cc:DataBoundStar ID="star" runat="server" />

3.13 编写类似于本次任务开始的代码以执行数据绑定,并在浏览器预览结果。

4. 总结

本次任务里我们再次为星级控件增加了新的特性——数据绑定,为了实现这个目的,自定义控件继承了DataBoundControl类,实现了其中的一个重要方法PerformDataBinding,通过该方法的IEnumerable参数迭代访问数据源,并调用DataBinder.GetPropertyValue方法获取数据源映射的值,最后为了更合理的保存数据,定义了StarDataItem类作为自定义控件的属性。在下次任务里,我们将一起开发一个列表控件。

附本文中使用到的背景图片:


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

前言

第一天 简单的星级控件 

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

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

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

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

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

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

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

第九天 自定义GridView

第十天 实现分页功能的DataList


全部源码下载

本系列文章PDF版本下载