摘要:了解 ASP.NET 2.0 中的用于生成自定义数据绑定控件的工具是如何演变的。
本页内容
为什么需要新的数据源模型
数据绑定是开发人员在 ASP.NET 1.x 中发现的最令人愉快的意外功能之一。与 Active Server Pages 对数据访问的支持相比,数据绑定是简单性和有效性的非凡结合。然而,如果根据真正开发人员的需要进行衡量,则它还不够完美。其局限不在于总体功能方面,而在于开发人员必须编写大量代码来处理甚至非常简单和常见的操作(例如,分页、排序或删除)。为了弥补这一缺陷,ASP.NET 2.0 添加了一种新的数据源模型(请参阅我的文章:More Load, Less Code with the Data Enhancements of ASP.NET 2.0)。它包括很多不带 UI 的新控件,这些控件将数据绑定控件的可视部分和数据容器联系起来。开发人员需要在 ASP.NET 1.x 中编写的绝大部分代码经过适当的分解和创作,现在基本上都被嵌入到一系列新的控件中:数据源组件。
使用数据源组件有很多好处 — 首先,可以得到完全声明性的数据绑定模型。新模型减少了以内联方式插入到 ASPX 资源中或者分散在代码隐藏类中的松散代码。新的数据绑定体系结构强制开发人员遵守严格的规则。此外,它还从本质上改变了代码的质量。附加到事件的较长代码块通常会消失,而被只是插入到现有框架中的组件所取代。这些数据源组件派生自抽象类,实现了已知的接口,并且总体而言意味着更高级别的可重用性。
Nikhil Kothari 的有关控件开发的优秀著作 — Developing Microsoft ASP.NET Server Controls and Components — 帮助成千上万的开发人员生成自定义控件,并且说明了设计和实现的最佳做法。但是,一本书 — 无论它有多么伟大 — 都永远无法取代一个更好的系统框架。借助于 ASP.NET 2.0,您还获得了一个完全重新设计的类图 — 当您沿着类树从基础类向叶子类滚动时,它能够添加更具体的数据绑定功能。通过新的数据绑定控件层次结构,所有开发人员都可以更容易地选取正确的类来加以继承,以便生成他们自己的自定义数据绑定控件。
在本文中,您将提前了解 ASP.NET 2.0 数据绑定模型中的能够对自定义控件产生影响的更改。在此过程中,您将了解可用的新基类以及新的高质量自定义控件的新要求。
ASP.NET 2.0 中的数据绑定控件
ASP.NET 2.0 数据源模型并未要求必须使用新的控件(例如,GridView 和 FormView);它仍然能够与旧样式的控件(例如,DataGrid 和 CheckBoxList)协同工作。这对于控件开发人员而言意味着什么呢?有两个截然不同类型的数据源需要处理 — 传统的基于 IEnumerable 的数据容器(例如,DataView 和集合)以及数据源控件(例如,SqlDataSource 和 ObjectDataSource)。最后,无论数据源是 ADO.NET 对象、自定义集合还是数据源组件,ASP.NET 2.0 数据绑定控件都必须能够将传入的任何数据规格化为可枚举的集合。
在 ASP.NET 1.x 中,文档在某种程度上领先于框架。文档正确地标识和讨论了三个类型的数据绑定控件 — 标准控件、列表控件和复合控件。任何只是提供 DataBind 方法和 DataSource 属性的非空实现的控件都属于第一个类别。列表控件是下列两者的有趣结合:高级布局属性(例如,RepeatColumns 和 RepeatLayout),以及为绑定的每个数据元素重复的固定的嵌入式项模板。最后,复合控件负责通过组合一个或多个现有控件来设计最终的用户界面。文档准确地阐述了与创建上述类型的控件相关的任何问题;然而,ASP.NET 框架却并未提供很多基类来简化开发人员的任务。图 1 显示了 ASP.NET 2.0 中的新的数据绑定控件层次结构。请注意显示为黄色的基类以及它们在整个类树中的分布。
图 1. ASP.NET 2.0 中的数据绑定控件的层次结构
对图 1 中呈现的基类进行一番观察是一件有趣的事情。它们在表 1 中列出并进行了详细说明。
BaseDataBoundControl
|
数据绑定控件的根类。执行数据绑定并验证任何绑定数据。
|
DataBoundControl
|
包含用于与数据源控件和数据容器进行通信的逻辑。可以从该类继承以生成标准的数据绑定控件。
|
ListControl
|
列表控件的基类,提供 Items 集合和高级布局呈现功能。
|
CompositeDataBoundControl
|
实现复合控件所必需的典型代码,包括在进行回发之后根据视图状态还原控件树的代码。
|
HierarchicalDataBoundControl
|
基于树的分层控件的根类。
|
表 1. ASP.NET 2.0 中的基本数据绑定类
对于任何曾经花费巨大精力来创建能够管理自己的数据集合并且能够正确地从视图状态还原的、具有丰富功能的数据绑定控件的人而言,这些类特别受欢迎。您需要能够对此加以阐明的示例吗?请继续阅读。
分析要点
ASP.NET 开发人员中心在过去几个月中发布了几篇有关下列 ASP.NET 1.1 数据绑定控件的文章:RssFeed 和 DetailsView 控件(这两篇文章分别为 Building DataBound Templated Custom ASP.NET Server Controls 和 A DetailsView Control for ASP.NET 1.x)。如果您深入分析这两个控件的代码,则您会看到它们在源代码中的各个位置利用了特殊的技术。例如,它们在向页进行回发(即不在该控件管辖范围内的回发)之后根据视图状态重新生成控件的树;它们通过一个自定义集合类来公开项集合;它们允许赋予项样式;它们支持很多个类型的输入源。在 ASP.NET 1.1 中,对于上述每一种功能,您都必须编写代码,并且更重要的是,您必须按照特定的顺序编写代码,重写特定的基础方法,并且认真遵守文档和前面提到的优秀著作 Developing Microsoft ASP.NET Server Controls and Components 中的指导和建议。
在 ASP.NET 2.0 中,两个示例控件中使用的大部分管线代码都被硬编码到表 1 中列出的基类中。为了对比 ASP.NET 1.1 和 2.0 中的数据绑定控件,我将重点讨论下列要点:
• |
总体数据绑定机制和不同的数据源类型
|
• |
集合和视图状态管理
|
该列表很可能是不完备的,但是它肯定足以使您对控件开发有一个大致的了解。您将既愉快又吃惊地看到,开发功能丰富的自定义控件所需的代码是如此之少。
数据绑定机制
要在 ASP.NET 2.0 中生成新的数据绑定控件,首先需要确定哪个类能够更好地适合您的要求。然而,您的选择并不局限于比较空的类,如 Control 和 WebControl 甚至 ListControl。让我们探索一下那些深藏于幕后的类。BaseDataBoundControl 是所有数据绑定控件类的根。它定义了 DataSource 和 DataSourceID 属性,并且验证它们被分配的内容。DataSource 接受按照 ASP.NET 1.x 的方式获得和分配的可枚举对象。
Mycontrol1.DataSource = dataSet;
Mycontrol1.DataBind();
DataSourceID 是一个字符串,并且是指绑定数据源组件的 ID。一旦将控件绑定到数据源,则二者之间的任何进一步的交互(无论是读还是写)都将脱离您的控制范围,并且不可见。这一点既有好的一面,也有坏的一面。好(更确切地说是伟大)的一面在于可以消除大量代码。ASP.NET 框架能够保证正确的代码得以执行,并且按照公认的最佳做法编写代码。您的工作效率会更高,因为您可以完全确信在工作过程中不会出现令人难以捉摸的错误,从而可以更快地创作页。如果您不喜欢这种情况(好像很多 ASP.NET 1.x 开发人员都抱怨这种情况),则您可以继续使用通过 DataSource 属性和 DataBind 方法完成的旧样式的编程。而且,在这种情况下,基类使您不必完成一些常见的工作,即使这种效果在代码中体现得不是那么明显。
DataBoundControl 类用于与现有控件没有多少共同点的标准的自定义数据绑定控件。如果您必须处理自己的数据项集合,管理视图状态和样式,创建简单但量身定制的用户界面,则该类可以提供一个良好的起点。最为有趣的是,DataBoundControl 类将控件连接到数据源组件,并且在 API 级别隐藏了可枚举数据源和特别组件之间的任何差异。简而言之,当您从该类继承时,您只需要重写一个接收数据集合(无论数据源是 DataSet 对象还是较新的数据源组件)的方法。
让我们详细阐述这一点(它代表体系结构中的重大更改)。
BaseDataBoundControl 重写了 DataBind 方法(原来在 Control 上定义),并且使它调用 PerformSelect 方法(该方法被标记为受保护的和抽象的)。正如其名称所暗示的那样,PerformSelect 能够检索有效的数据集合以使绑定发生。该方法是受保护的,因为它包含实现细节;它是抽象的(用 Visual Basic 行话说就是 MustInherit),因为它的行为只能由派生类(如 DataBoundControl)确定。
那么,DataBoundControl 完成哪些工作以重写 PerformSelect 呢?
它连接到数据源对象并获得默认视图。数据源对象(例如,像 SqlDataSource 或 ObjectDataSource 之类的控件)执行它的选择命令并返回得到的集合。操作数据检索的受保护方法(名为 GetData)还足够聪明,以便检查 DataSource 属性。如果 DataSource 非空,则将绑定对象包装到一个动态创建的数据源视图对象中,并且将其返回。
下一个步骤需要您以控件开发人员的身份参与。迄今为止,基类已经以一种完全自动的方式从 ADO.NET 对象或数据源组件中检索数据。下一个步骤取决于您期望该控件完成哪些任务。这里正好用到可重写的 PerformDataBinding 方法。以下代码片段显示了 DataBoundControl 中对该方法的实现。请注意,由框架传递给该方法的 IEnumerable 参数只包含要绑定的数据(不管它们的来源如何)。
protected virtual void PerformDataBinding(IEnumerable data)
{
}
在自定义数据绑定控件中,您只需要重写该方法,并且填充任何特定于控件的集合,如包含很多个列表控件的 Items 集合(例如,CheckBoxList)。控件的用户界面的呈现发生在 Render 方法或 CreateChildControls 中,具体取决于该控件的性质。Render 适用于列表控件;而 CreateChildControls 则非常适合于复合控件。
有一件事情尚未解释:由谁启动数据绑定过程?在 ASP.NET 1.x 中,数据绑定需要显式调用 DataBind 方法才能开始工作。在 ASP.NET 2.0 中,如果您使用 DataSource 属性将数据绑定到控件,则仍然需要这样做。如果您改而通过 DataSourceID 属性使用数据源组件,则应当避免这样做。数据绑定过程由 DataBoundControl 中定义的内部 OnLoad 事件处理程序自动触发,如下面的伪代码所示。
protected override void OnLoad(EventArgs e)
{
this.ConnectToDataSourceView();
if (!Page.IsPostBack)
base.RequiresDataBinding = true;
base.OnLoad(e);
}
每当该控件被加载到页中的时候(回发或首次加载),都会检索和绑定数据。需要由数据源决定是再次运行查询还是使用一些缓存数据。
如果该页是首次显示,则还会启用 RequiresDataBinding 属性以要求绑定数据。当分配的值为 true 时,该属性的设置程序会在内部调用 DataBind。下面的伪代码显示了 RequiresDataBinding 设置程序的内部实现。
protected void set_RequiresDataBinding(bool value)
{
if (value && (DataSourceID.Length > 0))
DataBind();
else
_requiresDataBinding = value;
}
正如您可以看到的那样,为了向后兼容,仅当 DataSourceID 不为空(即您绑定到 ASP.NET 2.0 数据源控件)时,才会发生对 DataBind 的自动调用。有鉴于此,如果您还显式调用 DataBind,则会导致双重数据绑定。
请注意,您无法同时设置 DataSource 和 DataSourceID。当发生这种情况时,将引发无效操作异常。
最后,稍微提一下 EnsureDataBound 这一受保护的方法。该方法是在 BaseDataBoundControl 类上定义的,它能够确保控件已经被正确地绑定到必需的数据。如果 RequiresDataBinding 为 true,则该方法调用 DataBind,如下面的代码片段所示。
protected void EnsureDataBound()
{
if (RequiresDataBinding && (DataSourceID.Length > 0))
DataBind();
}
如果您已经编写了复杂且完善的数据绑定控件,则您很可能已经知道我的意思。在 ASP.NET 1.x 中,在下列两种情况下,通常会将数据绑定控件设计为生成它自己的用户界面:该控件具有对数据源的完全访问权限,或者该控件基于视图状态。当该控件需要管理它自己的回发事件时(例如,假设该控件是支持分页的 DataGrid),则前面提到的两个选择似乎是两种极端的情况。在 ASP.NET 1.x 中,这些控件(同样,请考虑 DataGrid)只有一种解决办法:向要刷新的主页引发事件。该方法导致 ASP.NET 1.x 页中存在多余代码这一众所周知的问题 — 这也正是调用数据源组件来加以修复的问题。
在 ASP.NET 2.0 中,每当在控件的生存期中发生要求绑定数据的事情时,都需要将 RequiresDataBinding 设置为 true。设置该属性会触发相应的数据绑定机制,从而重新创建该控件的内部基础结构的更新版本。内置的 OnLoad 事件处理程序还会将该控件连接到数据源。为了确实有效,该技术必须依赖于能够将它们的数据缓存在某个位置的智能数据源控件。例如,SqlDataSource 控件支持很多属性,以便在给定期限内将任何绑定结果集存储到 ASP.NET 缓存中。
列表控件
数据绑定控件通常为列表控件。列表控件通过为它的主框架边界内的每个绑定数据项重复固定的模板,生成它自己的用户界面。例如,CheckBoxList 控件只是为每个绑定数据项重复 CheckBox 控件。同样,DropDownList 控件遍历它的数据源,并且在 <select> 父标记内创建新的 <option> 元素。除了列表控件以外,ASP.NET 还提供了迭代控件。它们有什么不同?
列表控件和迭代控件的不同之处在于被应用于每个数据项的可重复模板允许具有的自定义级别。像 CheckBoxList 控件一样,Repeater 控件遍历绑定数据项并应用用户定义的模板。Repeater(以及更完善的 DataList 控件)极为灵活,但是在使代码保持模块化和分层化方面不能提供多少帮助。要使用 Repeater,您需要在该页(或外部用户控件)中定义模板,并使用 ASPX 源中的数据绑定属性。它是快速、有效的,有时还是必要的,但肯定不是整洁和优雅的。
在 ASP.NET 1.x 中,所有列表控件都从 ListControl(它是表 1 中唯一一个已经在 1.x 中定义的类)继承。让我们进入编码猴子模式,并且开始练习使用 ASP.NET 2.0 中的数据绑定控件。我将首先生成一个 HeadlineList 控件,以便为每个数据项呈现两行数据绑定文本。此外,该控件还将具备一些布局功能,例如,垂直或水平呈现。
HeadlineList 示例控件
正如前面提到的那样,ListControl 是 ASP.NET 1.x 和 2.0 中所有列表控件的基类。非常令人愉快的是,可以用一种非常平滑的方式将在此为 ASP.NET 2.0 编写的 HeadlineList 控件向后移植到 ASP.NET 1.x。出于某种原因,当需要生成标题列表时,人们的大脑中涌现的第一个想法往往是使用 Repeater。的确,Repeater 会使这一工作变得非常简单。
<asp:Repeater runat="server">
<HeaderTemplate>
<table>
</HeaderTemplate>
<ItemTemplate>
<tr><td>
<%# DataBinder.Eval(Container.DataItem, "Title") %>
<hr>
<%# DataBinder.Eval(Container.DataItem, "Abstract") %>
</td></tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
这段代码有什么问题?或者更准确地说,这段代码中有哪些可以改进的地方?
注:在 ASP.NET 2.0 中,您可以将 DataBinder.Eval(Container.DataItem, field) 替换为一个较短的表达式,该表达式受益于 Page 类上的一个新的公共方法 — Eval。这一新的表达式类似于 Eval(field)。在内部,Eval 调用 DataBinder 类上的静态 Eval 方法,并且确定要使用的正确绑定上下文。
字段的名称在 ASPX 页中硬编码。可以实现可重用性,但只能通过剪切和粘贴实现。您所添加的用于使 Repeater 的行为更加丰富多彩的代码越多,对该解决方案及其跨越页和项目的可重用性的危害就越大。如果标题列表控件恰恰是您需要的东西,则请改而尝试以下方法。
public class HeadlineList : ListControl, IRepeatInfoUser
{
:
}
ListControl 是列表控件的基类(它位于与 CheckBoxList、DropDownList 和类似控件相同的系列中);IRepeatInfoUser 是上述大多数控件加以实现以便用水平或垂直方式在列和行中呈现的几乎不为人所知的界面。请注意,ListControl 和 IRepeatInfoUser 还存在于 ASP.NET 1.x 中,并且以几乎与 2.0 相同的方式工作。
列表控件是围绕一个要重复的控件生成的;该控件(或控件图)是一个类属性,并且在加载时实例化以节省一些 CPU 时间。以下为私有 ControlToRepeat 属性的实现。
private Label _controlToRepeat;
private Label ControlToRepeat
{
get
{
if (_controlToRepeat == null)
{
_controlToRepeat = new Label();
_controlToRepeat.EnableViewState = false;
Controls.Add(_controlToRepeat);
}
return _controlToRepeat;
}
}
在该示例中,要重复的控件(标题)是一个在首次读取时实例化的 Label。HeadlineList 控件还应当向用户提供通过多种属性(如 RepeatLayout、RepeatColumns 和 RepeatDirection)影响外观的方式。很多标准列表控件上都定义了这些属性,因此开发人员不应该对它们感到陌生。它们的实现是类似的,并且看起来像下面的代码。
public virtual RepeatDirection RepeatDirection
{
get
{
object o = ViewState["RepeatDirection"];
if (o != null)
return (RepeatDirection) o;
return RepeatDirection.Vertical;
}
set
{
ViewState["RepeatDirection"] = value;
}
}
为完成 HeadlineList 控件而需要编写的另一段代码以呈现为中心。IRepeatInfoUser 接口对您可以用来控制呈现过程的各种属性进行计数。这方面的属性示例有 HasHeader、HasFooter 和 HasSeparator 布尔型属性。您可以像实现其他任何普通属性一样实现这些属性,并且根据需要在 RenderItem 接口方法中使用它们。
public void RenderItem(ListItemType itemType, int repeatIndex,
RepeatInfo repeatInfo, HtmlTextWriter writer)
{
string format = "<b>{0}</b><hr style='solid 1px black'>{1}";
Label lbl = ControlToRepeat;
int i = repeatIndex;
lbl.ID = i.ToString();
string text = String.Format(format, Items[i].Text, Items[i].Value);
lbl.Text = text;
lbl.RenderControl(writer);
}
RenderItem 对向页提供的输出承担最终的责任。它获得要重复的控件,并且将其呈现到标记中。RenderItem 是从 Render 中调用的。
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);
}
}
RepeatInfo 是一个 Helper 对象,它经过专门设计,以便通过重复现有的控件图来生成新控件。以上就是所需的全部代码。让我们准备一个示例页,并测试该控件。
<expo:headlinelist id="HeadlineList1" runat="server"
repeatlayout="Table" repeatdirection="Vertical" repeatcolumns="2"
datatextfield="LastName" datavaluefield="Notes" />
图 2 显示了该控件的工作方式。
图 2. HeadlineList 数据绑定控件
该控件在设计时工作正常,并且不需要插入其他任何代码。然而,这段代码的最令人愉快的边界效应并非免费的设计时支持。对我来说,它简直太美妙了,因为它能够使用 ADO.NET 数据源对象(例如,DataTable 或 DataSet)和数据源组件(如 SqlDataSource)。您可以取走这段代码,将其编译为 ASP.NET 1.x 项目,而它就可以使用基于 IEnumerable 的数据源。如果将这段代码引入到 ASP.NET 2.0 项目中,则它无须更改就同样可以使用数据源对象。
这一事实的意义是什么?
在 ASP.NET 1.x 中,ListControl 类是一个令人愉快的例外 — 但仍然是一个例外。在 ASP.NET 2.0 中,您可以使用类似的简单但有效的方法来生成任何数据绑定控件。在这样做的时候,您可以利用合并了大部分复杂性并且将大多数已知的最佳做法硬编码的新基类。
管理自定义集合
ListControl 是一个过于专用的类,它以不受您控制的固定方式执行数据绑定 — 除非您重写诸如 PerformSelect、OnDataBinding 和 PerformDataBinding 之类的方法。它还提供了预定义的 Items 集合属性。让我们在 ASP.NET 2.0 中的更低级别处理数据绑定,并且设计具有下列功能的 ButtonList 控件:
• |
使用自定义集合类来保留组成项
|
• |
用自定义方式管理视图状态
|
ButtonList 控件是另一个为每个绑定数据项输出按钮的列表控件。您可以让它从 ListControl 继承;而且,您可以获得 HeadlineList 的源代码,将 Label 替换为 Button,而它仍然应当正常工作。这一次,我将采用一种不同的方法来说明 DataBoundControl 的行为。为简单起见,我仍将跳过 IRepeatInfoUser 接口。
public class ButtonList : System.Web.UI.WebControls.DataBoundControl
{
:
}
标题和命令名称表现了每个按钮的性质。该信息是通过几个自定义属性(如 DataTextField 和 DataCommandField)从绑定数据源中获得的。您可以容易地添加类似的属性,以提供数据绑定工具提示,甚至提供 URL。
public virtual string DataCommandField
{
get
{
object o = ViewState["DataCommandField"];
if (o == null)
return "";
return (string)o;
}
set { ViewState["DataCommandField"] = value; }
}
所发现的有关每个绑定按钮的所有信息都被填充到一个通过 Items 属性公开的自定义对象集合中。(请注意,Items 只是该属性的标准、惯用而任意的名称。)
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public virtual ButtonItemCollection Items
{
get
{
if (_items == null)
{
_items = new ButtonItemCollection();
if (base.IsTrackingViewState)
_items.TrackViewState();
}
return _items;
}
}
Items 集合是自定义 ButtonItemCollection 类的实例 — ButtonItem 对象的集合。ButtonItem 类只是存储了有关绑定按钮的关键信息 — Text 和 CommandName 属性,外加几个构造函数以及 ToString 方法。ButtonItem 类是作为普通列表控件的 ListItem 类的对等物。下面是一个示例。
public class ButtonItem
{
private string _text;
private string _command;
public ButtonItem(string text, string command) {
_text = text;
_command = command;
}
public string Text {
get {return _text;}
set {_text = value;}
}
public string CommandName {
get { return _command; }
set { _command = value; }
}
public override string ToString() {
return "Button [" + Text + "]";
}
}
现在,如何创建 ButtonItem 对象的集合呢?在 ASP.NET 1.x 中,您必须生成一个从 CollectionBase 继承的自定义集合类,并且起码重写几个方法。然而,自定义集合只是围绕 ArrayList 对象的包装而已,在访问速度方面并没有任何真正的优势。实际上,仍然需要进行转换。.NET 2.0 中的泛型提供了真正的转折点。要生成 ButtonItem 对象集合,您需要以下代码:
public class ButtonItemCollection : Collection <ButtonItem>
{
}
并且,它的性能也会更好,因为编译器在幕后完成了某些工作。ButtonList 控件只需要两个被重写的方法:Render 和 PerformDataBinding。Render 假定 Items 集合被填充;因此,它只是进行迭代并输出标记代码。
protected override void Render(HtmlTextWriter writer)
{
for(int i=0; i< } btn.RenderControl(writer); btn.CommandName="item.CommandName;" btn.Text="item.Text;" Button(); btn="new" Button item="Items[i];" ButtonItem { i++)>
Items 集合为什么如此重要?它可以帮助您获得两个结果。首先,您可以用手动添加的项填充该列表控件。其次,一旦在视图状态中持久保存该集合,您就可以在回发时重新生成该控件的用户界面,而无须绑定到数据。在进行数据绑定时,Items 集合是在何处以及由谁填充的呢?这需要用到 PerformDataBinding。该方法获得一个可枚举的数据列表(无论原始数据源是什么)并使用它来填充 Items 集合。
protected override void PerformDataBinding(IEnumerable dataSource)
{
base.PerformDataBinding(dataSource);
string textField = DataTextField;
string commandField = DataCommandField;
if (dataSource != null) {
foreach (object o in dataSource)
{
ButtonItem item = new ButtonItem();
item.Text = DataBinder.GetPropertyValue(o, textField, null);
item.CommandName = DataBinder.GetPropertyValue(o,
DataCommandField, null);
Items.Add(item);
}
}
}
每当需要进行数据绑定时,该方法都能够确保 Items 集合被填充。在回发时会发生什么?在这种情况下,必须根据视图状态重新构建 Items 集合。您可以通过 IStateManager 接口上的方法赋予自定义集合类这一能力。以下为该接口的关键方法:
public void LoadViewState(object state)
{
if (state != null) {
Pair p = (Pair) state;
Clear();
string[] rgText = (string[])p.First;
string[] rgCommand = (string[])p.Second;
for (int i = 0; i < rgText.Length; i++)
Add(new ButtonItem(rgText[i], rgCommand[i]));
}
}
public object SaveViewState()
{
int numOfItems = Count;
object[] rgText = new string[numOfItems];
object[] rgCommand = new string[numOfItems];
for (int i = 0; i < numOfItems; i++) {
rgText[i] = this[i].Text;
rgCommand[i] = this[i].CommandName;
}
return new Pair(rgText, rgCommand);
}
该类使用一个 Pair 对象(一种经过优化的 2 位置数组)将自身序列化为视图状态。您需要创建两个对象数组,以便保留每个按钮的文本和命令名称。这两个数组随后被成对打包并插入到该视图状态中。当还原该视图状态时,会将该数组对拆包,并且使用先前存储的信息重新填充 Items 集合。使用该方法要比使 ButtonItem 类可序列化更可取,因为传统的二进制格式化程序的性能(在空间和时间这两个方面)更差。
然而,向集合中添加视图状态支持还不够。还必须增强 ButtonList 控件以利用集合的序列化功能。您可以重写控件类上的 LoadViewState 和 SaveViewState。
protected override void LoadViewState(object savedState)
{
if (savedState != null) {
Pair p = (Pair) savedState;
base.LoadViewState(p.First);
Items.LoadViewState(p.Second);
}
else
base.LoadViewState(null);
}
protected override object SaveViewState()
{
object baseState = base.SaveViewState();
object itemState = Items.SaveViewState();
if ((baseState == null) && (itemState == null))
return null;
return new Pair(baseState, itemState);
}
控件的视图状态由两个元素组成:默认控件的视图状态以及 Items 集合。这两个对象被打包到 Pair 对象中。除了 Pair 对象以外,您还可以使用 Triplet 对象(包含三个对象的数组),或者使用 Pair 或 Triplet 对组成任意数量的对象。
以这种方式设计的自定义集合还可以在设计时满足需要。Visual Studio 2005 中嵌入的默认集合编辑器可以识别该集合并弹出如图 3 所示的对话框。
图 3. 设计时的 ButtonList Items 集合
值得说明的是,在 ASP.NET 2.0 中,某些数据绑定控件使您可以将数据绑定项与以编程方式通过 Items 集合添加的项分开。布尔型的 AppendDataBoundItems 属性用于控制该控件的编程接口的这一方面。该属性在 ListControl(而非 DataBoundControl)上定义,并且默认为 false。
关于复合控件的一点讨论
CompositeDataBoundControl 类是生成复合控件(我相信当您想到数据绑定控件时,您所指的就是这些控件)的起点。复合控件必须能够:
• |
充当命名容器。
|
• |
通过 CreateChildControls 方法创建它自己的用户界面。
|
• |
实现特定的逻辑,以便在回发之后还原它的子元素层次结构。
|
最后一点在 Nikhil Kothari 的著作中进行了良好的阐述,并且在 ASP.NET 1.x 的所有内置控件中都得到了实现。如果您迄今为止尚未完全了解该概念,则好消息是您现在可以彻底忘掉有关它的所有内容。一切内容现在都在 CompositeDataBoundControl 类中硬编码。您需要关心的主要方面是设计您的控件的子控件。您可以通过重写按以下方式定义的新方法来完成该工作:
protected abstract int CreateChildControls(
IEnumerable dataSource, bool dataBinding);
CompositeDataBoundControl 从 DataBoundControl 继承,因此,本文中陈述的有关集合、绑定和视图状态的大部分内容也适用于复合控件。
小结
数据绑定和数据绑定控件代表着 ASP.NET 1.x 中的一项巨大突破,但是有一些要点没有得到解释,并且有几个问题有待回答。Nikhil Kothari 的著作为所有开发人员提供了完美的权威的指南。ASP.NET 2.0 将该书的一些最佳做法(大部分已经在 ASP.NET 1.x 的幕后得以实现)转换为可重用的类以及新的数据绑定控件对象模型。
本文重点讨论了在从 ASP.NET 1.x 升级到 ASP.NET 2.0 时进行的主要更改,并且通过几个实际示例概述了它们影响开发的方式。接下来,我们将需要关注 ASP.NET 2.0 控件开发中的样式和主题。但是,那也许会成为在不久的将来问世的另一篇文章的中心议题。请继续关注我们的工作。
|