复合控件的创建
创建定制控件的第三种方法是组合二个或二个以上的现有的控件。在下面的例子中,读者将以合同编程人员的身份出现,而我则是客户,我希望读者能够开发一个稍微复杂一些的控件,使我能够用来记录收到的对我的书的询价。
作为客户,我将要求读者开发一个控件,使我能够输入一本或多本书籍,每当点击一本书时,控件就会记录下对该书的点击次数,如下图所示:

这一程序的.aspx文件如下所示,除@ Page命令外,该程序的C#和VB程序是相同的:
利便控件的.aspx文件
<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false" Inherits="CustomControlWebPage.WebForm1" %> <%@ Register TagPrefix="OReilly" Namespace="CustomControls" Assembly="CustomControls" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML> <HEAD> <meta content="Microsoft Visual Studio 7.0" name=GENERATOR> <meta content=C# name=CODE_LANGUAGE> <meta content="JavaScript (ECMAScript)" name=vs_defaultClientScript> <meta content=http://schemas.microsoft.com/intellisense/ie5 name=vs_targetSchema> </HEAD> <body MS_POSITIONING="GridLayout"> <form id=Form1 method=post runat="server"> <OReilly:BookInquiryList Runat="Server" id="bookInquiry1"> <OReilly:BookCounter Runat="server" BookName="Programming ASP.NET" ID="Bookcounter1"/>
<OReilly:BookCounter Runat="server" BookName="Programming C#" ID="Bookcounter2" /> <OReilly:BookCounter Runat="server" BookName="Teach Yourself C++ 21 Days" ID="BookCounter3" /> <OReilly:BookCounter Runat="server" BookName="Teach Yourself C++ 24 Hours" ID="Bookcounter4" /> <OReilly:BookCounter Runat="server" BookName="Clouds To Code" ID="Bookcounter5" /> <OReilly:BookCounter Runat="server" BookName="C++ From Scratch" ID="Bookcounter6" /> <OReilly:BookCounter Runat="server" BookName="Web Classes From Scratch" ID="Bookcounter7" /> <OReilly:BookCounter Runat="server" BookName="XML Web Documents From Srcatch" ID="Bookcounter8" /> </OReilly:BookInquiryList>
</FORM> </body> </HTML> |
在上面的代码中需要注意的是,BookInquiryList组件中包含许多BookCounter元素,其中有一个BookCounter元素是对应着我希望记录的书籍。这个控件非常灵活,我可以对任意数量的书进行记录。每个BookCounter元素有一个用来显示被记录书籍名字的BookName属性。
从图9中我们可以看到,每本书都由一个CountedButton定制控件进行记录,但.aspx文件中没有CountedButton控件的定义,它被完整地封装在了BookCounter定制控件中。
整个体系结构如下所示:
- BookInquiry利便控件是由WebControl派生的,实现了INamingContainer,在下面我们会提到。
- BookInquiry控件有一个由Control类派生的Controls特性。
- 在控件集合中有数量不等的BookCounter控件。
- BookCounter本身也是一个由WebControl派生得来的复合控件,WebControl也实现了INamingContainer。
- BookContainer的每个实例有二个特性:BookName和Count。
- Name特性是由Viewstate支持的,而且通过.aspx文件中的BookName BookName初始化。
- Count特性授权给private性质的CountedButton对象
使用BookInquiry对象的目的有二个:它是BookCounter对象的容器;它负责绘制它本身并确保它包含的BookCounter对象能够按需求绘制自己。
CountedButton控件的修改
我们需要对CountedButton控件进行一些很小的修改,下面分别是C#和VB.NET版的CountedButton控件。
修改后的CountedButton.cs文件
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; namespace CustomControls { // 由System.Web.UI.WebControls.Button派生出的定制控件 public class CountedButton : System.Web.UI.WebControls.Button { private string displayString; // 缺省的构造器 public CountedButton( ) { displayString = "clicks"; InitValues( ); } // 重载,显示字符串 public CountedButton(string displayString) { this.displayString = displayString; InitValues( ); } // 由构造器调用的函数 private void InitValues( ) { if (ViewState["Count"] == null) ViewState["Count"] = 0; this.Text = "Click me"; } // Count是ViewState中的一个特性 public int Count { get { // 在构造器中初始化,不能是NULL return (int) ViewState["Count"]; } set { ViewState["Count"] = value; } } // 覆盖OnClick事件处理程序,增大Count变量的值,并在更新按钮上的文本后调用基础类中的方法 protected override void OnClick(EventArgs e) { ViewState["Count"] = ((int)ViewState["Count"]) + 1; this.Text = ViewState["Count"] + " " + displayString; base.OnClick(e); } } } |
修改后的CountedButton.vb文件
Imports System.ComponentModel Imports System.Web.UI Imports System.Web.UI.WebControls ' 从System.Web.UI.WebControls.Button中派生的定制控件 Public Class CountedButton Inherits System.Web.UI.WebControls.Button Private displayString As String ' 构造器对ViewState进行初始化 Public Sub New( ) displayString = "clicks" Init( ) End Sub ' 重载,显示字符串 Public Sub New(ByVal displayString As String) Me.displayString = displayString Init( ) End Sub ' 由构造器调用的方法 Private Shadows Sub Init( ) If ViewState("Count") = Is Nothing Then ViewState("Count") = 0 Me.Text = "Click me" End If End Sub ' Count是ViewState中的一个特性 Public Property Count( ) As Integer Get Return CInt(ViewState("Count")) End Get Set(ByVal Value As Integer) ViewState("Count") = Value End Set End Property ' 覆盖OnClick事件处理程序,增大Count变量的值,并在更新按钮上的文本后调用基础类中的方法 Protected Overrides Sub OnClick(ByVal e As EventArgs) ViewState("Count") = CInt(ViewState("Count")) + 1 Me.Text = CStr(ViewState("Count") & " " & displayString MyBase.OnClick(e) End Sub End Class?) |
由于我们希望按钮上显示“5 Inquiries”而不是“5 clicks”字符串,因此必须修改OnClick方法中修改按钮字符串文本的代码:
| this.Text = ViewState["Count"] + " " + displayString; |
相应的VB.NET代码是:
| Me.Text = ViewState("Count") & " " & displayString |
我们还使用了一个private性质的成员变量displayString来存储传递给构造器中的数值:
| private string displayString; |
在VB.NET中的代码为:
| Private displayString As String |
我们必须在构造器中设置这一字符串。为了保护已经使用了缺省的构造器的代码,我们必须重载构造器,新增加一个带有字符串参数的构造器:
public CountedButton(string displayString) { this.displayString = displayString; Init( ); } |
在VB.NET中的代码是:
Public Sub New(ByVal displayString As String) Me.displayString = displayString Initialize( ) End Sub |
我们可以对缺省的构造器进行修改,将displayString成员变量有值设置为一个合理的缺省值。其C#代码如下:
public CountedButton( ) { displayString = "clicks"; InitValues( ); } |
相应的VB.NET代码是:
Public Sub New( ) displayString = "clicks" Init( ) End Sub
|
二个构造器的代码都没有考虑private性质的辅助方法Init,它能够保证Count特性被初始化为0,并设置最初时按钮显示的字符串:
private void Init( ) { if (ViewState["Count"] == null) ViewState["Count"] = 0; this.Text = "Click me"; } |
在VB.NET中,相应的代码为:
Private Shadows Sub Init( ) If ViewState("Count") = Nothing Then ViewState("Count") = 0 Me.Text = "Click me" End If End Sub |
作了上述的修改后,我们就可以在第一个复合控件━━BookCounter中使用CountedButton了。
BookCounter复合控件的创建
BookCounter复合控件用于记录和显示对某一本书查询的次数,下面分别是C#和VB.NET版的BookCounter复合控件的源代码:
C#版本的BookCounter控件源文件:BookCounter.cs
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; namespace CustomControls { public class BookCounter : System.Web.UI.WebControls.WebControl, INamingContainer {
// 初始化按钮成员 CountedButton btn = new CountedButton("inquiries"); public string BookName { get { return (string) ViewState["BookName"]; } set { ViewState["BookName"] = value; } } public int Count { get { return btn.Count; } set { btn.Count = value; } } public void Reset( ) { btn.Count = 0; } protected override void CreateChildControls( ) { Controls.Add(btn); } } } |
VB.NET版的BookCounter控件的源代码: BookCounter.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.ComponentModel Public Class BookCounter Inherits System.Web.UI.WebControls.WebControl Implements INamingContainer ' 初始化按钮成员 Public btn As CountedButton = New CountedButton("inquiries") Public Property BookName( ) As String Get Return CStr(ViewState("BookName")) End Get Set(ByVal Value As String) ViewState("BookName") = Value End Set End Property Public Property Count( ) As Integer Get Return btn.Count End Get Set(ByVal Value As Integer) btn.Count = Value End Set End Property Public Sub Reset( ) btn.Count = 0 End Sub Protected Overrides Sub CreateChildControls( ) Controls.Add(btn) End Sub End Class |
INamingContainer
BookCounter类中首先需要注意的是它实现了INamingContainer界面,这是一个没有方法的“记分器”界面。这一界面的目的是识别创建新的ID名字空间的容器控件,保证所有的子控件都有对于应用程序是唯一的ID。
包含CountedButton
BookCounter类包含有CountedButton的一个实例:
| CountedButton btn = new CountedButton("inquiries"); |
或:
| Public btn As CountedButton = New CountedButton("inquiries") |
btn成员是在由System.Control继承的CreateChildControls方法中被初始化的:
protected override void CreateChildControls( ) { Controls.Add(btn); } |
在VB.NET中,相应的代码为:
Protected Overrides Sub CreateChildControls( ) Controls.Add(btn) End Sub |
CreateChildControls是在绘制的准备工作时被调用的,它使BookCounter类能够添加btn对象作为被包含的控件。
BookCounter无需覆盖Render方法,它唯一需要绘制的是CountedButton。Render缺省的操作是绘制所有的子控件,因此无需对它作任何修改就能完成其功能。
BookCounter还有二个特性:BookName和Count。BookName是在控件中显示的一个字符串,而且通过ViewState进行管理,其C#代码如下所示:
public string BookName { get { return (string) ViewState["BookName"]; } set { ViewState["BookName"] = value; } } |
相应的VB.NET源代码为:
Public Property BookName( ) As String Get Return CStr(ViewState("BookName")) End Get Set(ByVal Value As String) ViewState("BookName") = Value End Set End Property |
Count是对一本特定的书查询的数量,记录这一数量的任务由CountedButton完成。其C#代码如下所示:
public int Count { get { return btn.Count; } set { btn.Count = value; } } |
相应的VB.NET源代码为:
Public Property Count( ) As Integer Get Return btn.Count End Get Set(ByVal Value As Integer) btn.Count = Value End Set End Property |
无需将该值放在ViewState中,因为按钮本身就可以对其数据进行管理。
BookInquiryList复合控件的创建
每个BookCounter对象都被包含在BookInquiryList控件集合中,该控件没有特性,只有一个Render方法,其C#和VB.NET代码如下所示:
BookInquiryList的C#代码:
[ControlBuilderAttribute(typeof(BookCounterBuilder)),ParseChildren(false)] public class BookInquiryList : System.Web.UI.WebControls.WebControl, INamingContainer { protected override void Render(HtmlTextWriter output) { int totalInquiries = 0; BookCounter current; // 输出头部 output.Write("<Table border='1' width='90%' cellpadding='1'" + "cellspacing='1' align = 'center' >"); output.Write("<TR><TD colspan = '2' align='center'>"); output.Write("<B> Inquiries </B></TD></TR>"); // 如果没有被包含的控件,输出相应的信息 if (Controls.Count == 0) { output.Write("<TR><TD colspan = '2'> align='center'"); output.Write("<B> No books listed </B></TD></TR>"); } // 否则绘制每个包含的控件 else { // 遍历控件集,显示每个控件的书名,然后让每个被包含的控件自己绘制自己 for (int i = 0; i < Controls.Count; i++) { current = (BookCounter) Controls[i]; totalInquiries += current.Count; output.Write("<TR><TD align='left'>" + current.BookName + "</TD>"); output.RenderBeginTag("TD"); current.RenderControl(output); output.RenderEndTag( ); output.Write("</tr>"); } output.Write("<TR><TD colspan='2' align='center'> " + " Total Inquiries: " + totalInquiries + "</TD></TR>"); } output.Write("</TABLE>"); } } |
BookInquiryList的VB.NET源代码:
Imports System.ComponentModel Imports System.Web.UI <ControlBuilder(GetType(BookCounterBuilder)), ParseChildren(False)> _ Public Class BookInquiryList Inherits System.Web.UI.WebControls.WebControl Implements INamingContainer Protected Overrides Sub Render(ByVal output As HtmlTextWriter) Dim totalInquiries As Integer = 0 ' 输出头部 output.Write("<Table border='1' width='90%' cellpadding='1'" & _ "cellspacing='1' align = 'center' >") output.Write("<TR><TD colspan = '2' align='center'>") output.Write("<B> Inquiries </B></TD></TR>") ' 如果没有被包含的控件,输出相应的信息 If Controls.Count = 0 Then output.Write("<TR><TD colspan = '2'> align='center'") output.Write("<B> No books listed </B></TD></TR>") ' 否则绘制每个包含的控件 Else ' 遍历控件集,显示每个控件的书名,然后让每个被包含的控件自己绘制自己 Dim current As BookCounter For Each current In Controls totalInquiries += current.Count output.Write("<TR><TD align='left'>" & _ current.BookName + "</TD>") output.RenderBeginTag("TD") current.RenderControl(output) output.RenderEndTag() output.Write("</tr>") Next Dim strTotalInquiries As String strTotalInquiries = totalInquiries.ToString output.Write("<TR><TD colspan='2' align='center'> " & _ " Total Inquiries: " & _ CStr(strTotalInquiries) & "</TD></TR>") End If output.Write("</TABLE>") End Sub End Class Friend Class BookCounterBuilder Inherits ControlBuilder Public Overrides Function GetChildControlType( _ ByVal tagName As String, ByVal attributes As IDictionary) As Type If tagName = "BookCounter" Then Dim x As BookCounter Return x.GetType Else Return Nothing End If End Function Public Overrides Sub AppendLiteralString(ByVal s As String) End Sub End Class |
ControlBuilder和ParseChildren属性
BookCounter类必须与BookInquiryClass联合使用,ASP.NET才能将.aspx网页中的元素转换为适当的代码。完成这一工作需要用到ControlBuilder属性:
[ControlBuilderAttribute(typeof(BookCounterBuilder)),ParseChildren(false)]
ControlBuilderAttribute的参数是一个通过传递BookCounterBuilder类获取的Type对象。下面是使用C#和VB.NET编写的BookCounterBuilder源代码:
C#语言版的BookCounterBuilder
internal class BookCounterBuilder : ControlBuilder { public override Type GetChildControlType( string tagName, IDictionary attributes) { if (tagName == "BookCounter") return typeof(BookCounter); else return null; } public override void AppendLiteralString(string s) { } } VB.NET版的BookCounterBuilder
Friend Class BookCounterBuilder Inherits ControlBuilder Public Overrides Function GetChildControlType(_ ByVal tagName As String, ByVal attributes As Idictionary) As Type If tagName = "BookCounter" Then Dim x As BookCounter Return x.GetType Else Return Nothing End If End Function Public Overrides Sub AppendLiteralString(ByVal s As String) End Sub End Class |
ASP.NET将使用由ControlBuilder中派生出的BookCounterBuilder来判断由BookCounter标记批指示的对象的类型。通过这种结合,每个BookCounter对象才能被实例化,并添加到BookInquiryClass的控件集合中。
第二个参数ParseChildren必须被设置成false,让ASP.NET知道我们已经对子属性进行了处理,无需再对它进行进一步地解析了。false值表明子属性不是外部对象的特性,而仅仅是子控件的特性。
Render
BookInquiryClass中的唯一方法是覆盖Render的方法,Render的作用是使用由每个BookCounter子控件管理的数据绘制图9中的表格。
BookInquiryClass提供了一个总的查询数字,如下图所示:

Figure 14-16. Total inquiries displayed
通过将totalInquiries整型变量初始化为0,然后依次遍历每个控件,将其Count特性与totalInquiries相加,就可以得到查询的总数了。除了C#中语句结束时的分号为,实现这一功能的C#和VB.NET语句相同:
totalInquiries += current.Count;
表现输出内容
通过遍历每个控件,下面的代码能够绘制出每个子控件:
for (int i = 0; i < Controls.Count; i++) { current = (BookCounter) Controls[i]; totalInquiries += current.Count; output.Write("<TR><TD align='left'>" + current.BookName + "</TD>"); output.RenderBeginTag("TD"); current.RenderControl(output); output.RenderEndTag( ); output.Write("</tr>"); } |
相应的VB.NET代码为:
For Each current in Controls totalInquiries += current.Count output.Write("<TR><TD align='left'>" & _ current.BookName + "</TD>") output.RenderBeginTag("TD") current.RenderControl(output) output.RenderEndTag( ) output.Write("</tr>") Next
|
局部的BookCounter对象型current变量的值被赋为控件集合中的每个对象:
for (int i = 0; i < Controls.Count; i++) { current = (BookCounter) Controls[i]; |
然后就可以利用下面的代码计算totalInquiries:
| totalInquiries += current.Count; |
然后我们就可以继续绘制对象了。通过利用current的BookName特性,HtmlTextWriter可以用来创建一个行并显示书的名字:
| output.Write("<TR><TD align='left'>" + current.BookName + "</TD>"); |
接下来,我们绘制一个TD标记,在该标记内我们让BookCounter对象绘制自己。最后,使用RenderEndTag来绘制一个结束性的TD标记,并使用HTMLTextWriter的Write方法绘制行结束标记。
output.RenderBeginTag("TD"); current.RenderControl(output); output.RenderEndTag( ); output.Write("</tr>"); |
下面的代码使被包含的控件自己绘制自己:
| current.RenderControl(output); |
上面的代码会调用BookCounter的Render方法。由于我们没有覆盖该方法,调用的仍然是基础类中的Render 类。唯一被包含的对象是CountedButton,由于我们没有覆盖CountedButton中的Render方法,在绘制该按钮时调用的仍然是基础类Button中的Render方法。
查询总数的绘制
所有的子控件绘制完毕后,BookInquiryList将创建一个新的行,显示总的查询次数:
output.Write("<TR><TD colspan='2' align='center'> " + " Total Inquiries: " + totalInquiries + "</TD></TR>"); |
至此,作为合同编程人员的你,就完成了任务。
结束语
在上面的文章中,我们讨论了开发定制控件的三种途径,尤其是通过一个例子,详细论述了建立复合控件的过程。在具体的工作中,灵活地运用这三种方式创建定制控件,使我们的开发工作事半功倍。
(出处:赛迪网) |