asp.net|web|控件
ASP.NET 2.0并没有抛弃1.1版本中的任何现有控件,而是增加了一组新的控件;同时还引入了若干新的控件开发技术。本系列文章将对这些内容展开全面探讨。
一、 引言
到目前为止,你可能已经了解了大量的ASP.NET 2.0新特征—母版页面,主题,提供者,等等……所有这样内容都相当精彩;但是,你是否了解到有关定制Web控件开发方面的重大变化?这正是我在本文中所想讨论的。如果你已经从事于控件开发,那么,我想本文所描述的ASP.NET 2.0中的新的改进特征会立即应用于你的控件开发中。
首先应该注意的是,你以前使用ASP.NET 1.1(或1.0)开发的所有Web控件在2.0版本下将继续良好运行—微软并没有破坏你的现有代码。在本文中,我将向你介绍的所有相关内容,包括许多新的令人激动的技术,所有这些你都可以添加到现有控件或在新的控件环境中使用。
作者注:本文假定你对定制Web控件开发已经有一个基本了解。在本文中,我以一个增强版本的EmailContact控件为例对ASP.NET 2.0中的Web控件改进技术作全面探讨。
二、 改进
表格1描述了ASP.NET 2.0在定制Web控件开发方面所作的大部分的重大改进。在本系列文章中,我将对这些特征展开逐一讨论。
表格1:ASP.NET 2.0 Web控件改进功能。
三、 增强EmailContact Web控件
本文中的定制EmailContact Web控件(参考图1)允许在你的站点中加入一个“contact us”表单,它具有完整的电子邮件功能。在本文中,我将使用该功能增强这一控件。
图1.缺省状态下的EmailContact控件
四、 一个新的基类
以前,开发者都是从WebControl类派生他们的可视化Web控件。我之所以在此使用了“可视化”一词是因为,典型情况上,没有在浏览器中生成任何内容的控件都是派生自Control类。这一点并没有改变—你应该继续使用该Control类来派生任何这样的非可视化控件—它们执行不可见功能或在浏览器中生成除可视化HTML内容之外的任何其它内容。而且,在开发可视化Web控件时,你还应该继续使用WebControl类。然而,我们所开发的大多数复合控件都是为了利用现有控件的功能。在这种情况下,你应该总是从WebControl类进行派生,但是你还要记住另外一些有关细节—否则的话,有可能导致许多问题。
复合控件必须实现INamingContainer接口,并且需要包括在你的控件类中。这个接口能够确保在你的控件及其可以生成的整个控件层次中的所有的HTML标签中都具有唯一的标签命名。当你在单个页面上存在多个相同类型的复合控件的情况下,这是相当关键的。在这样的情况下,你需要确保任何生成的子元素都具有唯一的名称。忘记实现该接口能够导致各种问题的出现。
在ASP.NET 2.0以前,复合控件开发者还需要记住在一个控件的Render方法中调用EnsureChildControls。在我以前的文章中曾经向你介绍如何重载该Render方法并且在调用基类的Render方法前调用这个方法。要使控件在Visual Studio设计时刻正确生成这一步是必要的;否则,有可能带来许多不便。
上面两个步骤在复合控件开发中如此普遍,以致于许多开发者往往都会构建一个包括这两个细节的基类,然后从该基类下派生他们所有的新的复合控件。作为代替,ASP.NET 2.0提供了(更准确地说是“名字为”)CompositeControl。借助于这个类来构建你的复合控件,你就不必再记住实现INamingContainer或从Render方法中执行一个EnsureChildControls调用了。
另外,还存在其它一些新的基类,例如用于数据绑定的控件等,在此不再赘述。
我的观点是:ViewState有可能成为你最好的朋友,也有可能成为你最坏的敌人—这要依赖于你使用它的方式来决定。如果你在以前曾经使用过ViewState,那么,你肯定会喜欢新的ControlState。
关于ViewState的最令人头痛的问题之一就是,它的“all-or-nothing”状态管理方法。页面开发者可以很容易地决定在任何控件级,页面级或在整个站点级(经由web.config)上关闭ViewState。事实上,如果你在整个站点级上通过web.config关闭ViewState的话,那么,你不妨猜测一下你还能够在其它什么地方关闭它?答案是:还可以在machine.config中实现—在此情况下,它能够影响到同一服务器上的所有站点。如果一个页面开发者决定关掉在ViewState中实现状态管理的能力,那么,你的控件生成有可能出现部分不可用,或更有甚者—完全不可用。
为此,在新版本中,微软创建了ControlState—旨在解决这一问题。页面开发者不能关掉ControlState,因此使用它进行属性选择更为安全。
使用ControlState与使用ViewState几乎完全一致。然而,ControlState并没有提供象ViewState这样的一个变量,而是提供了称为SaveControlState和LoadControlState的方法以便于你的控件能够进行重载。这些方法与SaveViewState和LoadViewState方法的工作原理完全一致。
因为ControlState在属性语句中没有提供一个相应的变量,所以,你必须借助于ASP.NET开发者以前在他们的对象中所使用的成员变量(属性语句)来实现相同的功能。
Protected _MailServer As String = "First name:"
Public Property MailServer() As String
Get
Return _ MailServer
End Get
Set(ByVal value As String)
_MailServer = value
End Set
End Property
然而,因为我使用了一个标准的成员变量来保存值,所以我需要一种方法以便把数据存储在ControlState中—这正是前面提到的方法“登场”的原因。就象在它们相应的ViewState方法中那样,ASP.NET将在页面生命周期内调用这两个方法。其中,SaveViewState方法返回一个将被持久存储的对象类型。通过返回一个对象数组,这个方法可以存储多个值。并且,就象发生在SaveViewState方法中一样,也是使用数组的0下标元素来调用基类的SaveControlState方法。
Protected Overrides Function SaveControlState() As Object
Dim state() As Object = New Object(2) {}
state(0) = MyBase.SaveControlState()
state(1) = _MailServer
Return state
End Function
注意:LoadControlState方法以一个对象作为参数—这个对象是以前在SaveControlState中返回的一个对象。在这个方法中,我重新分配了成员变量—通过把该参数转换为一个对象数组,然后获得每个下标的值。与以前一样,我使用数组的0下标来调用基类的LoadControlState方法。
Protected Overrides Sub LoadControlState( _
ByVal savedState As Object)
If savedState IsNot Nothing Then
Dim state() As Object = CType(savedState, Object())
MyBase.LoadControlState(state(0))
_MailServer = CType(state(1), String)
End If
End Sub
借助于这些方法来存储数据,在页面开发者关掉ViewState时,控件就不会出现前面那些麻烦。
你可能对ControlState的存储位置感到惊讶;它对应于另一个生成到HTML页面中的隐藏的文本框。就象在ViewState情况下数据被存储在__ViewState隐藏文本框中类似,ASP.NET 2.0使用__ControlState隐藏文本框来存储ControlState数据。
遗憾的是,微软没有向开发者提供内在地使用ControlState的能力—就象在ViewState情况下那样。所以,为了ControlState使用,你需要注册你的控件。你可以重载控件的OnInit事件并且调用Page对象的RegisterRequiresControlState方法。
Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
MyBase.OnInit(e)
If Page IsNot Nothing Then
Page.RegisterRequiresControlState(Me)
End If
End Sub
现在,你可以使用ControlState来存储你认为足够重要的数据—如果不把它存储起来,那么你的控件可能生成一些无用的内容。
记住,你在设计时刻对属性的修改将被硬编码到该控件的ASPX声明中,从而在相邻的再次回寄之间自动地存储。然而,如果表单上的一个行为改变了一个控件的属性,那么,这将会激活状态管理机制的使用。如果不把该属性存储在一个状态中,那么,在下一次回寄时它将恢复到“硬编码”状态。
现在,总的来看,我们应该把与外观相关的属性存储在ViewState中,而把与行为相关的属性存储在ControlState中。通过这种方式,如果一个页面开发者关掉ViewState,那么你的控件尽管可能看起来样子别扭,但是仍能正确工作。
当你最开始在Visual Studio 2005中使用Windows表单控件或是ASP.NET Web控件时,你首先会注意到,在许多控件右上角出现一个箭头形状的小玩意儿(见图2中的示例)。点击这个箭头会弹出一个小窗口,其中包含该控件的一些属性,还有一两个链接。微软设计这些灵敏标签是为了显示你需要操作的一些属性,其最终目的是为了使该控件在一个页面或表单上能够正确工作;并且你将注意到,它们比一个普通的快捷菜单更为精致。本节中我们讨论的内容既适用于Windows表单控件也适用于ASP.NET服务器控件。
图2.EmailContact控件的灵敏标签
为了构建你自己的灵敏标签,你需要使用一个控件设计器类。事实上,你在另外其它一些问题上也会使用这个类。但是,在我详细讨论设计器类前,我想先创建一个ActionList类—这个类将定义我的灵敏标签中包含的元素。
一个ActionList类继承自System.ComponentModel.Design命名空间中的DesignerActionList类。但是,在详细讨论这个类之前,让我先来解释一下存在于灵敏标签中的四种类型的元素:category header,property mapping,action link以及information item。图2展示了我构建这个灵敏标签的目标。你能够从中看出我所指的这四种类型的元素吗?我把这个灵敏标签根据标题分为三类:“Appearance & Behavior”,“Support”和“Information”。其中,“Appearance & Behavior”分类中包含了两个属性:Mail Server和Pre-defined Display。这些实际上都是EmailContact控件本身的属性。“Support”分类包含两个激活某些类型的一个行为的链接,而“Information”分类仅用于显示信息。现在,有了这四种类型的元素,我将着手创建我的ActionList类。
我将创建一个称为EmailContactActionList的类,并且从DesignerActionList中加以派生。(你可以在本文源码列表1中看到完整的类)。我将创建一个构造器—它接收一个EmailContact实例作参数并且把它的范围扩大到一个称为ctlEmailContact的类级变量。后面,当我把代码添加到设计器类时,你将看到这个构造器的使用情况。现在,我已经建立了一个类级的对象,它包含我正在设计的Web控件的实例。
接下来,我将创建灵敏标签将显示的属性的“property mapping”。在图2中,你看到我已经在该灵敏标签中标出了两个属性:MailServer和PreDefinedDisplay。这些将分别映射到EmailContact控件的称为MailServer和PreDefinedDisplay的属性上。ActionList类中的属性映射将在get存取器中返回控件的属性,而在set存取器中设置控件的属性。然而,由于微软设计ActionList基础结构的方式决定了,你不能直接设置该控件的属性。而是,你必须使用反射机制来存取该控件的属性,然后再设置它的值。为了方便这一实现,我编写了一个称为GetControlProperty的方法,它能够返回一个PropertyDescriptor对象。这样以来,开发者就不需要再重复每一种属性映射下的反射编码。下面是一个属性映射看起来的样子。
Public Property MailServer() As String
Get
Return ctlEmailContact.MailServer
End Get
Set(ByVal value As String)
GetControlProperty("MailServer"). _
SetValue(ctlEmailContact, value)
End Set
End Property
接下来,我需要建立的是你在图2中所看到的链接:“About EmailContact”和一个到我自己的网站的链接。这些链接将执行我将在这个类中创建的方法。我的第一个方法名为ShowAboutBox,它显示一个Windows表单以用作我的控件的一个“关于”信息提示窗口。第二个方法称为LaunchWebSite,它执行一个对System.Diagnostics.Process.Start的调用以便在一个浏览器实例中启动我的网站。这两个方法的唯一的要求是:每一个签名都必须是一个“Sub”(在C#语言中相应于一个void函数)并且不带参数。
注意,在这个灵敏标签示例中仅显示了两个属性和两个链接,但是借助于我刚才所展示的技术,你完全可以提供你所需要的尽可以多的这些对象。然而,我建议:不要使用太多的信息来重载一个灵敏标签。记住,你仅想把信息放于此以便页面开发者立即使用,从而使得Web控件开发更具直观性。
现在,既然我已经创建了我的属性映射和行为方法,那么接下来,我将创建灵敏标签的内容。其中,DesignerActionList类提供一个称为GetSortedActionItems的重载函数。以后,一个设计器类将重载这个函数,并且它会返回一个DesignerActionItemCollection(定义于System.ComponentModel.Design命名空间)类型的对象。
这个属性重载的实现部分将创建一个新的DesignerActionItemCollection对象并且使用四个不同的类(DesignerActionHeaderItem,DesignerActionPropertyItem,DesignerActionMethodItem和DesignerActionTextItem)的实例来填充它。注意,这四个类都派生自抽象DesignerActionItem类。下面,我将同你逐个展开讨论。
Dim o_Items As DesignerActionItemCollection = _
New DesignerActionItemCollection
这段代码使用DesignerActionHeaderItem类来创建灵敏标签分类头部,并且在它们的构造器中接收分类名字。我将直接把这个类的实例插入到我刚才创建的集合中。
o_Items.Add(New DesignerActionHeaderItem( _
"Appearance & Behavior"))
为每一种分类创建准确的标题是相当重要的,这不仅是因为它作为在灵敏标签中作为该分类的头部相应的显示文本这一用途。早些时候,我创建了两个分别称为MailServer和PreDefinedDisplay的属性映射;现在我想把它们添加到灵敏标签中。为此,我将创建DesignerActionPropertyItem类的实例并且把它们添加到集合中。
o_Items.Add(New DesignerActionPropertyItem( _
"MailServer", "Mail Server", _
"Appearance & Behavior"))
注意,该构造器接收三个参数:属性名,将出现在灵敏标签上的文本信息,以及相应类型的准确标题(在DesignerActionHeaderItem的实例中定义)。
接下来,我想以相同的方式把行为链接添加到灵敏标签。注意,也仅仅是在此时,我们使用了DesignerActionMethodItem类的实例。
o_Items.Add(New DesignerActionMethodItem( _
Me, "ShowAboutBox", "About EmailContact2", _
"Support", "Displays the about box.", True))
在此,构造器接收方法名,链接说明,类型文本以及一个用作链接的提示信息的描述等共四个参数。其中,第四个参数决定这个链接是否还出现在属性浏览器的底部。
最后,我将把信息项添加到灵敏标签中—这是使用DesignerActionTextItem类来实现的。
o_Items.Add(New DesignerActionTextItem( _
"ID: " & ctlEmailContact.ID, "Information"))
在此,构造器仅接收要显示的文本和该文本要放置的类型。
列表1(见源码文件)中的最终代码展示了我要添加到这个集合中的所有项。当该方法完成时,它简单地返回这个集合。在后面一篇中,我们将讨论控件设计器类的问题。
一、 控件设计器
控件设计器类派生自System.Web.UI.Design.WebControls.CompositeControlDesigner。该类通过控件类中声明的一个属性绑定到控件上:
DesignerAttribute(GetType(EmailContactDesigner))
在Web表单设计器中,它能够把控件的所有外观和行为特征呈现在用户面前。当页面开发者把一个Web控件拖动到Web表单上时,页面开发者可以通过各种交互方式取得控件的各种属性。这些属性将影响到页面开发者所看到的内容—不只是影响到控件本身,还包括一些更微妙的幕后元素(例如灵敏标签)。
实际上,控件设计器能够在设计时刻生成一种与运行时刻不同的输出结果。在有些情况下,一个控件在运行时刻可能没有任何可视化生成,但是却要求在设计时刻实现一些显示,以便更容易地操作它。这种情况的一个示例就是,ASP.NET 2.0中所提供的声明性数据源。这些控件提供了数据存储和缓冲功能,但是却没有可视化生成。然而,在设计时刻,它们表现为一个带有一些描述性文本的灰色窗口—出现在web表单设计界面上。另一个关于不同生成的例子是,有些控件不生成任何内容—除非它们被绑定到数据源。例如,当GridView(在1.1版本中是DataGrid) 带有数据时,它看上去十分正常;但当不存在要显示的数据行时,看上去什么东西也没有(或只显示为空的头部)。当你把一个这样的控件拖动到Web表单上时,控件往往使用几个具有示例性质的空数据行进行显示—这是由设计器类所提供的。
当Visual Studio需要决定在一个灵敏标签中显示的内容时,它存取设计器类的一个称为ActionLists的属性,并且使用它的结果来构建该灵敏标签。该ActionLists属性返回一个System.ComponentModel.Design命名空间中的DesignerActionListCollection类型的对象。在我的设计器类中,我将重载这个属性并且构建一个DesignerActionListCollection对象。我将返回的这个对象将在类范围上加以声明,并且检查ActionLists属性是否是一个“nothing”值。
Private o_ActionLists As _
DesignerActionListCollection
控件设计器中的值都是被缓冲的,因此不需要被重复创建,以便使设计器更为有效地生成控件。
控件设计器类的一个重要特征是一个称为Component的成员。它是在控件设计器类所继承的基类中进行声明的,并且包含一个对实际的控件(设计器类被绑定到其上)的引用。我可以使用这个变量,并且把它转换成我的实际的控件类型—既然该变量被声明为object类型。
Dim ctlEmailContact As EmailContact = _
CType(Component,EmailContact)
其结果是一个称为ctlEmailContact的对象,它包含一个这个类当前正在设计的实际控件的强类型实例。所以,我在这个对象上作的任何改变或执行的任何操作都将立即被反映到Web表单设计界面中,并呈现在页面开发者面前。
关于这个属性重载的其它方面的实现还包括,把我前面创建的ActionList类的一个实例添加到我在类级上创建的DesignerActionListCollection对象。
o_ActionLists.Add(New EmailContactActionList(ctlEmailContact))
你可能还记得我在EmailContactActionList类中创建的一个构造器,当时它接收EmailContact控件的一个实例。如你所见,我在此也使用了该构造器—把我设计的控件实例传递给它。
下面是控件设计器类用于构建灵敏标签的完整源码:
Private o_ActionLists As DesignerActionListCollection
Public Overrides ReadOnly Property ActionLists() As _
System.ComponentModel.Design.DesignerActionListCollection
Get
If o_ActionLists Is Nothing Then
o_ActionLists = New DesignerActionListCollection
Dim ctlEmailContact As EmailContact2 = _
CType(Component, EmailContact2)
o_ActionLists.Add( _
New EmailContactActionList(ctlEmailContact))
End If
Return o_ActionLists
End Get
End Property
在这个示例中,我仅创建了一个ActionList类,使用行为列表项填充它,并且把该类添加到将被返回的DesignerActionListCollection类—这是通过重载控件设计器的ActionLists属性来实现的。其实,我完全可以据实际需要创建许多ActionList类,并且简单地把它们添加到ActionLists属性集合即可。如果我想在逻辑上组织大量的灵敏标签项—为了在多个控件中重用它们时,这是很有用的。至于决定为何以及何时这样做,则要依赖于实际来决定。
现在,在我编译完这个控件并把它拖动到一个表单上时,我将看到一个小箭头出现在其右上角—点击它将显示你在图2中所看到的内容。在此,改变任何其中一个属性都与在属性浏览器中改变完全一致。点击相应的链接将执行在EmailContactActionList类的方法中定义的行为。
关于该控件,我们就讨论这些内容。记住一点:不要把暂时不需要的属性添加到一个灵敏标签中。
二、 模板设计时刻编辑
在以前的文章中,我曾经介绍过如何在你的控件中添加模板能力。在此,我仅简单地涉及其中一点,因为它与这里的讨论有一些关系—我指的是从Web表单设计界面上编辑模板内容的能力—在以前的ASP.NET 1.x时代这是不容易实现的。
模板设计时刻编辑功能出现在例如GridView这样的控件中—你可以把该控件置于“模板编辑”模式,然后只需把其它控件拖动到该模板区域即可。如果没有这种方便的话,页面开发者必须切换到ASPX视图并象下面这样手工地创建模板内容:
<dnd:EmailContact ID="ctl1" runat="server">
<HeaderTemplate>
<asp:Label ID="lbl1" runat="server" Text="Label" />
<asp:Textbox ID="txt1" runat="server"/>
</HeaderTemplate>
<FooterTemplate>
<asp:LinkButton ID="lnk1" runat="server" Text="LinkButton" />
</asp:LinkButton>
<asp:DropdownList ID ="ddl1" runat="server" />
</FooterTemplate>
</dnd:EmailContact>
尽管这也并不是太糟糕,但是使页面开发者使用设计器界面进行设计效果会更好一些。
为了把模板编辑能力添加到一个控件上,你必须重载控件设计器类的Initialize方法,并且设置一个标志以通知设计器你想支持模板编辑功能。
Public Overrides Sub Initialize( _
ByVal Component As IComponent)
MyBase.Initialize(Component)
SetViewFlags(ViewFlags.TemplateEditing, True)
End Sub
注意在此,我对基类方法进行了调用以确保我不取消自己不想实现的内容。这个调用通知设计器它将支持模板编辑功能,但是你仍然需要对编辑实现部分进行编程。
微软所使用的构建设计器的方式是,把模板分类到模板组中;这种情况下,在你的控件中将会存在许多模板。模板本身是在一个称为TemplateDefinition的对象中定义的;而TemplateGroup对象包含一个或多个这些定义对象。TemplateGroup对象包含在一个TemplateGroupCollection类型的对象中;因此,正是在这里(TemplateGroupCollection类型的对象中),设计器类将在类级上声明一个这种类型的变量。就象在灵敏标签情况下一样,基础结构负责缓存这个对象—这正是我在一个类级范围上声明它的原因。
Private o_TemplateGroups As
TemplateGroupCollection = Nothing
这个集合取得一个称为TemplateGroups的内置属性—我现在必须重载之。正是这个属性的内容将被暴露给页面开发者以实现相应的模板编辑功能。
Public Overrides ReadOnly Property
TemplateGroups() As TemplateGroupCollection
Get
If o_TemplateGroups Is Nothing Then
...
End If
Return o_TemplateGroups
End Get
End Property
在这个属性重载中,我将构建Visual Studio使用的TemplateGroupCollection。属性中的“Is Nothing”检查有助于阻止对这个对象不必要的重新构建。
首先我将实例化o_TemplateGroups对象。
o_TemplateGroups = New TemplateGroupCollection()
现在,我需要使用我在“灵敏标签”一节中所讨论的组件变量来取得我设计的控件。
Dim ctl As EmailContact = CType(Component, EmailContact)
稍后,我将使用这个变量。但是首先,我必须建立我要使用的相应于TemplateGroup和TemplateDefinition对象的两个对象变量。
Dim o_TemplateGroup As TemplateGroup
Dim o_TemplateDefinition As TemplateDefinition
现在我可以开始定义组和模板了。
o_TemplateGroup = New TemplateGroup("Surrounding Templates")
文本“Surrounding Templates”将出现在分类标题中,适用于我放在这个组中的所有模板定义。
o_TemplateDefinition = New TemplateDefinition(Me,
"Header Template", ctl,"HeaderTemplate",False)
o_TemplateGroup.AddTemplateDefinition _
(o_TemplateDefinition)
让我来详细分析一下该TemplateDefinition构造器中的参数。第一个参数是一个添加了模板编辑功能的设计器的实例—通常是Me;第二个参数是模板名—它将显示于一个快捷方式菜单或灵敏标签中。第三个参数是正在设计的控件—通过转换Component对象来得到它。第四个参数是控件中模板属性的名字。参数列表最后的Boolean参数被设置为False以便指定这个模板既接收服务器控件也接收HTML控件。如果把它设置为True则仅允许把服务器控件(常规Web控件)添加到模板上。
你可能已经猜出,对于你想定义的每个模板和每个模板组,都要重复这一操作。最终结果是,我的o_TemplateGroups对象中填充了我的模板中定义的所有信息—而这正是我在这个属性中所返回的内容。
图3.在模板编辑方式的EmailContact控件
现在,我重新编译并转到我的测试页面。当我右击EmailContact控件时,我将看到“EditTemplates”被添加到快捷方式菜单中。子菜单下的列表将显示我在设计器类中所定义的模板组,而当我选择“Surrounding Templates”组时,我会看到类似图3所示的内容。现在,你可以看到我手工地添加到这两个模板中的控件;而事实上,我现在就可以把其它控件从工具箱直接拖动到其上。再次右击鼠标并选择“Edit Templates”将返回到标准控件视图并且在控件中显示该模板内容。
另外还注意,一个“Edit Templates”链接也被自动地添加到该控件的灵敏标签上。如果以前不存在一个灵敏标签,那么,当我把模板编辑代码添加到设计器类上时,它会自动地为我创建一个。
下面是完整的模板编辑代码:
Public Overrides Sub Initialize(ByVal Component As IComponent)
MyBase.Initialize(Component)
' Turn on template editing
SetViewFlags(ViewFlags.TemplateEditing, True)
End Sub
Private o_TemplateGroups As TemplateGroupCollection = Nothing
Public Overrides ReadOnly Property TemplateGroups() _
As TemplateGroupCollection
Get
If o_TemplateGroups Is Nothing Then
o_TemplateGroups = New TemplateGroupCollection()
Dim o_TemplateGroup As TemplateGroup
Dim o_TemplateDefinition As TemplateDefinition
Dim ctl As EmailContact2 = CType(Component, _
EmailContact2)
o_TemplateGroup = New TemplateGroup( _
"Surrounding Templates")
o_TemplateDefinition = New TemplateDefinition( _
Me, "Header Template", ctl, "HeaderTemplate", True)
o_TemplateGroup.AddTemplateDefinition( _
o_TemplateDefinition)
o_TemplateDefinition = New TemplateDefinition( _
Me, "Footer Template", ctl, "FooterTemplate", True)
o_TemplateGroup.AddTemplateDefinition( _
o_TemplateDefinition)
o_TemplateGroups.Add(o_TemplateGroup)
End If
Return o_TemplateGroups
End Get
End Property
一、 自动格式化
现在,让我们开始讨论本文中最有趣的内容。你是否曾把一个GridView拖动到一个表单上并且注意到在属性浏览器有一个标有“Auto Format…”的链接?当你点击它时,你会得到一个预定义格式的选择:例如Corporate,Elegant,Classic,等。事实上,随着ASP.NET 2.0发行的所有安全控件套件(Security Control Suite)都提供了这个特征。读完本节后,你一定会为在你的控件中提供这种功能的容易程度而感到吃惊。
首先,我要确定我想在控件中提供什么格式。我将通过创建三种格式来简化操作。其中的两个分别称为“Monochrome”和“Colorful”。第三个(实际上是第一个)称为“No Format”;当把它拖动到一个表单时,它能够把控件恢复到其原始状态。
我创建的每一种格式都需要它自己的继承自System.Web.UI.Design命名空间的DesignerAutoFormat类。因此,我先从一个称为ColorfulFormat的继承自DesignerAutoFormat的类开始。首先,我需要为这个类提供一个缺省的构造器;并且我将在其中调用基类的构造器,发送给它一个参数—格式名称(将出现在列表中)。
Public Sub New()
MyBase.New("Colorful")
End Sub
接下来,我必须重载Apply方法。这个方法接收一个Control类型的参数。我的控件设计器类将调用这个方法,并且把我的控件实例发送到这个参数中。然后,我要把该参数转化成我的控件类型;这样以来,我就有一个可以使用的强类型引用了。
Public Overrides Sub Apply( _
ByVal control As System.Web.UI.Control)
Dim ourControl As EmailContact2 = CType(control, EmailContact2)
If ourControl IsNot Nothing Then
...
End If
End Sub
现在,我要设置我需要的任何属性—这通常包括一些风格属性,但是它事实上能够包括我需要的一切。因此,在“IsNot Nothing”检查中,针对这个特定的格式,我只需简单地设置我想实现的任何风格属性。列表2展示了完整的列表。
ourControl.BackColor = Drawing.Color.Aquamarine
ourControl.BorderStyle = BorderStyle.Double
ourControl.BorderWidth = Unit.Pixel(2)
ourControl.BorderColor = Drawing.Color.DarkRed
ourControl.HeadingStyle.Font.Name = "arial"
ourControl.HeadingStyle.Font.Bold = True
ourControl.HeadingStyle.Font.Size = FontUnit.Large
接下来,我将创建一个称为MonochromeFormat的类并且执行相同的步骤—仅在这次,我把风格属性设置为black和white以相应于一种“monochrome”格式。源码文件中的列表3展示了这个类的完整代码。
最后,我必须创建NoFormat类。这个类遵循与前两个相同的设计;但是,我不是把风格属性值设置成各种颜色及字体,而是在所有的风格属性上调用Reset方法。这可以把所有的风格值设置为它们的缺省状态。
顺便说一下,既然我的EmailContact控件派生自CompositeControl类,而它又派生自WebControl类;所以,我还可以在我的控件的类级上取得风格属性。这意味着:我可以拥有BackColor,ForeColor等属性,而且可以直接从我的控件中存取它们而不必通过一个属性来实现。然而,为了复位这些风格值,我不是直接从我的控件类取得一个Reset方法,而是由WebControl类为我提供了一个ControlStyle属性,它用作相应于所有的容器风格值的入口点。注意:在复位控件的外观时,我还把Height属性设置为一个具有400个像素的缺省值。下面是完整的类:
Public Class NoFormat
Inherits DesignerAutoFormat
Public Sub New()
MyBase.New("No Format")
End Sub
Public Overrides Sub Apply(ByVal control _
As System.Web.UI.Control)
Dim ourControl As EmailContact2 = CType(control, _
EmailContact2)
If ourControl IsNot Nothing Then
ourControl.ControlStyle.Reset()
ourControl.HeadingStyle.Reset()
ourControl.CaptionStyle.Reset()
ourControl.FieldStyle.Reset()
ourControl.ReadonlyFieldStyle.Reset()
ourControl.ButtonStyle.Reset()
ourControl.Height = Unit.Pixel(400)
End If
End Sub
End Class
现在,既然我已经创建了我想实现的所有格式类,那么,接下来,我需要对它们做一些实际的操作。为此,我将再转回到控件设计器类—实现一些重载。我需要重载的属性称为AutoFormats,它返回一个System.Web.UI.Design命名空间中的DesignerAutoFormatCollection类型的对象。
就象在灵敏标签和模板编辑部分一样,我将实例化的对象(为了通过属性返回之)是在类级上声明的,因为ASP.NET基础结构会负责对它进行缓冲处理。
Private o_AutoFormats As _
DesignerAutoFormatCollection
然后,在属性重载实现中,我可以测试对象变量是否是一个null值。现在,我们只需简单地使用我以前创建的每一个格式类的实例来填充o_AutoFormats集合对象。
以下是引用片段:
Public Overrides ReadOnly Property AutoFormats() _
As DesignerAutoFormatCollection
Get
If o_AutoFormats Is Nothing Then
o_AutoFormats = New DesignerAutoFormatCollection
With o_AutoFormats
.Add(New NoFormat)
.Add(New MonochromeFormat)
.Add(New ColorfulFormat)
End With
End If
Return o_AutoFormats
End Get
End Property
如果我重新编译并再次测试该控件,我将看到一个称为“Auto Format…”的链接被添加到灵敏标签的顶部。如果以前不存在灵敏标签,那么Visual Studio将自动地使用“Auto Format…”链接在其中创建一个。你将还注意到一个“Auto Format…”链接出现在属性浏览器的底部。
如果我点击任何一个链接,我将看到如图4所示的内容。选择列表中的任何一种格式从而在预览窗口中改变它;然后点击OK把此设置提交给Web表单设计界面下的控件。
图4.自动格式化屏幕
现在,既然你已经了解了如何进行格式化,那么接下来,你应该学习何时以及为什么进行格式化。
我仿佛听到CSS爱好者正在对本文大声叫喊。其实,我本人就是一个“层叠式样表(CSS)”迷,并在我所有的站点中使用它们。自动格式化特征在有些情况下(例如控件组)是非常有用的。例如,我们不妨考虑微软的安全控件套件(Security Control Suite)。从中,你会看到七种Web控件可用于一个站点中以为页面添加认证功能。其中,每一种控件都提供了自动格式化功能,并且每一个控件都显示一样的列表用于从中选择一个格式—这是相当不错的功能。
试想一下我们今天已经实现的一些流行的可视化效果:例如Outlook工具栏中的许多风格,各种TreeView控件,菜单和工具栏,等。你完全可以把这其中的许多特征集成到几乎任何站点的设计风格中。在这种情况下,自动格式化是一种相当有用的工具。
现在请记住,在每一个格式类中,我把风格值改变为我的一些风格属性。如果你在Web表单中应用这些格式中的任一个,那么,你将会看到这些风格值确实在属性浏览器中改变了。如果我想(尽管我不会选择)这样做的话,那么我可以修改style属性的CssClass值,并且把一个风格表类指派给它们。这当然可以顺利地工作,但是之后,我需要同我的控件一起发布该CSS文件,并且还要确保页面链接到它。
如果我能够在我的控件中使用这些资源文件而不必随同控件的DLL文件一起发行它们,不是相当酷吗?好,下面我们来讨论本文的最后一个话题—Web资源。
一、 Web资源
Web资源是被编译到你的控件DLL中的一些嵌入式文件。Windows表单控件开发者很久以来就拥有这种能力,只是到现在它才被应用于Web领域。让我们想象开发一个依赖于某些图像才能正确生成的控件的情形。只是简单地发布该控件相应的DLL文件而不必考虑单独发布外部图像文件,这是特别有用的。分析一下微软提供的新的Tree Web控件,那么你就会发现这一特征。在此,图像文件是讨论使用Web资源的最佳候选,尽管我们还能够把它们使用于JScript文件中。
为了说明这一问题,我想把一个邮箱图像添加到我的EmailContact控件标题的左边。你可能已经注意到,在图4中“CONTACT US”标题文字的左边已经存在一个空图像。为此,我是通过简单地创建一个Image子控件并且把它添加到控件层次中的适当位置来实现的。事实上,我还创建了一个称为ShowMailIcon的属性以允许页面开发者隐藏/显示图像。
使用常规方法就能创建一个HeadingImageUrl字符串属性,并且在图像控件的ImageUrl属性中设置它的值。这将允许页面开发者把这个图像设置为他所希望的任何内容。我同意这一方法,而我在这个控件中就使用了这种方法。然而,我更深入了一步。如果HeadingImageUrl属性不是空的,那么我使用它填充图像控件的ImageUrl属性(如前所述);但是如果该属性是空的,那么我要实现一些更酷的内容。
在我的Web控件工程中,我有一个称为mailbox.gif的邮箱图像。我将把图像的BuildAction属性设置为EmbeddedResource(注意,我可以通过属性浏览器来实现这一点)。
接下来,我需要存取我的工程的AssemblyInfo.vb文件。在ASP.NET 1.1中,这个文件位于我的工程的根文件夹下,但是在ASP.NET 2.0中,它位于我的工程文件夹下。不仅如此,我还必须点击属性浏览器顶部的“Show All Files”按钮存取这个文件。在AssemblyInfo.vb文件的底部,我需要添加下列语句:
<Assembly: WebResource("mailbox.gif","image/gif")>
这一句把该嵌入式文件标识为一个可用的Web资源,以便于后面的提取操作。
为了提取该文件,我使用了Page.ClientScript对象的GetWebResourceUrl方法。如前所述,我想使页面开发者能够使用我自己的属性重载图像;因此我以如下方式进行提取:
If Me.HeadingImageUrl <> "" Then
imgMail.ImageUrl = Me.HeadingImageUrl
Else
imgMail.ImageUrl = Page.ClientScript.GetWebResourceUrl( _
Me.GetType(), "mailbox.gif")
End If
这正是我所喜爱的新的Web控件特征,因为我往往不时地在一些地方使用小图像来修饰我的控件。现在,我能够嵌入它们,并且只需要我的控件的DLL文件。
测试该控件将显示如图5所示的结果。
图5.带有嵌入式图像的最终完成后的控件
该Web表单使用一个HTTP处理器来检索图像并且把它生成到浏览器。显然,这种使用可能性远远不止此。
二、 总结
我希望你喜欢本系列文章所介绍的内容。其实,ASP.NET 2.0在Web开发方面加入了许多酷的内容,例如引入了母板页面,主题,数据源,以及其它优秀特征。事实上,你也可以把回调(一种新的页面特征)嵌入到控件中,从而为它们加入AJAX功能以实现更好的响应时间并产生一种更为丰富的客户端体验。遗憾的是,有关这一方面的内容已经非本文所及。在本文中所介绍的新的控件开发特征当前尚不多见;当然,这也是我写本文的原因。如你所见,这其中的大多数内容都与设计时刻体验相关;但是,我一直认为,提高控件的直观性与使之有用且能够实现既定功能一样重要。如果不容易使用,那么你甚至无法全面地使用该控件所提供的功能。