ASP.NET 2.0服务器控件之复合控件概述

 在前面的文章中,主要介绍了服务器控件的基本概念、基本理论,这些内容是构建所有自定义服务器控件的基石。然而,仅仅依靠这些知识还不足以创建出优秀的服务器控件。因为,不同类型的服务器控件具有不同的创建方法,开发人员必须在掌握基本概念和理论之后,掌握不同类型服务器控件的开发方法。本文及其随后几篇文章将详细介绍与创建复合控件相关的内容。本文重点介绍有关复合控件的概念、创建方法等理论,然后,通过一个典型示例加深读者对于复合控件创建方法的理解。

  复合控件概述

  复合控件中的“复合”一词表明该类型控件本质上由多个组件组合而成。同时,复合控件对外暴露的成员对象通常由构成组件的方法和属性提供,并且可能加入一些新的成员。复合控件也可以实现自定义事件,并处理并引发子控件所引起的事件。就功能方面而言,复合控件的功能要比简单组合几个控件的功能要强大的多,而且很多时候具有一定的专项性。例如,ASP.NET 2.0新增的Login控件就是一个典型的复合控件。该控件用户界面由多个单独控件组合而成,并且使用单一的API对控件进行设置和访问。另外,Login控件由于与成员资格等功能集成的原因,因此,其具有快速实现用户登录的功能。

  可能部分有经验的读者在了解了复合控件的基本概念之后会有所疑惑:复合控件与用户控件好像非常相似,那么它们之间有什么区别吗?到底什么时候创建复合控件,什么时候创建用户控件呢?回答这个问题,我们必须从用户控件的基本概念入手进行研究。

  简单而言,用户控件是指在一个项目中,由于同样一些功能模块在多处引用,例如,导航菜单等,可以把这一块代码做成一个用户控件,然后,在需要引用的页面中注册后,直接按控件使用的方式引用,省去了重复编写相同代码的工作。就复合控件与用户控件的区别而言,主要可以总结为以下几点:

  一、复合控件创作的最短设计时支持,用户控件创作的完全设计时支持。在可视化设计器中,创作用户控件与创作ASP.NET页没有差别。

  二、复合控件是以目标为公共语言运行库的面向对象的编程语言,如C#,是用编程方式创作的用户控件是使用ASP.NET页语法和脚本块声明性地创作的。

  三、复合控件是作为程序集(.dll)编译和保持的。用户控件是带有.ascx扩展名的文本文件。

  四、复合控件非常适于创作一般的可重新发布的控件,用户控件适合应用程序特定的功能。

  五、可将复合控件添加到可视化设计器的工具箱中并拖放到页面上,使用时可以在属性框中设计,用户控件只能在HTML中编写。

  通过以上内容,相信读者已经能够基本了解了复合控件。下面介绍一下创建复合控件的实现方法。在这个过程中,开发人员必须把握以下几个要点:

  第一、通常情况,复合控件类必须派生自System.Web.UI.WebControls.CompositeControl类。这一点与ASP.NET 1.x环境下开发复合控件有些不同。在ASP.NET 1.x中,复合控件必须实现System.Web.UI.INamingContainer接口。然而,在ASP.NET 2.0下,复合控件类的基类则发生了变化。下面简单介绍一下CompositeControl类。

  CompositeControl类是一个抽象类,该类可为自定义控件提供命名容器和控件设计器功能,并且可包含全部子控件或使用其他控件功能。CompositeControl类的声明代码如下所示:





