1. 引言

在上次任务里,我们为星级控件增加了数据绑定的特性,但是在实际运用中还会产生更多的需求,例如用户可能希望创建一个课程列表(如图1):

或者在数据项比较多的时候,能够手动控制数据的排列方式(图2)

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

2. 分析

以上两个图例中显示的都是列表控件,在ASP.NET2.0中ListControl类是列表控件的父类,通过上次任务的分析可以了解CheckBoxList、RadioButtonList和DropDownList等控件均继承自ListControl类,这些列表控件都是对于每一个数据项重复的应用一个样式,全如CheckBoxList对于每个列表项显示一个复选框,而RadioButtonList对于每个列表项显示一个单元框。实际上,列表控件中的每一个列表项都是ListItem类型的,而且为了显示一个列表,列表控件常常拥有每一个元素都是ListItem类型的集合,也就是我们经常用到的Items属性,该属性在ListControl类上定义,ListControl类还拥有许多其他非常有用的属性:

属性 描述
AppendDataBoundItems 获取或设置一个值,指示是否在绑定数据之前清除列表项
DataTextField 获取或设置为列表项提供文本内容的数据源字段
DataTextFormatString 获取或设置格式化字符串,该字符串用来控制如何显示绑定到列表控件的数据
DataValueField 获取或设置为各列表项提供值的数据源字段
SelectedIndex 获取或设置列表中选定项的最低序号索引
SelectedItem 获取列表控件中索引最小的选定项
SelectedValue 获取列表控件中选定项的值,或选择列表控件中包含指定值的项

在以上属性中,AppendDataBoundItems属性是.NET2.0中新增的,该属性在列表中已经手动定义某些项目后再执行数据绑定时特别有用。例如下拉列表中,可能希望为用户添加“请选择”项目,那么就可以使用<asp:ListItem />标记定义该列表项,并将AppendDataBoundItems属性设置为true,在该下拉列表再次执行数据绑定时会保留“请选择”项目。

对于本次任务的第一个需求,只需要使其继承自ListControl类,并且在呈现时将每一个数据项输出为一个超链接即可。但是该列表控件有一些局限,它只能采用ListItem类作为数据项对象类型,如果希望修改或添加列表项属性时,此类列表控件就无能为力了,为了实现这种目的(例如第二个需求),需要做一些额外的工作。

接下来考虑第二个需求,需要在数据绑定的基础上为自定义控件增加按照指定的方向呈现数据项的能力,这有点像DataList控件,可以通过设置RepeatDirection、RepeatColumns等属性实现上述功能。对于此类问题,.NET为我们提供了IRepeatInfoUser接口,该接口包含了以下成员的声明:

属性 描述
HasFooter属性 获取一个值,指示列表控件是否包含脚注部分
HasHeader属性 获取一个值,指示列表控件是否包含标题节
HasSeparators属性 获取一个值,指示列表控件是否包含列表项之间的分隔符
RepeatedItemCount属性 获取列表控件中的项数
GetItemStyle方法 检索列表控件中指定索引位置的指定项类型的样式
RenderItem 用指定的信息呈现列表中的项

为了实现新特性,使自定义控件实现IRepeatInfoUser接口,重要的是该接口中的RenderItem方法,每个绑定的数据项都自动调用此方法,此方法签名如下所示:

void RenderItem (
     ListItemType itemType,
     int repeatIndex,
     RepeatInfo repeatInfo,
     HtmlTextWriter writer
)

其参数说明如下:

参数 描述
itemType ListItemType枚举值之一,该枚举指定列表控件中项的类型
repeatIndex 指定列表控件中项的位置的序号索引
repeatInfo RepeatInfo类型对象,表示用于呈现列表中的项的信息
writer System.Web.UI.HtmlTextWriter类实例,表示用于在客户端呈现HTML内容的输出流

最后如果控件想要支持特殊的布局功能,还需要使用特殊的代码重写自定义控件的Render方法,在该方法中使用RepeatInfo类的RenderPrpeater方法呈现控件,以下是该方法的定义:

public void RenderRepeater (
     HtmlTextWriter writer,
     IRepeatInfoUser user,
     Style controlStyle,
     WebControl baseControl
)

其参数说明如下:

参数 描述
writer System.Web.UI.HtmlTextWriter类实例,表示用于在客户端呈现HTML内容的输出流
user 一个实现了IRepeatInfoUser接口的对象,表示要呈现的控件
controlStyle 表示显示项所采用的样式
baseControl 复制基属性的控件

根据以上分析,编写两个自定义控件实现数据项的输出。

3. 实现简单列表控件

3.1 在解决方案的ControlLibrary类库中创建SimpleHyperLinkList类,并引入必要命名空间:

