上一章详细讨论了为什么需要开发自定义控件和组件。ASP.NET服务器控件有两种基本类型:自定义控件和用户控件。为实现自定义控件必须使用编程语言,例如C#或者VB.NET,编写一个继承自如Control基类的类。可以使用与实现普通ASP.NET页面相同的方法实现用户控件,可以使用相同的拖放方法,所见即所得(WYSIWYG)的开发用户控件。
本章首先介绍自定义控件,然后讲解实现简单自定义控件的细节,如以下内容所示。
● 实现继承自Control基类的自定义控件,重写其Render方法。
● 部署自定义控件。
● 在ASP.NET页面中使用自定义控件。
● 向自定义控件添加属性,以便允许页面开发人员自定义属性。
● 向自定义控件添加设计时属性(Attribute),以便使其在Visual Studio环境中类似于标准ASP.NET服务器。
● 将自定义控件添加到Visual Studio的工具箱中,这样就可以在Visual Studio设计窗口拖放控件。
本章还介绍了实现简单用户控件的细节,您将了解如何向用户控件添加属性和方法,如何在Web页面中使用用户控件。
考虑如图2-1所示的Web页面,其收集用户的信用卡信息,并将页面回传给服务器。该Web页面包括4个文本、3个下拉框、2个文本框和1个按钮。如示例2-1所示,可以轻松实现该Web页的HTML标记文本。
图2-1
示例2-1:图2-1所示Web页的HTML标记文本
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Untitled Page </title></head>
<body>
<form method="post" action="Default.aspx" id="form1">
<table id="ccf" style="font-weight: bold;">
<tr>
<td><strong>Payment Method</strong></td>
<td>
<select style="width: 100%;">
<option value="Visa">Visa</option>
<option value="MasterCard">MasterCard</option>
</select>
</td>
</tr>
<tr>
<td><strong>Credit Card No.</strong></td><td><input type="text" /></td>
</tr>
<tr>
<td><strong>Cardholder’s Name</strong></td><td><input type="text" /></td>
</tr>
<tr>
<td><strong>Expiration Date</strong></td>
<td>
<select>
<option value="1">01</option>
...
</select>
<select>
<option value="2005">2005</option>
...
</select>
</td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="Submit" /></td>
</tr>
</table>
</form>
</body>
</html>
在本章读者将学习如何将包含打开和关闭标记的表单元素的嵌套HTML标记文本,转换为名为CreditCardForm1的服务器控件,开发人员使得该控件隐藏了HTML标记文本。这样可以仅使用一行标记文本来实现如图2-1所示的相同Web页,如示例2-2中突出代码行所示。
示例2-2:自定义控件CreditCardForm1隐藏实现内容
<html xmlns="http://www.w3.org/1999/xhtml" >
<head><title>Untitled Page</title></head>
<body>
<form id="form1" runat="server">
<custom:creditcardform1 runat="server" ID="ccf" />
</form>
</body>
</html>
可以认为CreditCardForm1控件作为一个容器封装了示例2-1所示的HTML代码。自定义控件CreditCardForm1继承自标准的ASP.NET基类Control。每个服务器控件都直接或者间接继承自Control基类。实际上,继承该基类的就是一个服务器控件。
基类为服务器控件提供了在ASP.NET框架中运作所需要的基础架构。这些基础性方法之一是一个名为Render的方法。该方法用于为服务器控件生成或者呈现HTML标记文本。
单词“Render”在服务器控件开发的上下文中有一个特殊的含义。其不同于该词通常所讲的“Rendering(表现)”之意,即指从服务器获取并由Web浏览器显示或者呈现HTML标记文本。在服务器控件开发的上下文中,Rendering是指生成服务器发送给浏览器的HTML标记文本。换言之,服务器控件呈现(生成)浏览器呈现(显示)的HTML标记文本。
在运行时,页面创建一个HtmlTextWriter类实例,并且将该示例传给页面所包含服务器的Render方法。HtmlTextWriter类公开了一个Write方法。服务器控件可使用该方法编写输出的HTML标记文本。示例2-3包含了CreditCardForm1控件中实现的Render方法代码。这个方法真的很简单。它将逐行扫描整个示例2-1中的HTML代码行,并将这些代码行传递给HtmlTextWriter对象的Writer方法。
例如,HTML代码的第一行是一个打开的table元素和它的属性:
<table style='width:287px;height:124px;border-width:0;'>
将以上HTML代码行简单的传递给HtmlTextWriter实例的Write方法:
writer.Write("<table style='width:287px;height:124px;border-width:0;'>");
对示例2-1中的每个HTML代码行都会重复同样的做法。正如所看到的,实现简单自定义控件的方法十分简单,步骤如下。
(1)写下需要生成的HTML(例如示例2-1所示的HTML)。
(2)编写一个继承自Control基类的类。
(3)重写Render方法,将HTML逐行传递给HtmlTextWriter的Write方法。
示例2-3:自定义控件CreditCardForm1
namespace CustomComponents
{
public class CreditCardForm1 : Control
{
protected override void Render(HtmlTextWriter writer)
{
writer.Write("table style='width:287px;height:124px;
border-width:0;'>");
writer.Write("<tr>");
writer.Write("<td>Payment Method</td>");
writer.Write("<td>");
writer.Write("<select name='PaymentMethod' id='PaymentMethod'
style='width:100%;'>");
writer.Write("<option value='0'>Visa</option>");
writer.Write("<option value='1'>MasterCard</option>");
writer.Write("</select>");
writer.Write("</td>");
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td>Credit Card No.</td>");
writer.Write("<td><input name='CreditCardNo' id='CreditCardNo'
type='text' /></td>");
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td>Cardholder's Name</td>");
writer.Write("<td><input name='CardholderName' id='CardholderName'
type='text' /></td>");
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td>Expiration Date</td>");
writer.Write("<td>");
writer.Write("<select name='Month' id='Month'>");
for (int month = 1; month < 13; day++)
{
if (month < 10)
writer.Write("<option value='" + month.ToString() + "'>" + "0" +
month.ToString() + "</option>");
else
writer.Write("<option value='" + month.ToString() + "'>" +
month.ToString() + "</option>");
}
writer.Write("</select>");
writer.Write(" ");
writer.Write("<select name='Year' id='Year'>");
for (int year = 2005; year < 2015; year++)
{
writer.Write("<option value='" + year.ToString() + "'>" +
year.ToString() + "</option>");
}
writer.Write("</select>");
writer.Write("</td>");
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td align='center' colspan='2'>");
writer.Write("<input type='submit' value='Submit' />");
writer.Write("</td>");
writer.Write("</tr>");
writer.Write("</table>");
base.Render(writer);
}
}
}
HtmlTextWriter类继承自TextWriter抽象基类。TextWriter基类的每个派生类都特别设计用于将文本字符流写入指定的存储中。例如,StringWriter和StreamWriter派生类可将文本字符流分别写入字符串和流(例如FileStream)。另一方面,TextWriter基类的派生类HtmlTextWriter被特别设计用于将HTML标记文本字符流写入服务器控件的输出流中。
将HTML标记文本封装到单个服务器控件中,具有以下优点。
● 不中断现有应用程序即可优化封装的HTML标记文本。例如,可以修改示例2-3中所示的Render方法,将不同类型的HTML传递给用于该控件的Write方法,因为示例并不知道所传递给Write方法的是什么HTML。这不会对示例2-2有任何影响。
这是一个说明面向对象隐藏信息的方法的示例,组件(CreditCardForm1)对客户端隐藏了实现细节(HTML标记文本)。这为隐藏信息提供了显著优势。
● 其他人能够编写新的继承自原有控件特性的自定义控件,同时还能够对原有控件进行扩展,以支持新特性,而不必重新实现已经支持的控件特性。
● 页面开发人员能够使用较少的标记文本来使用控件,如示例2-2所示。
● 可将控件添加到Visual Studio工具箱中,这样页面开发人员能够将控件拖放到设计窗口中
必须将自定义控件编译为程序集,并将程序集部署到服务器端,这样才能够在Web应用程序中使用自定义控件。如何实现部署程序集依赖于如何编译自定义控件。当开始部署自定义控件时有两个选项。一个选项是将包含自定义控件的程序集添加到使用该程序集的Web应用程序的bin文件夹内。这样只有该应用程序的页面才能够使用这个自定义控件。如果考虑使用这个选项,那么可以从命令行生成程序集,其使用如下命令编译CreditCardForm1.cs文件:
csc /t:library /out:CustomComponents.dll /r:System.dll /r:System.Web.dll
CreditCardForm1.cs
另一个部署选项是将包含自定义控件的程序集添加到服务器的全局程序集缓存(Global Assembly Cache,缩写为GAC)中,这样运行于该服务器上的所有Web应用程序都可以使用该自定义控件。然而,只有强命名程序集才能被添加到GAC中。强命名程序集名称包括4个部分:程序集名、版本、文化和公钥标记。如果从命令行生成程序集,可创建一个名为AssemblyInfo.cs的文件(或者其他希望的名称),向该文件添加如下代码,并将该文件移动到CreditCardForm1.cs文件所在的目录中:
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyKeyFile("KeyFile.snk")]
如果在Visual Studio中进行生成,则需将这些代码添加到AssemblyInfo.cs中。
KeyFile.snk是一个签名文件,其用于标记程序集。可使用如下命令来创建签名文件:
sn -k KeyFile.snk
如果从命令行生成,则可使用如下命令来编译和标记CreditCardForm1.cs文件,以便生成一个名为CustomComponents.dll的强命名程序集:
csc /t:library /out:CustomComponents.dll /r:System.dll /r:System.Web.dll AssemblyInfo.cs CreditCardForm1.cs
接下来将强命名程序集CustomComponents.dll添加到GAC中。您可以使用Windows资源管理器访问GAC,并拖放程序集。
在使用自定义控件之前,必须使用Register指令在页面中注册该自定义控件:
<%@ Register TagPrefix="custom" Namespace="CustomComponents" %>
Register指令包括两个重要属性:TagPrefix和Namespace。必须将自定义控件的名字空间设置为Namespace属性值。由于自定义控件CreditCardForm1属于CustomComponent名字空间,因此,Namespace属性则设置为“CustomComponents”。这表示自定义控件必须属于一个名字空间,以便页面开发人员注册它,并在ASP.NET Web页面中声明使用它。以下代码显示了如何在Web页面中使用自定义控件CreditCardForm1:
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="CreditCardForm1.aspx.cs" Inherits="CreditCardForm1" %>
<%@ Register TagPrefix="custom" Namespace="CustomComponents" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head> <title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<custom:creditcardform1 runat="server" id="ccf" />
</form>
</body>
</html>
页面开发人员实例化自定义控件有下面两种不同的方法。
● 使用C#或者VB.NET的new关键字显式创建控件实例:
CreditCardForm1 ccf = new CreditCardForm1();
● 在ASP.NET页面中声明控件:
<custom:CreditCardForm1 runat="server" ID="ccf" />
如果页面开发人员使用C#或者VB.NET代码编程,则自定义控件不必属于某个名字空间。换言之,如果页面开发人员需要在ASP.NET Web页面中声明使用控件,则需要名字空间。也就是说,应该总是将自定义控件封装在名字空间内,因为多数页面开发人员更愿意采用声明方式,而不是使用如C#或者VB.NET等语言进行编程。
图2-1显示了包含4个文本和1个按钮的自定义控件CreditCardForm1。示例2-3中的Render方法对这4个文本和按钮文本都使用了硬编码。示例2-4是该控件的新版本,其命名为CreditCardForm2,其公开了5个公共属性,以便页面开发人员自定义文本。
示例2-4:公开了5个属性的自定义控件CreditCardForm2
namespace CustomComponents
{
public class CreditCardForm2 : Control
{
private string paymentMethodText = "Payment Method";
private string creditCardNoText = "Credit Card No.";
private string cardholderNameText = "Cardholder’s Name";
private string expirationDateText = "Expiration Date";
private string submitButtonText = "Submit";
public virtual string PaymentMethodText
{
get { return this.paymentMethodText; }
set { this.paymentMethodText = value; }
}
public virtual string CreditCardNoText
{
get { return this.creditCardNoText; }
set { this.creditCardNoText = value; }
}
public virtual string CardholderNameText
{
get { return this.cardholderNameText; }
set { this.cardholderNameText = value; }
}
public virtual string ExpirationDateText
{
get { return this.expirationDateText; }
set { this.expirationDateText = value; }
}
public virtual string SubmitButtonText
{
get { return this.submitButtonText; }
set { this.submitButtonText = value; }
}
protected override void Render(HtmlTextWriter writer)
{
. . .
}
}
}
注意每个属性都使用一个私有字段作为其内部存储(Backing Store)。还要注意前文的硬编码字符串值已被设置为私有字段。示例2-5显示了使用这些属性代码代替硬编码字符串值后的Render方法的新版本。
类中的属性由两个部分组成,它们以get和set关键字开头。这两个部分分别称为属性的get访问器和set访问器。get访问器主要负责获取或者检索特定存储(Storage)中当前属性的值。另一方面,set访问器主要负责设置或者存储在特定存储(Storage)中属性的设置值。特定存储(Storage)或者内部存储(Backing Store)可以是任何对象。例如,示例2-5中所示属性使用私有字段作为内部存储(Backing Store)。单词“Storage”则使用了其常见的意义。例如,get访问器的存储或者内部存储可能是一段动态计算当前属性值的代码。
示例2-5:CreditCardForm2控件的Render方法
namespace CustomComponents
{
public class CreditCardForm2 : Control
{
...
protected override void Render(HtmlTextWriter writer)
{
writer.Write("<table style='width:287px;height:124px;border-width:0;'>");
writer.Write("<tr>");
writer.Write("<td>" + PaymentMethodText + "</td>");
writer.Write("<td>");
...
writer.Write("<tr>");
writer.Write("<td>" + CreditCardNoText + "</td>");
writer.Write("<td><input name='CreditCardNo' id='CreditCardNo'
type='text' /></td>");
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td>" + CardholderNameText + "</td>");
writer.Write("<td><input name='CardholderName' id='CardholderName'
type='text' /></td>');
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td>" + ExpirationDateText + "</td>");
writer.Write("<td>");
writer.Write("<select name='Month' id='Month'>");
writer.Write("<select name='Month' id='Month'>");
for (int day = 1; day < 13; day++)
{
if(day < 10)
writer.Write("<option value='" + day.ToString() + " '>" + "0" +
day.ToString() + "</option>");
else
writer.Write("<option value='"+day.ToString() + "'>"+day.ToString() +
"</option>");
}
writer.Write("</select>");
writer.Write(" ");
writer.Write("<select name='Year' id='Year'>");
for (int year = 2005; year < 2015; year++)
{
writer.Write("<option value='"+year.ToString()+"'>"+year.ToString()
+"</option>");
}
writer.Write("</select>");
writer.Write("</td>");
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td align='center' colspan='2'>");
writer.Write("<input type='submit' value='" + SubmitButtonText + "' />");
writer.Write("</td>");
writer.Write("</tr>");
writer.Write("</table>");
base.Render(writer);
}
}
}