科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件探讨ASP.NET 2.0中的Web控件改进技术

探讨ASP.NET 2.0中的Web控件改进技术

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

ASP.NET 2.0并没有抛弃1.1版本中的任何现有控件,而是增加了一组新的控件;同时还引入了若干新的控件开发技术。本系列文章将对这些内容展开全面探讨。

作者:朱先忠 来源:IT专家网 2008年6月10日

关键字: 技术 web asp net Windows

  • 评论
  • 分享微博
  • 分享邮件
全面探讨ASP.NET 2.0中的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调用了。

  另外,还存在其它一些新的基类,例如用于数据绑定的控件等,在此不再赘述。全面探讨ASP.NET 2.0中的Web控件改进技术之ControlState篇(二)

  我的观点是: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,那么你的控件尽管可能看起来样子别扭,但是仍能正确工作。

  全面探讨ASP.NET 2.0中的Web控件改进技术之灵敏标签篇(三)

  当你最开始在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(见源码文件)中的最终代码展示了我要添加到这个集合中的所有项。当该方法完成时,它简单地返回这个集合。在后面一篇中,我们将讨论控件设计器类的问题。

  全面探讨ASP.NET 2.0中的Web控件改进技术之控件设计与模板设计篇(四)

  一、 控件设计器

  控件设计器类派生自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()

  现在,我需要使用我在“灵敏标签”一节中