using System.Web.UI;
using System.Web.UI.WebControls;
  namespace ControlLibrary
{
     [ToolboxData("<{0}:SimpleHyperLinkList runat=\"server\"></{0}>")]
     public class SimpleHyperLinkList : ListControl
     { 
     }
}

3.2 重写控件的Render方法,在呈现时迭代访问数据项以呈现多个链接控件,此处调用了RenderControl方法,该方法在Control类上定义,用于将服务器控件的内容输入到所提供的HtmlTextWriter对象中:

protected override void Render(HtmlTextWriter writer)
{
     HyperLink link = new HyperLink();
      for (int i = 0; i < Items.Count; i++)
     {
         link.ApplyStyle(ControlStyle);
         link.Text = Items[i].Text;
         link.NavigateUrl = Items[i].Value;
          link.RenderControl(writer);
         writer.Write("<br />");
     }
}

这个类的代码很简单,唯一比较有趣的地方是类使用了ToolboxData特性(对应System.Web.UI.ToolboxDataAttribute类),该特性指定了将控件从可视化设计器的工具箱中拖放到设计页面上时默认生成的标记,在需要为自定义控件指定属性的初始值时该特性很有用。

3.3 在ASPX页面中声明并定义控件,浏览运行结果。

4. 实现丰富特性列表控件

4.1 在ControlLibrary类库中创建HyperLinkItem实体类,用于保存最终生成的每一个链接的文本、注释和地址字符串:

namespace ControlLibrary
{
     public class HyperLinkItem
     {
         public HyperLinkItem()
         {
             Text = string.Empty;
             Tooltip = string.Empty;
             Url = string.Empty;
         }
          public HyperLinkItem(string text, string tooltip, string url)
         {
             Text = text;
             Tooltip = tooltip;
             Url = url;
         }
          public string Text
         {
             get;
             set;
         }
          public string Tooltip
         {
             get;
             set;
         }
          public string Url
         {
             get;
             set;
         }
     }
}

4.2 在类库中创建HyperLinkItemCollection集合类,该类继承了Collection泛型集合类,以确保该集合中只参添加HyperLinkItem类型对象,并且为了手动管理视图状态,该类实现了IStateManager接口:

using System.Collections.ObjectModel;
using System.Web.UI;
  namespace ControlLibrary
{
     public class HyperLinkItemCollection:Collection<HyperLinkItem>,IStateManager
     {
         private bool marked;
          public HyperLinkItemCollection()
         {
             marked = false;
         }
          public bool IsTrackingViewState
         {
             get
             {
                 return this.marked;
             }
         }
          public void TrackViewState()
         {
             marked = true;
         }
          public void LoadViewState(object state)
         {
             if (state == null)
                 return;
              Clear();
              Triplet trip = (Triplet)state;
              string[] rgTooltip = (string[])trip.First;
             string[] rgText = (string[])trip.Second;
             string[] rgUrl = (string[])trip.Third;
              for (int i = 0; i < rgUrl.Length; i++)
             {
                 Add(new HyperLinkItem(rgText[i], rgTooltip[i], rgUrl[i]));
             }
         }
          public object SaveViewState()
         {
             int num = Count;
              object[] rgTooltip = new string[num];
             object[] rgText = new string[num];
             object[] rgUrl = new string[num];
              for (int i = 0; i < num; i++)
             {
                 rgTooltip[i] = Items[i].Tooltip;
                 rgText[i] = Items[i].Text;
                 rgUrl[i] = Items[i].Url;
             }
              return new Triplet(rgTooltip, rgText, rgUrl);
         }
     }
}

以上代码稍微有些复杂的就是视图状态的保存了读取,为了保证将某一个HyperLinkItem的三个属性全部保存至视图状态中,定义了三个object数组最终保存到Triplet对象中(读取时仍遵循此规则)。

4.3 向类库中添加HyperLinkList类,为了使该类具有更丰富的展示方式,仍然继承自DataBindControl类并实现IRepeatInfoUser接口:

using System.Collections;
using System.Web.UI;
using System.Web.UI.WebControls;
  namespace ControlLibrary
{
     [ToolboxData("<{0}:HyperLinkList runat=\"server\"></{0}>")]
     public class HyperLinkList:DataBoundControl,IRepeatInfoUser
     {
     }
}

4.4 定义控件内部使用的重复项属性和暴露出来的集合属性,在HyperLinkList类中编写如下代码:

private HyperLinkItemCollection items;
private HyperLink controlToRepeat;
  private HyperLink ControlToRepeat
{
     get
     {
         if (controlToRepeat == null)
         {
             controlToRepeat = new HyperLink(); 
         }
         return controlToRepeat;
     }
}
  public virtual HyperLinkItemCollection Items
{
     get
     {
         if (items == null)
             items = new HyperLinkItemCollection();
         if (base.IsTrackingViewState)
             items.TrackViewState();
          return items;
     }
}

4.5 定义数据源映射属性DataTextField、DataValueField和DataTooltipField以从数据源中读取相应的数据填充重复项:

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

4.6 重写PerformDataBinding方法根据映射从数据源读取数据:

protected override void PerformDataBinding(IEnumerable data)
{
     base.PerformDataBinding(data);
      string urlField = DataUrlField;
     string textField = DataTextField;
     string tooltipField = DataTooltipField;
      if (data != null)
     {
         foreach (object o in data)
         {
             HyperLinkItem item = new HyperLinkItem();
             item.Url = DataBinder.GetPropertyValue(o, DataUrlField,null);
             item.Text = DataBinder.GetPropertyValue(o, DataTextField,null);
             item.Tooltip = DataBinder.GetPropertyValue(o, DataTooltipField,null);
              Items.Add(item);
         }
     }
}

4.7 重写SaveViewState和LoadViewState方法保存或读取数据项集合:

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

4.8 定义RepeatDirection、RepeatColumns和RepeatLayout属性供RepeatInfo对象使用以控制重复项的呈现:

public virtual RepeatDirection RepeatDirection
{
     get
     {
         object o = ViewState["RepeatDirection"];
          return o == null ? RepeatDirection.Vertical : (RepeatDirection)o;
     }
     set
     {
         ViewState["RepeatDirection"] = value;
     }
}
  public virtual int RepeatColumns
{
     get
     {
         object o = ViewState["RepeatColumns"];
          return o == null ? 0 : (int)o;
     }
     set
     {
         ViewState["RepeatColumns"] = value;
     }
}
  public virtual RepeatLayout RepeatLayout
{
     get
     {
         object o = ViewState["RepeatLayout"];
          return o == null ? 0 : (RepeatLayout)o;
     }
     set
     {
         ViewState["RepeatLayout"] = value;
     }
}

4.9 实现IRepeatInfoUser接口相关成员,此处为了方便,并没有增加新的特性:

bool IRepeatInfoUser.HasFooter
{
     get
     {
         return false;
     }
}
  bool IRepeatInfoUser.HasHeader
{
     get
     {
         return false;
     }
}
  bool IRepeatInfoUser.HasSeparators
{
     get
     {
         return false;
     }
}
  Style IRepeatInfoUser.GetItemStyle(ListItemType type, int repeatIndex)
{
     return null;
}

4.10 实现IRepeatInfoUser接口的RepeatedItemCount属性,返回当前输出项的数目:

int IRepeatInfoUser.RepeatedItemCount
{
     get
     {
         return this.Items.Count;
     }
}

4.11 实现IRepeatInfoUser接口的RenderItem方法以呈现数据项,该方法调用了ControlToRepeat私有属性以得到一个超链接:

void IRepeatInfoUser.RenderItem(ListItemType type, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer)
{
     HyperLink link = ControlToRepeat;
      int i = repeatIndex;
     link.ID = i.ToString();
     link.Text = Items[i].Text;
     link.NavigateUrl = Items[i].Url;
     link.ToolTip = Items[i].Tooltip;
      link.RenderControl(writer);
}

4.12 最后重写Render方法呈现控件,根据分析,这里使用RepeatInfo类:

protected override void Render(HtmlTextWriter writer)
{
     if (Items.Count > 0)
     {
         RepeatInfo ri = new RepeatInfo();
         Style controlStyle = base.ControlStyleCreated ? base.ControlStyle : null;
          ri.RepeatColumns = RepeatColumns;
         ri.RepeatDirection = RepeatDirection;
         ri.RepeatLayout = RepeatLayout;
         ri.RenderRepeater(writer, this, controlStyle, this);
     }
}

4.13 创建测试ASPX页面,声明并定义自定义控件,预览运行结果。

5. 总结

本次任务里,我们编写了一个继承自ListControl类的简单列表控件,在此基础上,我们再次开发了一个继承自DataBindControl类,并且为了实现丰富的布局功能,还实现了IRepeatInfoUser类,在呈现方法中,通过RepeatInfo类按照定义格式输入数据项。在下一个任务里,我们将开始一个稍复杂的看起来和GridView有些相似的组合数据绑定控件以显示一系列星级评分的列表。


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

前言

第一天 简单的星级控件 

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

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

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

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

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

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

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

第九天 自定义GridView

第十天 实现分页功能的DataList


全部源码下载

本系列文章PDF版本下载