迪诺·埃斯波西托
坚实的质量学习
2006 年 2 月
适用于:
ASP.NET 2.0
Visual Basic 2005
Visual C# 2005
.NET Frameworks
Visual Web Developer 2005
总结: Dino Esposito 继续他的系列 ASP.NET 控件开发,在第四部分介绍了如何使用和创建复合控件。 (16 个打印页)
本文提供了 Visual Basic 和 C# 源代码。 从 此处下载它们。
简介
复合控件的要点在哪里?
复合控件的常见方案
复合控件的呈现引擎
用于修复Design-Time问题的 CompositeControl
生成复合Data-Bound控件
结论
复合控件只是普通 ASP.NET 控件,还不是要处理的另一种 ASP.NET 服务器控件。 因此,为什么书籍和文档倾向于为它们保留特殊部分? ASP.NET 复合控件有什么特别之处?
顾名思义,复合控件是一个在单个屋脊和单个 API 下聚合多个其他组件的控件。 如果你有一个由标签和文本框组成的自定义控件,则可以说你有一个复合控件。 “复合”一词表示控件在内部产生于其他构成控件的运行时组合。 复合控件公开的方法和属性集通常由构成控件的方法和属性以及一些新成员提供,但不一定。 复合控件也可以触发自定义事件;它还可以处理子控件引发的事件并弹出事件。
复合控件在 ASP.NET 中如此特殊之处与其说是作为新类型服务器控件的代表,不如说是可能的标识。 在呈现方面,它是它从 ASP.NET 运行时获得的支持。
复合控件是一种功能强大的工具,可用于生成由实时对象的交互而不是某些字符串生成器对象的标记输出产生的丰富和复杂的组件。 复合控件呈现为构成控件的树,每个控件都有自己的生命周期和事件,并且所有控件都配合形成一个全新的 API,根据需要尽可能抽象。
在本文中,我将讨论复合控件的内部体系结构,以明确在许多情况下从这些控件中获得的好处。 接下来,我将生成一个复合列表控件,该控件具有一组更丰富的功能,这与我在上一篇文章中讨论的控件相比。
前段时间,我自己也试图在 ASP.NET 中理解复合控件。 我从 MSDN 文档中学习了理论和实践,并找出了一些很好的控制措施。 但是,只有当我偶然遇到以下示例时,我才真正明白复合控件 (和美) 。 想象一下有史以来最简单的 (和最常见的) 控件,它由两个其他控件(Label 和 TextBox)组合而成。 下面是编写此类控件的可能方法。 让我们将其命名为 LabelTextBox。
public class LabelTextBox : WebControl, INamingContainer { public string Text { get { object o = ViewState["Text"]; if (o == null) return String.Empty; return (string) o; } set { ViewState["Text"] = value; } } public string Title { get { object o = ViewState["Title"]; if (o == null) return String.Empty; return (string) o; } set { ViewState["Title"] = value; } } protected override void CreateChildControls() { Controls.Clear(); CreateControlHierarchy(); ClearChildViewState(); } protected virtual void CreateControlHierarchy() { TextBox t = new TextBox(); Label l = new Label(); t.Text = Text; l.Text = Title; Controls.Add(l); Controls.Add(t); } }
控件具有两个公共属性(Text 和 Title)以及呈现引擎。 属性将保存到 viewstate 并分别表示 TextBox 和 Label 的内容。 控件没有 对 Render 方法的重写,并通过 CreateChildControls 重写的方法生成自己的标记。 稍后我将介绍呈现阶段的机制。 CreateChildControls 的代码首先清除子控件的集合,然后生成构成当前控件输出的控件树。 CreateControlHierarchy 是特定于控件的方法,不一定要标记为受保护和虚拟。 但请注意,大多数本机复合控件 (DataGrid,例如,) 只是公开用于通过类似的虚拟方法生成控件树的逻辑。
CreateControlHierarchy 方法根据需要实例化尽可能多的构成控件,并编写最终输出。 完成后,控件将添加到当前控件的 Controls 集合中。 如果控件的输出预期为 HTML 表,请创建 一个 Table 控件,并根据需要添加行和单元格及其自己的内容。 所有行、单元格和包含控件都是最外层表的子级。 在这种情况下,只需将 Table 控件添加到 Controls 集合。 在前面的代码中,Label 和 TextBox 是 LabelTextBox 控件的直接子级,并直接添加到集合中。 控件呈现和工作正常。
就纯性能而言,创建控件的暂时性实例不如呈现一些纯文本那么高效。 让我们考虑一种不使用子控件编写同一控件的另一种方法。 这次将其命名为 TextBoxLabel 。
public class LabelTextBox : WebControl, INamingContainer { : protected override void Render(HtmlTextWriter writer) { string markup = String.Format( "<span>{0}</span><input type=text value='{1}'>", Title, Text); writer.Write(markup); } }
控件具有相同的两个属性(Text 和 Title),并重写 Render 方法。 如你所看到的,实现要简单得多,并且提供了稍微快一点的代码。 在字符串生成器中撰写文本并为浏览器输出最终标记,而不是编写子控件。 同样,在本例中, 控件呈现良好。 但是,我们真的能说它工作正常吗? 图 1 显示了在示例页中运行的两个控件。
图 1. 使用不同呈现引擎的类似控件
在页面中打开跟踪并重新运行。 当页面显示在浏览器中时,向下滚动并查看控件树。 如下所示:
图 2. 两个控件生成的控件树
复合控件由构成控件的实时实例组成。 ASP.NET 运行时知道这些子控件,并且可以在处理已发布的数据时直接与它们通信。 因此,子控件可以自动处理视图状态和气泡事件。
基于标记组合的控件的情况有所不同。 如图所示,控件是一个具有空 Controls 集合的原子代码单元。 如果标记在页面 (文本框、按钮、下拉列表) 注入交互式元素,ASP.NET 无法在不涉及控件本身的情况下处理回发数据和事件。
尝试在这两个文本框中输入一些文本,然后单击图 1 的“刷新”按钮,以便发生回发。 第一个控件(复合控件)在回发时正确维护分配的文本。 使用 Render 的第二个控件在回发时丢失新文本。 为什么会这样? 有两个综合原因。
第一个原因是,在前面的标记中,我没有为 <输入> 标记命名。 这样,它的内容就不会发回。 请注意,必须使用 name 属性来命名元素。 让我们按如下所示修改 Render 方法。
protected override void Render(HtmlTextWriter writer) { string markup = String.Format( "<span>{0}</span><input type=text value='{1}' name='{2}'>", Title, Text, ClientID); writer.Write(markup); }
现在,客户端页中注入的输入>元素与服务器控件具有相同的 ID。< 当页面回发时,ASP.NET 运行时可以找到与已发布字段的 ID 匹配的服务器控件。 但是,它不知道如何使用它。 要使 ASP.NET 将所有客户端更改应用于服务器控件,该控件必须实现 IPostBackDataHandler 接口。
包含 TextBox 的复合控件无需担心回发,因为嵌入控件将自动使用 ASP.NET。 呈现 TextBox 的控件需要与 ASP.NET 交互,以确保正确处理已发布的值,并按预期触发事件。 以下代码演示如何扩展 TextBoxLabel 控件,使其完全支持回发。
bool LoadPostData(string postDataKey, NameValueCollection postCollection) { string currentText = Text; string postedText = postCollection[postDataKey]; if (!currentText.Equals(postedText, StringComparison.Ordinal)) { Text = postedText; return true; } return false; } void IPostBackDataHandler.RaisePostDataChangedEvent() { return; }
复合控件是构建复杂组件的正确工具,其中多个子控件是聚合在它们之间以及与外部世界交互的。 呈现的控件正好适用于输出不包含交互式元素(如下拉列表或文本框)的控件的只读聚合。
如果你对事件处理和发布数据感兴趣,我衷心建议你选择复合控件。 使用子控件时,生成复杂控件树更简单,最终结果更整洁、更优雅。 此外,仅当需要提供其他功能时才需要处理回发接口。
呈现的控件需要实现其他接口,而不是提及使用针和线程将标记的静态部分与属性值缝合在一起。
复合控件也非常适合呈现同质项的列表,例如在 DataGrid 控件中。 将每个构成项作为实时对象提供,可以触发创建事件并以编程方式访问其属性。 在 ASP.NET 2.0 中,完全实现现实和数据绑定复合控件所需的大部分样板代码(上一个只是玩具示例)都埋藏在新基类 (CompositeDataBoundControl)的折叠中。
在深入了解 ASP.NET 2.0 编码技术之前,让我们回顾一下复合控件的内部机制。 如前所述,复合控件的呈现以从基 Control 类继承的 CreateChildControls 方法为中心。 你可能会认为,重写 Render 方法对于服务器控件呈现其内容至关重要。 如前所述,如果 重写 CreateChildControls ,则并非始终需要此功能。 但是,何时在控件的调用堆栈中调用 CreateChildControls ?
第一次显示页面时,将在预呈现阶段调用 CreateChildControls ,如图所示。
图 3. 在预呈现阶段调用 CreateChildControls
具体而言, Page 类中的请求处理代码 (,) 立即调用 EnsureChildControls ,然后再将 PreRender 事件激发到页面和每个子控件。 换句话说,如果尚未完全生成树,则不会呈现控件。
以下代码片段演示了 EnsureChildControls 的伪代码,这是 Control 上定义的另一种方法。
protected virtual void EnsureChildControls() { if (!ChildControlsCreated) { try { CreateChildControls(); } finally { ChildControlsCreated = true; } } }
在页面和控件的生命周期中,可以重复调用 方法。 为了避免控件重复, ChildControlsCreated 属性设置为 true。 如果 属性返回 true,该方法将立即退出。
当页面回发时, ChildControlsCreated 在周期的早期调用。 如图 4 所示,它在已发布的数据处理阶段被调用。
图 4。 在回发时在发布数据处理阶段调用
当 ASP.NET 页开始处理从客户端发布的数据时,它会尝试查找其 ID 与已发布字段的名称匹配的服务器控件。 这样做时,页面代码会调用 Control 类上的 FindControl 方法。 此方法反过来需要确保在继续操作之前完全生成控件的树,因此它会调用 EnsureChildControls 并根据需要生成控件的层次结构。
在 CreateChildControls 方法中执行的代码呢? 尽管没有要遵循的官方准则,但通常一致认为 CreateChildControls 必须至少完成以下任务:清除 Controls 集合、生成控件的树以及清除子控件的视图状态。 严格不需要从 CreateChildControls 方法中设置 ChildControlsCreated 属性。 事实上,ASP.NET 页框架始终通过 EnsureChildControls 调用 CreateChildControls,这将自动设置布尔标志。
ASP.NET 2.0 附带一个名为 CompositeControl 的基类。 因此,新的复合控件(而不是数据绑定)应派生自此类,而不是 WebControl。 CompositeControl 的使用不会在开发控件的方式上发生太大变化。 仍需要重写 CreateChildControls 并按前面所示的方式编写代码。 那么 ,CompositeControl 的作用是什么? 让我们从其原型开始:
public class CompositeControl : WebControl, INamingContainer, ICompositeControlDesignerAccessor
类可避免使用 INamingContainer 修饰控件,实际上节省的成本并不大,因为接口只是一个标记,没有方法。 更重要的是, 类实现了名为 ICompositeControlDesignerAccessor 的全新接口。
public interface ICompositeControlDesignerAccessor { void RecreateChildControls(); }
复合控件的标准设计器使用 接口在设计时重新创建控件树。 下面是 CompositeControl 中 方法的默认实现。
void ICompositeControlDesignerAccessor.RecreateChildControls() { base.ChildControlsCreated = false; EnsureChildControls(); }
简言之,如果你从 CompositeControl 派生复合控件,则你不会遇到设计时问题,也不必使用技巧和调整来使控件在运行时和设计时都正常工作。
若要充分了解此接口的重要性,请获取承载 LabelTextBox 复合控件的示例页,并将其转换为设计模式。 控件在运行时工作正常,但在设计时不可见。
图 5。 复合控件不享受特殊的设计时处理,除非派生自 CompositeControl
如果只是将 WebControl 替换为 CompositeControl,则控件在运行时仍能正常工作,但在设计时也表现良好。
图 6。 在设计时工作出色的复合控件
大多数复杂的服务器控件都是数据绑定的,也许由各种子控件进行模板化和形成。 这些控件维护构成项的列表-通常是表的行或单元格。 该列表在 viewstate 中跨回发保持,并且根据绑定数据生成或从 viewstate 重新生成。 控件还会在视图中保存其构成项的数目,以便在页面中的其他控件导致回发时可以正确重新创建表的结构。 让我通过 DataGrid 控件来举例说明一下。
DataGrid 由行列表组成,其中每个行表示绑定数据源中的一条记录。 每一个网格行都用 DataGridRow 对象(派生自 TableRow 的类)表示。 创建单个网格行并将其添加到最终网格表时,将触发相应的事件,如 ItemCreated 和 ItemDataBound 到页面。 通过数据绑定创建 DataGrid 时,行数取决于绑定的项数和页面大小。 如果 包含 DataGrid 的页面回发,该怎么办?
在这种情况下,如果 DataGrid 本身导致回发(例如,用户单击排序或分页),则新页面将通过数据绑定再次呈现 DataGrid 。 这很明显,因为 DataGrid 需要显示新数据。 如果主机页面回发,则情况会有所不同,因为单击了页面上的另一个控件,例如按钮。 在这种情况下, DataGrid 未绑定到数据,必须从 viewstate 重新生成。 (如果禁用 viewstate,则为另一个故事,并且只能通过数据绑定显示网格。)
数据源不保留在 viewstate 中。 作为复合控件, DataGrid 包含子控件,每个子控件将自己的状态保存到视图状态并从该视图状态还原。 DataGrid 只需跟踪它必须循环访问的次数,直到从 viewstate 还原所有行和包含控件为止。 此数字与显示的绑定项数一致,并且必须作为控件状态的一部分存储在 viewstate 中。 在 ASP.NET 1.x 中,必须自行学习并实现此模式。 在 ASP.NET 2.0 中,从新类 CompositeDataBoundControl 派生复合控件就足够了。
让我们尝试一个类似网格的控件,该控件显示可扩展的数据绑定新闻标题。 这样做时,我们将重用前面文章中介绍的 “标题” 控件。
public class HeadlineListEx : CompositeDataBoundControl { : }
HeadlineListEx 控件对收集所有绑定数据项的 Items 集合属性进行计数。 集合是公共的,并且还可以在大多数列表控件中以编程方式填充集合。 通过几个属性(DataTextField 和 DataTitleField)实现对经典数据绑定的支持。 这些属性指示数据源上的字段,这些字段将用于填充新闻的标题和文本。 Items 集合将保存到 viewstate。
若要将 HeadlineListEx 控件转换为真正的复合控件,请先从 CompositeDataBoundControl 派生该控件,然后重写 CreateChildControls。 值得注意的是, CreateChildControls 已重载。
override int CreateChildControls() override int CreateChildControls(IEnumerable data, bool dataBinding)
第一个重载重写 在 Control 类上定义的 方法。 第二个重载是每个复合控件必须重写的抽象方法。 实际上,开发复合控件可以减少到两个main任务:
- 重写 CreateChildControls。
- 实现 Rows 集合属性以跟踪控件的所有构成项。
Rows 属性不同于 Items,因为它不保留在 viewstate 中,具有与请求相同的生存期,并且引用帮助程序对象,而不是绑定数据项。
public virtual HeadlineRowCollection Rows { get { if (_rows == null) _rows = new HeadlineRowCollection(); return _rows; } }
在生成控件时填充 Rows 集合。 让我们看一下 CreateChildControls 的替代。 方法采用两个参数 - 绑定项和布尔标志,指示是通过数据绑定还是视图状态创建控件。
override int CreateChildControls(IEnumerable dataSource, bool dataBinding) { if (dataBinding) { string textField = DataTextField; string titleField = DataTitleField; if (dataSource != null) { foreach (object o in dataSource) { HeadlineItem elem = new HeadlineItem(); elem.Text = DataBinder.GetPropertyValue(o, textField, null); elem.Title = DataBinder.GetPropertyValue(o, titleField, null); Items.Add(elem); } } } // Start building the hierarchy of controls Table t = new Table(); Controls.Add(t); Rows.Clear(); int itemCount = 0; foreach(HeadlineItem item in Items) { HeadlineRowType type = HeadlineRowType.Simple; HeadlineRow row = CreateHeadlineRow(t, type, item, itemCount, dataBinding); _rows.Add(row); itemCount++; } return itemCount; }
在数据绑定的情况下,首先填充 Items 集合。 循环访问绑定集合、提取数据并填充 新创建的 HeadlineItem 类实例。 接下来,循环访问 Items 集合(可能包含以编程方式添加的其他项),并在 控件中创建行。
HeadlineRow CreateHeadlineRow(Table t, HeadlineRowType rowType, HeadlineItem dataItem, int index, bool dataBinding) { // Create a new row for the outermost table HeadlineRow row = new HeadlineRow(rowType); // Create the cell for a child control TableCell cell = new TableCell(); row.Cells.Add(cell); Headline item = new Headline(); cell.Controls.Add(item); // Fire HERE a HeadlineRowCreated event // Add the row to the HTML table being created t.Rows.Add(row); // Handle the data object binding if (dataBinding) { row.DataItem = dataItem; Headline ctl = (Headline) cell.Controls[0]; ctl.Text = dataItem.Text; ctl.Title = dataItem.Title; // Fire HERE a HeadlineRowDataBound event } return row; }
CreateHeadlineRow 方法创建并返回 HeadlineRow 类的实例,该类是从 TableRow 派生的类。 在本例中,该行包含一个用 标题 控件填充的单元格。 在其他情况下,可以更改此部分代码,以根据需要添加任意数量的单元格,并根据需要填充单元格。
必须按两个不同的步骤(创建和数据绑定)来划分要完成的任务。 首先创建行的布局,触发行创建的事件(如果有),最后将其添加到父表。 接下来,如果控件绑定到数据,则设置对绑定数据敏感的子控件属性。 完成后,将触发行数据绑定事件(如果有)。
请注意,这是更好地描述 ASP.NET 本机复合控件的内部体系结构的模式。
若要触发事件,可以使用以下代码。
HeadlineRowEventArgs e = new HeadlineRowEventArgs(); e.DataItem = dataItem; e.RowIndex = index; e.RowType = rowType; e.Item = row; OnHeadlineRowDataBound(e);
请注意,仅当触发数据绑定事件时,才设置 DataItem 属性。 事件数据结构任意设置为以下内容。 如果你认为它值得,请随意更改它。
public class HeadlineRowEventArgs : EventArgs { public HeadlineItem DataItem; public HeadlineRowType RowType; public int RowIndex; public HeadlineRow Item; }
若要以物理方式触发事件,通常使用受保护的方法,定义如下。
protected virtual void OnHeadlineRowDataBound(HeadlineRowEventArgs e) { if (HeadlineRowDataBound != null) HeadlineRowDataBound(this, e); }
若要声明事件,可以在 ASP.NET 2.0 中使用新的泛型事件处理程序委托。
public event EventHandler<HeadlineRowEventArgs> HeadlineRowDataBound;
在示例页面中,一切照常进行。 在控件标记上定义处理程序,并在代码文件中编写方法。 下面是一个示例。
<cc1:HeadlineListEx runat="server" ID="HeadlineListEx1" DataTextField="notes" DataTitleField="lastname" DataSourceID="MySource" OnHeadlineRowDataBound="HeadlineRowCreated" />
此处显示了 HeadlineRowCreated 事件处理程序的代码。
protected void HeadlineRowCreated(object sender, HeadlineRowEventArgs e) { if (e.DataItem.Title.Contains("Doe")) e.Item.BackColor = Color.Red; }
图 7。 操作中的标题列表Ex 控件
通过挂钩数据绑定事件,包含 Doe 的所有项都呈现红色背景。
复合控件是通过将其他控件聚合到通用 API 的屋顶下创建的控件。 复合控件维护其自己的子控件的实时实例,并且不限制要求它们呈现。可以通过检查页面的跟踪输出中的控件树部分来轻松查看此内容。 使用复合控件有一些好处,例如简化的事件和回发处理。 在 ASP.NET 1.x 中,生成复杂的数据绑定控件有点棘手,需要深入了解一些实现细节。 引入 CompositeDataBoundControl 基类后,ASP.NET,大部分复杂性都抽象化了。 最后,在 ASP.NET 2.0 中,如果需要不绑定数据的复合控件,请使用 CompositeControl 基类。 对于数据绑定复合控件,请改为查看 CompositeDataBoundControl 。 在这两种情况下,都必须提供 CreateChildControls 方法的有效替代,该方法是创建子控件层次结构的任何复合控件的核心。
Dino Esposito 是 Solid Quality Learning 导师,也是 Microsoft ASP.NET 2.0 编程 (Microsoft Press,2005) 的作者。 Dino 总部设在意大利,经常在世界各地的行业活动中发表演讲。 请通过 cutting@microsoft.com 联系或加入博客 https://weblogs.asp.net/despos。