public abstract class CompositeControl : WebControl, INamingContainer, ICompositeControlDesignerAccessor

  如上代码所示,CompositeControl类基层自WebControl基类,并且实现INamingContainer和ICompositeControlDesignerAccessor接口。INamingContainer是一个没有方法的标记接口。当控件在实现INamingContainer时,页框架可在该控件下创建新的命名范围,因此,能够确保子控件在控件的分层树中具有唯一的名称。当复合控件公开模板属性,提供数据绑定或需要传送事件到子控件时,这是非常重要的。ICompositeControlDesignerAccessor接口使复合控件设计器可以在设计时重新创建其关联控件的子控件。该接口包含一个需要实现的方法RecreateChildControls。该方法使复合控件的设计器可以在设计时重新创建该控件的子控件。

  另外,如果创建的是数据绑定复合控件,那么自定义控件类的基类应该是CompositeDataBoundControl。有关该类的具体内容,请读者查阅相关资料。

  第二、必须重写Control基类的CreateChildControls方法,以便对子控件进行初始化、实例化,并将其添加到控件树中。CreateChildControls用于通知使用基于合成实现的服务器控件,创建它们包含的任何子控件,以便为回发或呈现做准备。重写该方法是实现复合控件的关键所在。这种类撰写的方法将通知.NET框架有关复合控件中包含哪些子控件,以及各个子控件在控件树中的位置和关系等内容。通过这种方法,复合控件将复用子控件提供的实现来进行呈现、事件处理、样式及其他功能。

  在实现复合控件过程中,除了掌握CompositeControl基类和CreateChildControls方法之外,ASP.NET 2.0还提供了与复合控件相关的其他方法和属性,掌握这些成员对于开发复合控件也很重要。下面列举了这些常见方法和属性。

  · protected virtual void EnsureChildControls()

  该方法用于确定服务器控件是否包含子控件。如果不包含,则创建子控件。该方法首先检查 ChildControlsCreated 属性的当前值。如果此值为假,则调用CreateChildControls方法。当需要确保已创建子控件时,将调用该方法。大多数情况下,自定义服务器控件的开发人员无需重写此方法。如果确实重写了此方法,请按与其默认行为相似的方式来使用。

  · public virtual Control FindControl(string)

  该方法用于在当前的命名容器中搜索指定的服务器控件。

  · public virtual bool HasControls()

  该方法用于确定服务器控件是否包含任何子控件。如果控件包含其他控件,则为true;否则为 false。由于该方法仅确定是否存在任何子控件,它可以通过允许您避免不必要的Controls.Count属性调用来改进性能。调用此属性要求实例化ControlCollection对象。如果没有子级,则创建该对象会浪费服务器资源。

  · protected virtual void DataBindChildren ()

  该方法是ASP.NET 2.0新增内容,其用于将数据源绑定到服务器控件的子控件。这为开发数据绑定类型的复合控件提供了便利。然而,需要注意的是,在服务器控件上调用此方法时,此方法不会将数据绑定到控件。若要绑定服务器控件及其所有子控件,请调用DataBind方法。

  · protected bool HasEvents ()

  这也是一个ASP.NET 2.0新增的方法,其用于返回一个值,该值指示是否为控件或任何子控件注册事件。如果注册事件,则为true;否则为false。

  · Controls属性

  该属性的数据类型为ControlCollection,其用于获取ControlCollection对象,该对象表示 UI 层次结构中指定服务器控件的子控件。其属性值指定服务器控件的子控件集合。

  · NamingContainer属性

  该属性的数据类型为Control,其用于获取对服务器控件的命名容器的引用,此引用创建唯一的命名空间,以区分具有相同Control.ID属性值的服务器控件。

  · ChildControlsCreated属性

  该属性的数据类型为bool,其用于获取一个值,该值指示是否已创建服务器控件的子控件。如果已创建子控件则为true;否则为false。
  典型应用

#p#副标题#e#
  上文介绍了有关创建复合控件的一些基本知识,下面将通过一个典型应用加深读者对于复合控件实现方法的理解,其重点放在针对复合控件的呈现方法上。

  多数控件呈现通过重写Render方法实现,然而,在复合控件中则大有不同。复合控件由多个子控件组合而成,其呈现逻辑是由子控件提供的。

  据此,在少数较为简单的情况下,复合控件不用重写Render方法,例如,创建一个包含文本框和按钮的复合控件,这时,只要通过类撰写的方法在CreateChildControls中添加相关控件即可,而无需Render方法。但是,在多数情况下,复合控件中既包含子控件,又包含用于格式化和布局的HTML。

  针对这种情况,如果只采取类撰写的实现方法,那么很容易造成错误,并且生成的复合控件性能受到很大影响。最好的解决方法是重写CreateChildControls方法,同时也重写Render方法。在CreateChildControls方法中,为复合控件添加子控件;在Render方法中,为HTTP输出流添加用于格式化和布局的HTML。

  下面列举了呈现复合控件的关键步骤:

  · 控件基类继承自CompositeControl基类。这是在ASP.NET 2.0中创建复合控件的关键所在。

  · 重写CreateChildControls方法,完成实例化、初始化子控件,并且将子控件添加到控件集合中。

  · 重写ICompositeControlDesignerAccessor接口的RecreateChildContrls方法。

  · 如果复合控件中存在用于格式化和布局的HTML,那么建议将这些内容写入Render方法中,而不要在CreateChildControls方法中创建和添加所需的LiteralControl实例。另外,在添加相关HTML代码过程中,为了让子控件使用默认生成方法,必须使每个子控件调用RenderControl方法。

  为了便于读者更好的理解以上内容,下面举例说明。在此示例中,Register控件使用子控件创建用户界面(UI),用于输入用户信息,以向网站注册。此用户界面包括两个TextBox控件(一个用于输入用户名,另一个用于输入用户的电子邮件地址)和一个用于提交信息的Button控件。Register还将RequiredFieldValidator控件与两个TextBox控件关联起来,以确保用户输入了名称和电子邮件地址。复合控件Register源代码如下所示:





using System;
using System.ComponentModel;
using System.Drawing;
using System.Security.Permissions;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace WebControlLibrary{
[
 DefaultProperty(“ButtonText”),
 ToolboxData(“<{0}:Register runat=”server”> </{0}:Register>”),
]

public class Register : CompositeControl {
// 定义私有字段
private Button submitButton;
private TextBox nameTextBox;
private Label nameLabel;
private TextBox emailTextBox;
private Label emailLabel;
private RequiredFieldValidator emailValidator;
private RequiredFieldValidator nameValidator;
// 实现属性ButtonText

[
 Bindable(true),
 Category(“Appearance”),
 DefaultValue(“”),
 Description(“按钮上的文字内容.”)
]

public string ButtonText {
 get {
  EnsureChildControls();
  return submitButton.Text;
 }
 set {
  EnsureChildControls();
  submitButton.Text = value;
 }
}

// 实现属性Name

[
 Bindable(true),
 Category(“Default”),
 DefaultValue(“”),
 Description(“用户名.”)
]

public string Name {
 get {
  EnsureChildControls();
  return nameTextBox.Text;
 }
 set {
  EnsureChildControls();
  nameTextBox.Text = value;
 }
}

// 实现属性NameErrorMessage

[
 Bindable(true),
 Category(“Appearance”),
 DefaultValue(“”),
 Description(“用户名验证错误信息.”)
]

public string NameErrorMessage {
 get {
  EnsureChildControls();
  return nameValidator.ErrorMessage;
 }
 set {
  EnsureChildControls();
  nameValidator.ErrorMessage = value;
  nameValidator.ToolTip = value;
 }
}

// 实现属性NameLabelText

[
 Bindable(true),
 Category(“Appearance”),
 DefaultValue(“”),
 Description(“用户名文本框旁的文字.”)
]

public string NameLabelText {
 get {
  EnsureChildControls();
  return nameLabel.Text;
 }
 set {
  EnsureChildControls();
  nameLabel.Text = value;
 }
}

// 实现属性Email

[
 Bindable(true),
 Category(“Default”),
 DefaultValue(“”),
 Description(“邮件地址.”)
]

public string Email {
 get {
  EnsureChildControls();
  return emailTextBox.Text;
 }
 set {
  EnsureChildControls();
  emailTextBox.Text = value;
 }
}

// 实现属性EmailErrorMessage

[
 Bindable(true),
 Category(“Appearance”),
 DefaultValue(“”),
 Description(“邮件地址验证错误信息.”)
]

public string EmailErrorMessage {
 get {
  EnsureChildControls();
  return emailValidator.ErrorMessage;
 }
 set {
  EnsureChildControls();
  emailValidator.ErrorMessage = value;
  emailValidator.ToolTip = value;
 }
}

// 实现属性EmailLabelText

[
 Bindable(true),
 Category(“Appearance”),
 DefaultValue(“”),
 Description(“电子邮件文本框旁的文字.”)
]

public string EmailLabelText {
 get {
  EnsureChildControls();
  return emailLabel.Text;
 }
 set {
  EnsureChildControls();
  emailLabel.Text = value;
 }
}

// 重写ICompositeControlDesignerAccessor接口的RecreateChildContrls方法

protected override void RecreateChildControls() {
 EnsureChildControls();
}

// 重写Control基类的CreateChildControls方法

protected override void CreateChildControls() {
 // 清除所有子控件
 Controls.Clear();
 nameLabel = new Label();
 nameTextBox = new TextBox();
 nameTextBox.ID = “nameTextBox”;
 nameValidator = new RequiredFieldValidator();
 nameValidator.ID = “validator1”;
 nameValidator.ControlToValidate = nameTextBox.ID;
 nameValidator.Text = NameErrorMessage;
 nameValidator.Display = ValidatorDisplay.Static;
 emailLabel = new Label();
 emailTextBox = new TextBox();
 emailTextBox.ID = “emailTextBox”;
 emailValidator = new RequiredFieldValidator();
 emailValidator.ID = “validator2”;
 emailValidator.ControlToValidate = emailTextBox.ID;
 emailValidator.Text = EmailErrorMessage;
 emailValidator.Display = ValidatorDisplay.Static;
 submitButton = new Button();
 submitButton.ID = “button1”;
 this.Controls.Add(nameLabel);
 this.Controls.Add(nameTextBox);
 this.Controls.Add(nameValidator);
 this.Controls.Add(emailLabel);
 this.Controls.Add(emailTextBox);
 this.Controls.Add(emailValidator);
 this.Controls.Add(submitButton);
}

// 重写Render方法

protected override void Render(HtmlTextWriter writer) {
 AddAttributesToRender(writer);
 writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, “1”, false);
 writer.RenderBeginTag(HtmlTextWriterTag.Table);
 writer.RenderBeginTag(HtmlTextWriterTag.Tr);
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 nameLabel.RenderControl(writer);
 writer.RenderEndTag();
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 nameTextBox.RenderControl(writer);
 writer.RenderEndTag();
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 nameValidator.RenderControl(writer);
 writer.RenderEndTag();
 writer.RenderEndTag();
 writer.RenderBeginTag(HtmlTextWriterTag.Tr);
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 emailLabel.RenderControl(writer);
 writer.RenderEndTag();
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 emailTextBox.RenderControl(writer);
 writer.RenderEndTag();
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 emailValidator.RenderControl(writer);
 writer.RenderEndTag();
 writer.RenderEndTag();
 writer.RenderBeginTag(HtmlTextWriterTag.Tr);
 writer.AddAttribute(HtmlTextWriterAttribute.Colspan, “2”, false);
 writer.AddAttribute(HtmlTextWriterAttribute.Align, “right”, false);
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 submitButton.RenderControl(writer);
 writer.RenderEndTag();
 writer.RenderBeginTag(HtmlTextWriterTag.Td);
 writer.Write(“ ”);
 writer.RenderEndTag();
 writer.RenderEndTag();
 writer.RenderEndTag();
}
}
}

  以上列举了复合控件类Register的源代码。虽然代码有些冗长,然而还是比较容易理解的。下图1列举了Register类结构图。






