当编写自定义控件时,您必须谨记页面开发人员将在Visual Studio环境中使用自定义控件,他们希望服务器控件具有某些标准的行为和外观。例如,页面开发人员希望当他们选中一个属性浏览器中的属性时,能够看到一行相关说明。
必须用设计时属性注释自定义控件,这样控件才能够与Visual Studio环境中的标准ASP.NET服务器控件行为类似。
当页面开发人员在Visual Studio中选中CreditCardForm2控件时,他们希望在属性浏览器中显示有关PaymentMethodText、CreditCardNoText、CardholderNameText、ExpirationDate Text和SubmitButtonText属性的如下信息。
● 属性名称和值。
● 每个属性都包含一行描述属性作用的说明。
● 每个属性的默认值。
● 每个属性的特定类别,这样页面开发人员可以在属性浏览器中查看属性的分组。
可以对这5个属性(Property)应用BrowsableAttribute、DescriptionAttribute、DefaultValueAttribute和CategoryAttribute等设计时属性(Attribute),以便对这些属性定义上文所述的信息。例如,考虑如下代码:
[BrowsableAttribute(true)]
[DescriptionAttribute("Gets and sets the payment method")]
[DefaultValueAttribute("Payment Method")]
[CategoryAttribute("Appearance")]
public virtual string PaymentMethodText
{
get { return this.paymentMethodText; }
set { this.paymentMethodText = value; }
}
这些代码实现了以下动作。
(1)对PaymentMethodText属性(Property)应用BrowsableAttribute(true)属性(Attribute)可指示属性浏览器显示该属性的名称和值。默认情况下,每个公共属性都可考虑使用BrowsableAttribute。对于只读属性(Property)应用该属性(Attribute)将指示属性浏览器不要显示属性名称和值,因为如果页面开发人员不能修改属性值,则显示属性信息就变得没有意义了。
(2)对属性(Property)应用DescriptionAttribute("Gets and sets the payment method")可指示属性浏览器每当页面开发人员选中该属性(Property)时,显示文本“Gets and sets the pay- ment method”。
(3)对属性(Property)应用DefaultValueAttribute("Payment Method")可指示属性浏览器将文本“Payment Method”作为属性(Property)的默认值显示。
(4)对属性(Property)应用CategoryAttribute("Appearance")可指示属性浏览器在外观类别中显示该属性(Property)。
当页面开发人员在Visual Studio中选择一个服务器控件时,他们希望看到一个高亮的特殊控件属性。该属性称为服务器控件的默认属性。以下代码对自定义控件CreditCardForm2应用了DefaultPropertyAttribute ("CardholderNameText"),以便将CardholderNameText作为默认属性:
[DefaultPropertyAttribute("CardholderNameText")]
public class CreditCardForm2 : Control
当页面开发人员将CreditCardForm2控件从工具箱拖放到设计窗口时,设计器将自动将以下代码添加到相关ASP.NET Web页面中:
<cc1:CreditCardForm2 ID="CreditCardForm2" runat="server"/>
以下代码针对CreditCardForm2自定义控件应用类层次属性(Attribute)ToolboxData- Attribute,这样可设置CreditCardForm2控件的多个属性的默认值:
[ToolboxData("<{0}:CreditCardForm2 PaymentMethodText='Payment Options'
CreditCardNoText='Credit Card Number' CardholderNameText='Cardholder Full Name'
SubmitButtonText = 'Send' runat='server'></{0}:CreditCardForm2>")]
public class CreditCardForm2 : Control
属性(Attribute)指示设计器当页面开发人员将CreditCardForm2控件从工具箱拖放到设计器窗口时,将以下代码添加到.aspx页面中:
<ccl:CreditCardForm2 runat='server' CardholderNameText='Cardholder Full Name'
CreditCardNoText='Credit Card Number'
PaymentMethodText='Payment Options' SubmitButtonText='Send'>
</ccl:CreditCardForm2>
正如所介绍的,当页面开发人员将CreditCardForm2控件从工具箱拖放到设计器窗口时,设计器会将以下代码添加到.aspx文件中:
<%@ Register Assembly="CustomComponents" Namespace="CustomComponents"
TagPrefix="cc1" %>
...
<cc1:CreditCardForm2 ID="CreditCardForm2" runat="server"/>
注意,设计器使用cc1作为前缀。可以通过向AssemblyInfo.cs文件添加(以下)程序集层次属性(Attribute)来指示设计器使用“custom”作为标记前缀:
using System.Web.UI;
[assembly: TagPrefix("CustomComponents","custom")]
另外,设计器将使用自定义前缀替代ccl:
<%@ Register Assembly="CustomComponents" Namespace="CustomComponents"
TagPrefix="custom" %>
...
<custom:CreditCardForm2 ID="CreditCardForm2" runat="server"/>
如果使用命令行生成程序集,则必须创建AssemblyInfo.cs文件,向该文件添加如下代码,并将该文件移动到CreditCardForm2.cs文件所在的目录:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Web.UI;
[assembly: TagPrefix("CustomComponents","custom")]
接着,必须使用如下命令编译AssemblyInfo.cs和CreditCardForm2.cs文件:
csc /t:library /out:CustomComponents.dll /r:System.dll /r:System.Web.dll
AssemblyInfo.cs CreditCardForm2.cs
在Visual Studio中创建一个新项目,其包含一个单独的页面(例如Default.aspx)。通过以下步骤将自定义控件CreditCardForm2添加到Visual Studio的工具箱中。
(1)右键单击工具箱,在弹出的菜单中选择“选择项...”,这样可加载“选择工具箱项”对话框。
(2)使用“浏览...”按钮,使其转至包含CreditCardFormf2控件所在目录,并选择程序集(如图2-2所示)。
此时就可以将CreditCardForm2控件从工具箱拖放到设计器窗口。注意,设计器将自动将前文所述代码行添加到.aspx文件中。
图2-2
CreditCardForm1和CreditCardForm2控件中的Render方法(参见示例2-3和示例2-5)都将字符串值传递给HtmlTextWriter类的Write方法。字符串引起了以下问题。
● 字符串操作总是容易出错,且编译器无法捕获错误。例如,以下输入错误仅在运行时才可被捕获:
Writer.Write("<spon/>");
● 无法获得Visual Studio提供的智能感知功能的支持。
● 不得不为不同的浏览器编写不同的HTML标记文本。
HtmlTextWriterTag、HtmlTextWriterAttribute和HtmlTextWriterStyle枚举能够解决所有问题。HtmlTextWriterTag枚举包括与HTML 4.0标记同名的值。例如,HtmlTextWriter Tag.Span对应HTML标记<span>。HtmlTextWriterAttribute枚举公开了与HTML 4.0属性同名的值。例如,HtmlTextWriterAttribute.Id对应HTML的id属性。最后,HtmlTextWri- terStyle枚举包括与CSS样式属性同名的值。例如,HtmlTextWriterStyle.Width对应CSS样式中的width属性。
这3个枚举具有以下优点。
● 由于需要键入枚举值,因此使用编译器的拼写检查功能可避免与字符串操作相关的问题。
● 这些枚举受到Visual Studio提供的智能感知功能的支持。
● ASP.NET框架包括两个版本的枚举。一个版本使用HTML 4.0标准编译,另一个版本使用HTML 3.2标准编译。ASP.NET框架允许自定义控件用户配置环境,以使ASP.NET框架使用这些枚举的适当版本。自定义控件不必作任何额外的工作就能够创建针对不同浏览器的不同HTML标记文本。只要使用这些枚举,ASP.NET框架就可以自动使用正确的版本。如果使用字符串则无法从该特性中受益。
为了利用这3个优点,必须编写一个名为CreditCardForm3的自定义控件,其Render方法使用HtmlTextWriterTag、HtmlTextWriterAttribute和HtmlTextWriterStyle枚举来替代字符串值。这好像在暗示,由于编写一个新类,所以不得不重写所有CreditCardForm2中曾经写过的属性(Property)属性层次和类层次属性(Attribute)。
是否将Render方法不声明为virtual是问题真正的关键所在。为此,可以使CreditCardForm3继承CreditCardForm2,并重写Render方法,而不必重新实现CreditCardForm2中实现过的属性(Property)和属性(Attribute),因为CreditCardForm3已经继承了它们。您应该总是将自定义控件中的方法声明为virtual,那么其他需要重写的人则能够提供他们自己的实现。这就是扩展自定义控件并添加新特性的方法。
示例2-6显示了CreditCardForm3控件实现的Render方法。
示例2-6:使用HtmlTextWriterTag、HtmlTextWriterAttribute和HtmlTextWriterStyle枚举
public class CreditCardForm3 : CreditCardForm2
{
protected override void Render(HtmlTextWriter writer)
{
writer.AddStyleAttribute(HtmlTextWriterStyle.Width, "287px");
writer.AddStyleAttribute(HtmlTextWriterStyle.Height, "124px");
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "0");
writer.AddAttribute(HtmlTextWriterAttribute.Id, "mytable");
writer.RenderBeginTag(HtmlTextWriterTag.Table);
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.Write(PaymentMethodText);
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.AddAttribute(HtmlTextWriterAttribute.Name, "PaymentMethod");
writer.AddAttribute(HtmlTextWriterAttribute.Id, "PaymentMethod");
writer.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%");
writer.RenderBeginTag(HtmlTextWriterTag.Select);
writer.AddAttribute(HtmlTextWriterAttribute.Value, "0");
writer.RenderBeginTag(HtmlTextWriterTag.Option);
writer.Write("Visa");
writer.RenderEndTag();
writer.AddAttribute(HtmlTextWriterAttribute.Value, "1");
writer.RenderBeginTag(HtmlTextWriterTag.Option);
writer.Write("MasterCard");
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.Write(CreditCardNoText);
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.AddAttribute(HtmlTextWriterAttribute.Name, "CreditCardNo");
writer.AddAttribute(HtmlTextWriterAttribute.Id, "CreditCardNo");
writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
writer.RenderBeginTag(HtmlTextWriterTag.Input);
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.Write(CardholderNameText);
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.AddAttribute(HtmlTextWriterAttribute.Name, "CardholderName");
writer.AddAttribute(HtmlTextWriterAttribute.Id, "CardholderName");
writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
writer.RenderBeginTag(HtmlTextWriterTag.Input);
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.Write(ExpirationDateText);
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.AddAttribute(HtmlTextWriterAttribute.Name, "Month");
writer.AddAttribute(HtmlTextWriterAttribute.Id, "Month");
writer.RenderBeginTag(HtmlTextWriterTag.Select);
for (int day = 1; day < 13; day++)
{
writer.AddAttribute(HtmlTextWriterAttribute.Value, day.ToString());
writer.RenderBeginTag(HtmlTextWriterTag.Option);
if (day < 10)
writer.Write("0" + day.ToString());
else
writer.Write(day);
writer.RenderEndTag();
}
writer.RenderEndTag();
writer.Write(" ");
writer.AddAttribute(HtmlTextWriterAttribute.Name, "Year");
writer.AddAttribute(HtmlTextWriterAttribute.Id, "Year");
writer.RenderBeginTag(HtmlTextWriterTag.Select);
for (int year = 2005; year < 2015; year++)
{
writer.AddAttribute(HtmlTextWriterAttribute.Value, year.ToString());
writer.RenderBeginTag(HtmlTextWriterTag.Option);
writer.Write(year);
writer.RenderEndTag();
}
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
writer.AddAttribute(HtmlTextWriterAttribute.Align, "center");
writer.AddAttribute(HtmlTextWriterAttribute.Colspan, "2");
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.AddAttribute(HtmlTextWriterAttribute.Type, "submit");
writer.AddAttribute(HtmlTextWriterAttribute.Value, SubmitButtonText);
writer.RenderBeginTag(HtmlTextWriterTag.Input);
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
writer.RenderEndTag();
}
}
Render方法的目的是生成在客户端浏览器显示的控件的HTML标记文本。HTML标记文本包括HTML元素,如table、tr、td、th、select、option和input等。通常情况下,每个HTML元素包括以下5个部分:
● 打开标记,例如,<table>、<tr>、<td>、<th>、<select>、<option>和<input>等。
● 属性,如id、name、type和value等。元素属性呈现在打开标记中,例如,<table id="mytable" name="mytable">。
● 样式属性,如width、height等。元素的样式属性呈现在打开标记的style属性中,例如,<table style="width:100px;height:200px">。
● 内容。元素内容包括文本、其他元素,或者二者都有。
● 关闭标记,例如</table>、</tr>等。
HtmlTextWriter类公开了一些用于呈现上述各部分标记的方法,如下所示。
● RenderBeginTag:呈现或者生成HTML元素的打开标记。
● AddAttribute:当为HTML元素的打开标记生成或者呈现属性时调用该方法。在RenderBeginTag之前必须调用该方法。
● AddStyleAttribute:当为HTML元素的style属性生成或者呈现样式属性时,调用该方法。该方法必须在RenderBeginTag方法之前调用。ASP.NET将自动生成一个名为style的属性,其中包含所有样式属性,例如,width,height等。换言之,在元素的打开标记中,多次调用AddStyleAttribute方法的生成结果将集中在一个单独的style属性中。
● Write:HTML元素能够包含文本、其他元素或者二者皆有。使用Write方法可生成任何文本内容类型,例如,string、int和float等。应使用RenderBeginTag、AddAttribute、AddStyleAttribute和RenderEndTag方法生成非文本内容,即元素的子元素。
● RenderEndTag:呈现或者生成HTML元素的关闭标记。
举例而言,示例2-6的部分代码根据以上方法生成了表格的HTML元素:
writer.AddStyleAttribute(HtmlTextWriterStyle.Width, "287px");
writer.AddStyleAttribute(HtmlTextWriterStyle.Height, "124px");
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "0");
writer.AddAttribute(HtmlTextWriterAttribute.Id, "mytable");
writer.RenderBeginTag(HtmlTextWriterTag.Table);
...
Writer.RenderEndTag();
这些代码完成了以下动作。
(1)3次调用AddStyleAttribute方法来生成CSS样式属性width、height和border- width。这些方法使用HtmlTextWriterStyle的枚举成员Width、Height和BorderWidth,替代字符串“width”、“height”和“border-width”,这样有利于发挥前文所述的枚举的优点。
(2)调用AddAttribute方法生成id属性。该方法使用HtmlTextWriterAttribute的枚举成员Id,其替代了字符串“Id”,这样有利于发挥前文所述的枚举的优点。
(3)调用RenderBeginTag方法生成HTML元素table的打开标记(<table>)。该方式使用了HtmlTextWriterTag的枚举成员Table。
(4)调用RenderEndTag方法生成HTML元素table的关闭标记(</table>)。
如示例2-7所示,CreditCardForm3控件的5个属性使用私有字段作为内部存储。在桌面应用程序中,同一会话使用同一控件实例并不是一个问题。然而,在Web应用程序中,同一会话使用控件的不同实例就是一个大问题。举例而言,考虑示例2-7所示的使用CreditCardForm3控件的Web页面。
注意,当第一次访问页面时,Page_Load方法都会设置CreditCardForm3控件的Cardholder NameText、PaymentMethodText和SubmitButtonText属性值为字符串“Full Name”、“Payment Options”和“Send”,这与它们的默认值不同,其默认值分别为“Cardholder’s Name”、“Payment Method”和“Submit”。
示例2-7:使用CreditCardForm3控件的Web页面
<%@ Page Language="C#" %>
<%@ Register TagPrefix="custom" Namespace="CustomComponents" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
creditcardform.CardholderNameText = "Full Name";
creditcardform.PaymentMethodText = "Payment Options";
creditcardform.SubmitButtonText = "Send";
}
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<custom:creditcardform3 runat="server" ID="creditcardform" />
</form>
</body>
</html>
图2-3显示了第一次请求Web的页面,其3个文本标签分别对应这3个属性的字符串值“Full Name”、“Payment Options”和“Send”。
如果单击Send按钮将页面回传到服务器,那么将获得如图2-4所示的页面。3个文本标签将分别对应显示PaymentMethodText、CardholderNameText和SubmitButtonText属性的默认值。图中显示了这些属性值设置为默认值的情况。
图2-3 图2-4
当ASP.NET处理完成第一次请求时,它将释放用于处理请求的CreditCardForm3对象。当对象被释放,其属性也将永久丢失。当处理第二次请求时,ASP.NET将创建一个新的CreditCardForm3对象,并设置其属性为默认值。
这是ASP.NET应用程序隐藏的扩展能力。如果应用程序通过页面请求保持状态,那么它将不可能支持数以千计的访问者。也就是说,如果自定义控件的属性需要通过页面回传保持值,那么必须采取额外的步骤。
Control类包括了一个名为ViewState的集合属性。该集合涉及一个非常重要的对象,它能够自动存储和加载通过页面回传的内容,而不需要任何编码的参与。对于需要通过页面回传保持值的自定义控件属性,应该使用该集合作为内部存储。换言之,属性的get访问器和set访问器必须委托给ViewState集合,而不是私有字段。
因此,此时需要编写一个新的名为CreditCardForm4的自定义控件,其PaymentMethodText、CreditCardNoText、CardholderNameText、ExpirationDateText和SubmitButtonText属性使用ViewState作为内部存储,而不是私有字段。
示例2-8显示了新控件。
如前文所述,使CreditCardForm4继承CreditCardForm3,能够避免重新实现Render方法以及属性层次和类层次属性(Attribute)。由于CreditCardForm3的属性声明为virtual,所以CreditCardForm4可重写它们,从而进行自身的实现。
示例2-8:使用ViewState作为属性的内部存储
public class CreditCardForm4 : CreditCardForm3
{
public override string PaymentMethodText
{
get
{
return ViewState["PaymentMethodText"] != null ?
(string)ViewState["PaymentMethodText"] : "PaymentMethod";
}
set { ViewState["PaymentMethodText"] = value; }
}
public override string CreditCardNoText
{
get
{
return ViewState["CreditCardNoText"] != null ?
(string)ViewState["CreditCardNoText"] : "CreditCardNo";
}
set { ViewState["CreditCardNoText"] = value; }
}
public override string CardholderNameText
{
get
{
return ViewState["CardholderNameText"] != null ?
(string)ViewState["CardholderNameText"] : "CardholderName";
}
set { ViewState["CardholderNameText"] = value; }
}
public override string ExpirationDateText
{
get
{
return ViewState["ExpirationDateText"] != null ?
(string)ViewState["ExpirationDateText"] : "Expiration Date";
}
set { ViewState["ExpirationDateText"] = value; }
}
public override string SubmitButtonText
{
get { return ViewState["SubmitButtonText"] != null ?
(string)ViewState["SubmitButtonText"] : "Submit"; }
set { ViewState["SubmitButtonText"] = value; }
}
}
如果重复图2-3和图2-4所示的操作,那么将注意到,页面回传后文本标签保持了文本值,而并未被属性默认值重置。可以将ViewState集合看作是一个可存储任何类型对象的包。换言之,无论自定义控件属性值的真实类型是什么,这个包中都会存储类型为System.Object的自定义控件属性值,并且将返回这些值作为System.Object的项。这就是为什么每个属性的get访问器必须将ViewState集合的返回对象转换为属性此前的真实类型。
在每个请求结束时,ASP.NET使用与包含在ViewState中每个对象相关的类型转换器,将对象转换为其对应的字符串表达式。类型转换器是继承自TypeConverter基类的类。该类可将与其相关的类型转换为指定的目标类型,如System.String。
.NET基本类型如Int32和Boolean包括自身的类型转换器,例如,Int32Converter和BooleanConverter。正如将在第7章将介绍的,您可以编写自定义类型转换器,它能够将自定义类型转换为另一个类型。
在将每个对象转换为相应的字符串表达式后,ASP.NET会将这些字符串值存储到一个__VIEWSTATE隐藏字段中,并发送到客户端。因此,存储在ViewState中的对象字符串表达式的大小是个重要问题。
将对象存储到ViewState中时,必须注意以下两个重要问题。
● ViewState可针对特定类型的转换而进行优化,这些类型如System.String、System.Int32、System.Boolean、System.Drawing.Color、System.Unit、Hashtable、Array,以及Int32、Boolean、Color和Unit的ArrayList。如果需要在ViewState中存储其他类型,那么应该编写一个自定义类型转换器,其针对自定义类型转换为相应的字符串表达式而进行优化。
● 由于添加到ViewState的对象字符串表达式存储在ASP.NET页面中,所以必须只将必要的信息存储在ViewState中,以减少ASP.NET页面数据大小。例如,不应该将每个请求能够轻松计算的值存储起来。