作者:David S. Platt 出自:微软
本文假定您熟悉 Visual Basic .NET、C# 和 HTML
下载本文的代码: WebC.exe (274KB)
摘要 预建的自定义控件可以简化和加快应用程序的设计,并使您能够维护 UI 的一致性。但是,预先打包的控件可能很大,速度很慢,并且是特定于操作系统的。对于不愿意使用预先打包的控件的人来说,Visual Studio .NET 提供了类似于 Windows 窗体中的控件的 Web 窗体控件,其中包括标签和文本框,以及新增的 DataGrid 等,所有这些控件都可以进行自定义。
如果要设计自己的控件,您可以通过使用 .NET 框架所提供的可继承类来避免繁杂无味的工作过程,包括页生存周期、在调用之间维护状态以及浏览器检测。本文将对这些概念以及事件处理、呈现和客户端脚本进行介绍。
控件是一个很吸引人的概念。使用预先打包的 UI 功能时,可以更快、更廉价地完成设计,并能够在不同的应用程序之间保持更为一致的 UI。但这并不是它的所有特点。控件也可能会很大,与为特定任务而专门编写的代码相比,可能运行速度也要慢些。并且,基于 Windows 的控件结构如 Windows 窗体控件和早期的 ActiveX、OCX 和 VBX 控件,仅可以在 Windows 环境中运行 - 在当今几乎每天都有新的平台类型脱颖而出的异类 Internet 环境中,这很成问题。
为了能够跨平台运行,Microsoft 设计了Web 窗体结构 ASP.NET。使用 Visual Studio,您可以从工具箱中选择称为 Web 窗体控件的组件,并将它们放置在 ASPX 页上。然后,您可以设置该控件的属性,并使用任何支持 .NET 的语言编写代码,从而将其行为与其他控件相关联。这个过程被设计为与用 Visual Basic 编写应用程序类似,对于大多数程序员来说,这都是一个非常熟悉的过程。
客户端请求包含 Web 窗体控件的页时,ASP.NET 处理器将加载该页并在服务器上创建这些控件,如图 1 所示,然后执行该页的编程逻辑,将控件关联到一起。该过程结束时,每个控件都会向 ASP.NET 提供描述其当前外观的 HTML。这些 HTML 将被返回到客户端,并在浏览器中呈现。请在以下位置阅读有关 ASP.NET Web 窗体的信息:ASP .NET:Web Forms Let You Drag and Drop Your Way to Powerful Web Apps。
Visual Studio 附带了一组通用的 Web 窗体控件,或多或少地与 Windows窗体中的可用控件组相对应。它包含标签和文本框等已为大家所熟悉的常用控件,还包含更新、更为复杂的控件如 DataGrid。
本文介绍了 .NET 框架为编写自己的 Web 窗体控件而提供的功能。由于我已经在 MSDN® Magazine 的 2002 年 4 月刊中介绍了控件的基本概念(方法、属性和事件),本文中,我将主要介绍 Web 窗体控件与基于 Windows 的控件之间的区别。之所以存在这些区别,主要是由于在相对简陋的浏览器运行库环境中运行,而不是在资源丰富的Windows 环境中运行。
.NET 框架中包含预先创建的软件类,这些类使得编写 Web 窗体控件变得相对简单。您需要理解 HTML 才可以生成控件所要求的输出,就像 Windows 窗体控件的设计者需要理解 Windows GDI 一样。但是,挂钩到 ASP.NET 页生存周期的例程、在多次调用之间维护状态的例程和检测宿主浏览器功能的例程(全部控件都具备的基础结构)已经为您编写好了。
您可以使用自己所选择的与 .NET 兼容的语言来编写控件,通过从自己选择的若干个 .NET 框架类继承来使用预先创建的基础结构。这些基类大致对应于 Windows 窗体中的类。但是,常见的情况是,Microsoft 使用了同一个名称来在看上去类似的环境中指代不同的事物,因此,必须谨慎从事,并阅读附属细则。下面介绍开发 Web 窗体控件的五种方法。
第一种选择是从 System.Web.UI.Control 派生,该类是所有 Web 窗体控件的基类。它参与 ASP.NET 页呈现过程的所有生存周期事件。文档中指出该类没有任何特定于用户界面的功能,虽然它存在于 System.Web.UI 命名空间中。我有不同意见。它包含一个 Render 方法(将在下一节对其进行介绍),控件使用该方法来发出要在浏览器中显示的 HTML。它比我要介绍的下一个类具有更少的内置 UI 属性;例如,它没有 Width、Height、ForeColor、BackColor 和 Font,但如果您一定要使用它来生成 UI,实现起来仍是非常容易的。由于省略了以上元素,它更为轻型化,但由此所造成的差异微乎其微。如果您不关注它所省略的任何功能,使用它作为基类完全没有问题。
您的下一个选项是从 System.Web.UI.WebControls.WebControl 派生,该类派生自 System.Web.UI.Control。这是一个添加了基本用户界面属性的控件。当您生成新的 Web 控件库项目时,Visual Studio 会自动使用它作为基类。如果您想要得到像大多数控件一样提供用户界面的控件,您可能应当从这里开始。
您还可以从现有的 Web 窗体控件入手,可以是 Visual Studio 附带的控件,或者从第三方购买的控件。此时,您需要使用 .NET 继承机制从现有控件派生自己的控件。您需要重用现有功能中自己需要的部分,重写要更改的部分,并添加要使控件具备的任何附加功能。在我以前的文章中,曾经介绍了如何使用 Windows 窗体文本框控件来完成以上任务。针对 Web 窗体控件完成该任务与此完全相同,这里不再重复介绍。
另一个选项是设计一个包含其他控件的控件。在 Windows 窗体中,它称为用户控件,但是在 Web 窗体中,它称为复合控件。您可以为自己的基类选择以上所介绍的三种继承方案中的任一种。不幸的是,Web 窗体不像 Windows 窗体一样支持添加子控件。因此,必须在代码中手动创建和定位子控件。这并不困难,因此这里不作讨论。但是,我发现这一省略颇为令人吃惊。
最后,您可以选择创建用户控件。以上所讨论的每个控件都是完全编译的 .NET 程序集。它们可用于 Visual Studio 工具箱和设计器,并可以保存在全局程序集缓存 (GAC) 中,因此无需为每个要使用它们的客户端使用一个单独的副本。Web 窗体提供了另外一种生成可重用控件包的方法(用户控件)。像您在 Windows 窗体中看到的用户控件一样,Web 窗体用户控件是通过在 Visual Studio 设计器中将其他控件放置到设计图面上来生成的。但是,与 Windows 窗体用户控件不同,这种用户控件是一个 HTML 页,而不是已编译的程序集,因此不能保存到 GAC 或 Visual Studio 工具箱中,而且,也不能在 Visual Studio 设计器中显示其外观。因此,我认为它的有用性远不如我所介绍的其他类型的 Web 窗体控件。我怀疑它之所以存在,是因为复合 Web 窗体控件中缺少设计器支持。
在学习或者讲解新软件时,我总会从我所能想到的最简单的示例入手。这是一个标签控件,它包含一个称为 Text 的属性,该属性是标签所显示的字符串,它还包含一个称为 ForeColor 的属性,即该文本字符串的颜色(参见图 2)。
我首先在 Visual Studio 中生成了该项目,然后从 New Project 对话框中选择了 Web Control Library,如图 3 所示。向导生成了一个项目,该项目包含一个派生自 System.Web.UI.WebControls.WebControl 的新类。向该类中添加方法和属性与向任何其他 .NET 类添加方法和属性完全相同。实际上,系统提供的基类已经包含了本示例中所使用的称为 Text 和 ForeColor 的属性。
就像我说的一样,这非常简单。基本上是这样的。理解自定义 Web 窗体控件的关键在于 Render 方法,该方法在概念上与 Windows 窗体 OnPaint 方法相同,区别仅在于前者发出 HTML,而后者则发出 GDI 调用。当 ASP.NET 服务器框架为响应用户请求而汇编 Web 窗体页时,它将创建页上列出的控件,设置这些控件的属性和持久性数据,然后调用它们的各个 Render 方法。框架实际上是告诉控件:“你是活动的,并正处于预期的状态。你需要告诉我你的外观,因为我没有别的办法来了解。”Web 窗体控件的作者会在 Render 方法中放入一些代码,以发出 HTML,告知浏览器如何根据控件的当前状态和属性,以及与控件有关的环境中的任何其他信息来显示控件的外观。
Web 窗体控件的作者需要了解 HTML,这是因为环境提供的相关摘要信息很少。以下是指定文本颜色的 HTML:
Here is some text
为了生成这些 HTML,我编写了图 4 中所示的代码。大多数读者都告诉我他们希望源代码使用 Visual Basic 来编写;为了照顾 C# 读者,我使用 Visual Basic 和 C# 两种语言编写了可下载的示例代码。
当 ASP.NET 框架调用控件的 Render 方法时,将传递一个 System.Web.UI.HtmlTextWriter 类型的对象。这在概念上类似于 OnPaint 方法在 Windows 窗体控件中收到的 System.Windows.Forms.PaintEventArgs 的 Graphics 成员。两者都代表到框架的连接,该框架将输出定位到其相应的位置。HtmlTextWriter 包含的方法、属性和常数使得您的控件能够将 HTML 发送到将被发送到客户端浏览器的输出页上。在示例代码中,我首先调用了方法 AddStyleAttribute,该方法在内部创建一个称为 style 的 HTML 属性,将其值设置为控件所继承的 ForeColor 属性的值,然后将其添加到内部缓冲区。可以通过对 AddStyleAttribute 方法的附加调用向缓冲区添加 style 属性的附加值;通过调用 AddAttribute 方法,可以添加其他属性,当然,在本例中,并不需要这么做。
接着,我调用了方法 RenderBeginTag,指定文本中要显示的 HTML 标记的名称,本例中为“span”。该调用从内部缓冲区中提取任何属性(此处为 style),将它们放置到标记中,然后写入 HTML输出流中。这两个调用生成了第一行 HTML:
接下来,为了编写标签的文本,我调用了方法 HtmlTextWriter.Write,以传递控件的内部文本字符串。该方法将文本字符串 verbatim 传递到 HTML 输出流中,从而生成了第二行:
Here is some text
为了关闭 标记,我调用了 HtmlTextWriter.RenderEndTag。这导致编写器读回最后一个打开的标记,并发出该标记的关闭标记,在本例中为 ,以作为最后一行 HTML。
该对象包含用于执行输出的其他方法,这些方法能够提供更为精细的控件,但较为复杂。为简单起见,我将在本文的其余部分中沿用前面的方法。
最后,我需要一个客户端来使用该控件,以便对其进行调试和显示。我向现有的解决方案添加了一个包含 ASP.NET 页的新 Web 应用程序。为了向工具箱中添加新的 Web 窗体控件,我右键单击并选择了 Customize Toolbox,调出了图 5 中所示的对话框。我浏览到了新的 Web 控件 DLL,选择了它,该控件即出现在控件列表中,如图 6 中所示。
之后,就可以将它放置到 ASPX 页上并设置其属性。生成项目并在浏览器中启动它时,控件的外观如图 2 所示。
现在,我们已经介绍了 .NET Web 控件的基本功能,接下来,让我们看一个示例,该示例演示了 Web 窗体控件如何为页上的其他控件激发 .NET 事件。我发现 SDK 文档在介绍 Web 控件事件时很模糊。它将用户在浏览器上的单击(从而启动过程)、对它所触发的服务器的回发,以及收到回发的服务器端控件向页上的其他控件发送的通知全部都定义为事件。下面,我将尝试对这些事件之间的区别进行准确的说明。
我编写了一个更为复杂的示例控件,该控件显示在图 7 中的 ASPX 页上。它在用户的浏览器中显示一个表格,该表格的每个单元格中都显示自己的行号和列号。该控件公开称为 Rows 和 Columns 的属性。这两个属性都是整型数,在设计时设置。当用户单击表格中的任何单元格时,窗体将被回发到服务器。在服务器上汇编并初始化该页后,该表格控件将确定用户单击的是哪个单元格,并在服务器上向页上任何愿意侦听的其他控件激发一个 .NET 框架事件。在本例中,页中包含一个事件处理程序,该处理程序设置一个单独的标签控件的值,以显示用户单击的单元格的行号和列号。
开发此控件时,我先是将 Render 方法编写为直接发出显示具有所需行数和列数的表格的 HTML,那非常简单。接着,我想要添加导致浏览器在用户单击表格中的单元格时将窗体回发到服务器的 HTML。这需要使用一些其他技巧(参见图 8)。
可以看到,每个表格数据项 () 都创建一个单元格,且都包含一个 onClick 属性,该属性调用一个客户端脚本,传递要处理回发的服务器端控件(本例中为表格控件)的 ID 以及一个包含任意参数的字符串。这里,该字符串为表格单元格的文本,这使得服务器端控件能够标识用户所单击的单元格,但该字符串可以是您所需要的任何内容。图片底部显示的客户端回发脚本将这些参数放置到隐藏的输入控件中,并执行对服务器的回发。
稍后我将讲解对这种回发的服务器端处理,让我们首先看看这些 HTML 是如何生成的。这看上去很繁琐,好消息是您无需自己编写它们。请记住,您的控件存在于 ASP.NET 页上,因此能够通过基础 Page 类成员变量访问该页的所有方法。方法 Page.GetPostBackEventReference 导致框架生成页上的 HTML 脚本,并返回调用它的 HTML 字符串。接着,我将该字符串添加到 元素的属性中,该元素能够使用前述的 HtmlTextWriter 方法。您可以在图 9 中看到 Render 方法的代码。请暂且忽略处理视图状态的代码部分;我将在下一节中对它们进行说明。)
如果浏览器不能运行 JScript 怎么办?我将在稍后进行讲解。现在,先假定浏览器能够运行 JScript,或者,我们明确声明我们仅关注浏览器能够运行 JScript 的情况。
回发窗体到达 ASP.NET,ASP.NET 在服务器上加载目标页,并创建它上面的控件。ASP.NET 需要将回发传递给它的目标控件。ASP.NET 是通过客户端脚本填充的隐藏输入控件得知该目标控件的。服务器端控件通过实现称为 IPostBackEventHandler 的接口并重写 RaisePostBackEvent 方法来接受该输入通知。我认为该方法名称非常容易引起误解。它不会引发回发事件;它通过 ASP.NET 从浏览器接受一个事件并引发一个服务器端 .NET 事件。该方法存在的唯一目的是将浏览器发送的通用窗体回发事件转换为具有有用参数的已命名且有意义的 .NET 控件事件,其他服务器端控件可以侦听该事件,页设计器可以方便地为该事件编写代码。如果将它看作 AcceptPostbackAndOptionallyRaiseServerEvent,那么您大脑中得到的就是正确的模型。
我在这个命名方式很差劲的方法中放入了一些代码,在控件从用户的浏览器接收回发时执行这些代码(参见图 10)。查明哪个控件应当接收回发后,ASP.NET 调用此方法并向它传递 eventArgument 字符串,该字符串是由客户端传递给客户端脚本然后在隐藏输入变量中传输的。本例中,该字符串为用户单击的表格单元格的文本。我的示例代码可以从字符串中分析出行号和列号,因此服务器端处理程序知道用户单击了哪个单元格。
如果已知您的控件是唯一一个关注谁导致了回发的控件,什么也不需要做;只需在 RaisePostBackEvent 方法中的适当位置编写处理程序代码即可。但是,您的控件在收到回发后所需要完成的主要任务之一是通知页上的其他控件您的控件上发生了某些事件。
为此,您的控件需要使用 Windows 窗体控件中使用的相同通用事件处理机制向页和其他服务器控件激发 .NET 事件。本例中,我向控件添加了一个称为 TableCellClicked 的事件,其中包含两个参数,即单击的行和列,如图 10 所示。可以为所需的任何控件安装处理程序,以接收该事件。在我的示例中,页中包含一个处理程序,接收来自表格控件的事件,并将所单击的单元格设置到标签控件中(参见图 11)。
总之,Web 窗体控件中的事件处理包括两个必需的部分,和一个可选的第三部分。首先,控件的 Render 方法必须生成客户端 HTML,这些 HTML 在客户端发生您所关注的事件时导致对控件的回发。其次,您的控件必须实现 IPostBackEventHandler 接口,以便 ASP.NET 能够通知您的控件它收到了该回发,并传递有关它的附加信息。接下来是一个可选的部分,您的控件可以(很可能会选择)激发 .NET 事件,这样其他控件就可以接收所发生的这些事件的通知。
Web 页本质上是无状态的。这么讲的意思是一个页上所显示的内容与用户以前所查看的内容无关,除非编写代码将它们关联起来。当用户只是查看静态文本页时,没有太大问题。但是,由于目前与 Web 站点的大多数交互都涉及跨越多个页的持续会话,这将造成严重的问题。SDK 文档声称您必须“向用户提供连续性的假相”。文档作者对问题的分析是完全过时的。用户体验是一切的核心,这是金科玉律。应当是通过您的代码来达到用户的预期,而不是其他方式。如果您的编程模型与用户的需求不符,您就必须编写代码来使其相符。用户的连续性是真实的,真正的假相是代码的连续性。
在 ASPX 页上编写代码的设计者能够访问 Session 和 Application 集合对象等功能,以维护一个页到另一个页的状态。但是控件设计者无法使用它们,因为她不知道页的会话状态何时因超时而被放弃,或者被页程序员显式转储。实际上,她甚至不知道是否已打开会话状态,并且无法在未打开会话状态时对其进行更改。因此,如果您的控件需要在对页的一次呈现到另一次呈现之间维护状态,需要采用其他方法。
.NET 框架提供了一种机制,使得 Web 窗体控件能够安全且方便地维护它们的状态。控件基类包含一个称为 ViewState 的成员。它是一个与 Session 和 Application 对象集合使用的属性包集合类型相同的属性包集合,不同之处在于它将自己的数据存储在页上的一个隐藏文本字段中。您可以在图8 中名为 __VIEWSTATE 的隐藏输入字段中看到视图状态。
当您的控件将数据置入视图状态集合中时,ASP.NET 将它序列化到 ViewState 字符串中,并将其作为已呈现的页的一部分传输到客户端。当页被回发到服务器时,ASP.NET 将提取隐藏的变量并将其反序列化到各个控件的 ViewState 成员变量。这种体系结构对于使用网络场时的可伸缩性尤其有利,这是因为它避免了任何类型的服务器关系。任何处理回发的计算机都可以看到上一次的状态,并为下一次存储它(可能是更新后的状态)。
我将我的示例表格控件编写为使用 ViewState 记忆其单击状态,即用户单击的单元格的行和列。在我的控件的构造函数中,可以看到我创建了用于记忆所单击的行和列的视图状态变量,并将它们设置为 -1,指示无选项。
' Start member variables in desired default state; ' no selected cell Public Sub New() Me.ViewState("SelectedRow") = -1 Me.ViewState("SelectedColumn") = -1 End Sub
从客户端收到单击回发时(参见图 10),我提取了用户所选择的单元格并将其存储到 ViewState 中。呈现时(参见图 9),我提取了选定的行和列,并调整 HTML 以正确地显示选定的单元格。这就是所有的步骤。很简单是吧?
请注意,基类包含一个称为 EnableViewState 的成员变量,它的说明指出它通知控件是否将其内部状态保存到 ViewState 中。但是,如果使用示例代码,将发现该变量看上去对其无任何效果。这是因为该变量没有在内部关闭 ViewState 机制。它只是一个 Boolean 标志,用于通知控件:对于页设计器来说,最好提前关闭 ViewState。是否编写代码以检查并响应该变量的状态全在乎您的选择,在本例中,我选择了这么做。看到有人违反了最少惊诧原则时多么惹人生厌了吗?所以千万别对自己的客户这么做,好吗?
前面讲解过的大多数控件功能都发生在服务器端,事实上,许多人将这些 Web 窗体控件称为“服务器控件”,以便强调这一点。真正好的 Web UI 设计通常要求客户端上至少存在一些浏览器脚本形式的代码。例如,Web 窗体工具包中的验证程序控件确保用户使用满足其条件的数据(任何字符串、有效的电子邮件地址、5 到 15 之间的整数等)填充了表单中必需的字段,然后才允许提交窗体。如果数据未能满足验证程序的条件,该验证程序将显示一条错误信息,并中止回发。这节省了网络带宽、服务器周期,并可以防止用户产生沮丧情绪,这是因为反馈是即时的。这正是我们所希望看到的组合。
ASP.NET 框架提供了用于 Web 窗体控件的内置功能,使其能够方便地发出要放在返回到客户端的页中的脚本,并能够方便地访问它们自身或其他控件放置在该页中的脚本。我编写了一个示例程序,显示了控件可以在返回给客户端的页上放置脚本的三个位置(参见图 12、图 13 和图 14)。
控件用于在页上放置脚本的所有方法都位于 Page 成员变量中,该变量表示控件所在的页。控件可以通过 Page.RegisterStartupScript 方法在页上放置一个启动脚本。在用户的浏览器中显示该页时,该脚本将自动执行。该示例程序在显示页时仅弹出一个消息框。您还可以使用 Page.RegisterClientScriptBlock 方法放置一个脚本块,该脚本块需要由页上的其他脚本代码显式调用。示例程序在您单击页上的文本时仅会弹出一个消息框。
两个方法都接受两个字符串参数:脚本块和脚本本身的名称。如果两个控件试图使用相同的名称注册一个脚本,页将忽略第二次尝试。这意味着要避免与不是由您自己编写的其他控件发生命名冲突,行业级的控件应当使用唯一的长名称,而避免使用一般性的短名称,如 MyScript。如果要在注册脚本块之前查明该脚本块是否已被注册,可以使用 Page 对象上的两个方法来实现,即 IsClientScriptBlockRegistered 和 IsStartupScriptBlockRegistered。
您所传递的脚本可以是文字的,如本示例中的脚本。但是,有时脚本可能会很长,像验证程序一样。这种情况下,您可能需要将脚本放置到单独的文件中,以便在运行时提取。本例中,可以将脚本标记编写为使用 src 属性将脚本执行引擎指向脚本文件的位置,如下所示:
客户端脚本最常见的用途可能是在允许提交窗体之前对数据进行验证。为此,必须通知您的代码即将发生的回发操作,并使脚本具有在要求未能得到满足时取消回发的能力。使用 ASP.NET 框架,可以通过 Page.RegisterOnSubmitStatement 方法注册提交处理程序。提交窗体时,浏览器将逐步通过所有已注册的提交语句,以查看是否应当继续提交。您的提交语句必须包含您的验证逻辑,如果要允许提交操作继续,则返回 true,否则返回 false。示例程序将一个提交处理程序放入生成的脚本中,该脚本在单击 Submit 按钮时弹出一个提示框。如果输入“Y”,提交将继续;否则将中止操作。
并不是所有的浏览器都可以运行以您首选的语言编写的脚本,有些浏览器根本不能运行脚本。您需要一种检测这种情况的方法,这样您的控件就可以决定是要以降级方式运行,通知用户,还是根本不运行。例如,当验证程序控件无法在客户端执行其验证任务时,将在服务器端运行其逻辑。实际上,即使它们认为自己已经成功地在客户端运行,它们仍会在服务器端运行,这只是为了确保它们没有以某种方式错误地被中止或者篡改,从而将无效的数据注入服务器。
Page.Request.Browser 对象保存有关浏览器功能的信息。示例代码读取属性 Type、VBScript、JavaScript 和 EcmaScriptVersion,并将它们显示在页上供您查看。您可能还希望控件上具有一个允许页设计器关闭脚本功能(即使浏览器能够运行脚本)的属性。验证程序控件包含一个称为 EnableClientScript 的 Boolean 属性。您可能需要遵照这一设计模式,除非您的控件在没有客户端脚本功能的情况下毫无意义。
Web 窗体简化了编写优秀 Web 应用程序的过程,而 .NET 框架则使您能够轻松地创建 Web 窗体控件。您所必须理解的只是控件的业务逻辑,以及如何以 HTML 呈现它;基础结构的其余部分都是由从 .NET 框架继承的类处理的。
相关文章,请参阅:
ASP .NET: Web Forms Let You Drag and Drop Your Way to Powerful Web Apps
Windows Forms: Developing Compelling User Controls that Target Forms in the .NET Framework
David S. Platt 是 Rolling Thunder Computing Inc. 的总裁和创始人。他在哈佛大学和全球各地的公司讲授 .NET 技术。他发布了一个有关 .NET 的免费电子邮件新闻稿,网址为 http://www.rollthunder.com。David 是 Introducing Microsoft .NET (Microsoft Press, 2001) 一书的作者。
本文摘自 MSDN Magazine 的 2002 年 6 月刊。