ASP.NET2.0服务器控件之复合控件概述(2)
图1 类结构图


  如图1并结合代码可知,Register类继承自CompositeControl基类,其实现了7个属性和3个方法。属性包括ButtonText、Email、EmailErrorMessage、EmailLabelText、Name、NameErrorMessage和NameLabelText。这些属性通过命名,相信读者基本可以了解其意义。

#p#副标题#e#


  另外,Register类中重写了来自不同对象的3个方法。

  (1)Render方法隶属于Control基类,在本例中主要在该方法中定义了一些与控件布局相关的HTML等内容。

  (2)CreateChildControls方法隶属于Control基类。在本示例中通过重写该方法,实现了将子控件添加到复合控件树中的任务。请读者牢记:每当需要Controls集合时,例如,在数据绑定期间(如果适用),服务器控件结构都会依赖对CreateChildControls的调用。为此,必须在CreateChildControls方法中添加子控件。

  (3)RecreateChildControls方法来自CompositeControl基类的ICompositeControlDesignerAccessor接口。通过实现这个方法可使复合控件的设计器可以在设计时重新创建该控件的子控件。

  下面列举了为测试Register控件而创建的Default.aspx页面源代码。






<%@ Page Language=”C#” AutoEventWireup=”true” CodeFile=”Default.aspx.cs” Inherits=”_Default” %>
<%@ Register TagPrefix=”Sample” Assembly=”WebControlLibrary” Namespace=”WebControlLibrary” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head runat=”server”>
<title>实现复合控件呈现</title>
</head>
<body>
<form id=”form1″ runat=”server”>
<div>
<Sample:Register id=”demo1″ runat=”server” ButtonText=”注册” EmailLabelText=”电子邮件” NameLabelText=”用户名” EmailErrorMessage=”不能为空” NameErrorMessage=”不能为空” />
</div>
</form>
</body>
</html>



  示例效果如图2所示。



ASP.NET2.0服务器控件之复合控件概述(3)
图2 应用程序效果图


  如图2所示,复合控件Register在页面中显示了文本、文本框、按钮等,同时,还提供了输入验证功能。然而,当用户单击“注册”按钮之后,即使在用户名和电子邮件的输入都通过验证的情况下,页面仍然没有引发相应的事件处理程序。这是由于在本例中没有实现按钮的事件处理内容。有关复合控件的事件实现将在随后的文章中进行详细讲解。

  小结

  复合控件是通过将其他控件聚合在某一公用API下创建而成的控件。复合控件将保留自己子控件的活动实例,并且不仅限于呈现这些实例。使用复合控件可以带来几点好处,例如可以简化对事件和回发的处理。然而,本文并没有对复合控件的事件实现等相关内容进行讲解,请读者继续关注本系列的随后文章。