Anthony Moore Microsoft Corporation
2000年9月
摘要: 本文讨论如何习惯于使用 ASP+ 和 Web Forms 设计器。该程序针对不熟悉 Web 开发的 Microsoft Visual Basic 或类似的 GUI 编程工具的传统用户。
目录
您已经具备的技能
如果您使用 Microsoft Visual Basic 但又不熟悉 Web 开发,则首先使用带 ASP+ 的 Forms 设计器最为方便。 总的来讲,在 ASP+ 中使用控件、属性、事件和数据,与使用 Visual Basic 6.0 编写 Windows 应用程序没有太大差别。通过比较,ASP 和其它 Web 开发环境确实有很大不同,从一种环境换到另一种环境进行编程,可能就象再次学步。
到现在,您可能已见过 Web 窗体演示,或者动手试过一些,包括这一个。通常,下面是您要使用 Visual Studio 进行的操作:
创建一个新的 Visual Basic Web 应用程序。
将一个 Button 控件和一个 Label 控件放到页面上。
双击按钮,以获得一个事件处理器。
将 Label 文本改为“Hello World!”
运行该项目。
下面是您的第一个 ASP+ 页面在 Web Forms 设计器中的样子:
图 1. Web Forms 设计器中的一个 ASP+ 页面
我并不了解你,但我第一次这样做时,我的感觉是“这下可把 ASP 或 CGI 脚本干掉了。这真是太容易了!”
与创建 Windows 应用程序一样,创建一个可靠且可缩放的 Web 应用程序也极其复杂。我们希望 ASP+ 能够将这种复杂性的大部分隐藏起来,就象 Visual Basic 1.0 为 Windows 开发所做的那样。我们还想将使这两种经历尽量彼此相似,以便您可以“利用您已有的技能”。
大多数情况下您可以使用这些技能...大多数情况下
Web Forms 能够奇妙地完成 Web 开发任务,就象 GUI 开发一样,以至于它能在安全方面给您一个错误的感觉。然而,在这两者之间还存在一些最基本的差异,如果您不注意这些差异,就可能在着手做的时候产生一些常见的错误。
在您第一次尝试做以下各项时,您可能会有些束手无策:
声明并使用一个成员变量。
在运行时添加控件。
在页面之间传递数据。
尝试使用数据绑定。
震撼一: 页面寿命
在页面上添加一个计数器以统计这个按钮被单击的次数:
Private myCounter As Integer
Protected Sub Button1_Click(ByVal sender As System.Object,
ByVal e As System.EventArgs)
myCounter += 1
Label1.Text = myCounter.ToString()
End Sub
看起来它应能够工作,但当您运行这个页面时,您会发现计数器的值永远不会大于 1。
当借助控件事件和属性做简单事情时,看起来页面和控件在创建后, 在用户操作过程中将一直存在。即使我作为一个富有经验的 Visual Basic 用户,当第一次使用 ASP+ 时,我都会有以下两个关于页面寿命的问题:
在将页面废弃之前,服务器将该页面保留多长时间?
这些对象是否大量占用服务器的内存?
这两个问题的答案分别是“0 秒”和“否”。每次访问页面时,页面及其控件在创建后被完全丢弃。只有在处理用户请求的特定瞬间才会发生占用服务器内存的 Page 类实例。
ASP+ 的伟大技术成就之一在于它能够在到服务器的往返过程中保持大部分的页面状态,并且从不在内存中保持页面。然而,它并不是绝对地自动保存任何事件,当您声明一个新的成员变量时,除非您明确地将其保存,否则它将被丢弃。这就是计数器例子为什么不能像预期设定的那样工作。
以下是一个能够正确运行的事件处理器的方案:
Protected Sub Button1_Click(
ByVal sender As System.Object,
ByVal e As System.EventArgs)
Dim myCounter As Integer
myCounter = CInt(State("myCounter"))
myCounter += 1
State("myCounter") = myCounter
Label1.Text = myCounter.ToString()
End Sub
State 对象是 Page 类中各对象的集合,在这个对象中,您可以对其赋值并在往返过程中进行保存。这成为页面的查看状态的一部分,实际上是写在 HTML 中隐藏区域的信息的集合。运行时属性发生变化的控件,也可以在重新处理该页时利用查看状态来恢复这些值。
拥有持续值的一个更加简洁的方法是定义一个正式的属性:
Private Property myCounter() As Integer
Get
Return CInt(State("myCounter"))
End Get
Set
State("myCounter") = value
End Set
End Property
Protected Sub Button1_Click(
ByVal sender As System.Object,
ByVal e As System.EventArgs)
myCounter += 1
Label1.Text = myCounter.ToString()
End Sub
您实际上可以在 State 中加入较为复杂的对象,而依旧使其得到保持。不同于其它简单的类型,可以存储许多由通用语言运行时定义的容器类型,例如:ArrayList、 Hashtable 和 DataSet 对象。
并非全部得到保存
事情容易搞乱的原因在于,大部分数据在往返过程中被保存,但不是所有的数据都得到保存。以下是保存与不保存数据的一个小结。
保存:
页面属性
页面中声明的控件的属性
已知 Web 控件中的数据,例如 Repeater、 DataList 和 DataGrid
不保存 (除非您自己将其保存):
您加到 Page 类中的变量和属性
挂接在代码中而未被声明的事件处理器
在您的代码中添加或删除的控件
通过代码添加到表中的行和单元格
在使用过程中进行的任何修改
我们已经看到如何在 Page 类中使用 State 属性来饶过的第一个问题。为了理解剩下的问题,有必要先纵览一下页面得到处理时所发生的情形。
页面处理顺序
以下简述页面处理过程中发生的情形:
创建页面和控件:
从查看状态中恢复页面和控件的状态 (只限于回传):
根据用户输入更新页面控件 (只限于回传):
页面验证 (只限于回传):
引发事件:
页面和控件的状态保存到查看状态中:
页面和控件转换到 HTML:
释放页面和控件:
创建页面和控件: 每次处理页面时 (包括第一次),它将同基于 ASPX 文件内容的控件一起创建。在第一次访问 ASPX 文件并修改时,它实际上转化为生成的源代码并编译成 DLL 文件。由此,访问页面是非常快的,因为不需要进行分析,只是执行编译后的代码。这些代码创建控件、设定属性并同事件挂钩。
从查看状态中获得页面和控件的状态 (只限于回传): 如果 ASPX 文件中声明的控件在您编写的代码遭到修改,这些修改都将写入查看状态中。在第一次访问后的往返过程中,这些修改被重新应用到已存在的控件。这就是为何在往返过程中属性保持不变,但是比较复杂的变化 (如添加事件或添加控件) 则不然。
根据用户输入更新页面控件(只限于回传):基于服务器的输入控件能更新其属性。在 ASP 中,您只能手动更新输入控件,以使其记住用户的输入 (如本例)。因为这一步发生在大多数事件引发之前,所以您总是可以查询控件 (如 TextBox )的属性以获取用户录入的值。
页面验证 (只限于回传): 如果您使用的是有效的控件,则此时它们得到验证。再次,因为这一步发生在大多数事件引发之前,您可以可靠地检查 Page 对象或具体验证器的有效性。
引发事件: 这里是您所写代码的执行部分。第一个执行的事件是 Page_Load,接着执行的是与数据变化有关系的事件,如 TextBox 的 TextChanged 事件,最后引发真正导致页面回传的事件,如 Button 的 Click 事件。在第一次访问页面时,Page_Load 通常是唯一被执行的事件。
页面和控件的状态保存到查看状态中:任何声明控件的变化都保存在查看状态中,在此之后的变化,如表现时所执行的任何代码,都不会保存到查看状态中。
页面和控件转换到 HTML: 第一个子步骤叫 pre-render ,在此期间完成输出前所需的各附加步骤。因为它发生在其它所有事件之后,利用 Page 类 PreRender 实际上是一种非常有用的方法,可以用于编写在其它所有事件执行完且在输出前执行的代码。下一个子步骤是 render,此时才实际产生 HTML。有一些事件,如 Calendar 的 DayRender 事件直到此时才实际执行。同样,出于 ASP 兼容性目的, <%?>块内的代码此时才被执行。
释放页面和控件: 通用语言运行时创建和删除较小的对象时效率很高,所以创建和删除这些对象并没有太多的性能之忧。
与 GUI 开发相比,这种页面处理顺序看起来可能有些费解。然而,这个过程使对象的寿命减到最小,就有可能建立快速且可以根据大量同时用户进行缩放的 Web 应用程序。
了解这个顺序可以帮助您饶过各种不同的问题。例如,顺序解释了为什么在输出过程中对对象的进行的更改得不到保持的原因。在动态添加事件的情况下,您希望在 Page_Load 中进行添加,这样它们就总可以及时得到挂接。动态控件有点棘手,我们将在下面进行论述。
震撼二: 运行时添加控件
我们假定您想在一个表中动态地添加行,您必须借助设计器,在页面中放置一个 Table 控件,一个 TextBox 控件以及一个 Button 控件,如图 2 所示:
图 2. 带有 able、 TextBox 和 Button 控件的 Web Forms 设计器
当按钮被单击时,我们希望 TextBox 中的内容添加到表的行中去。您可以通过以下代码来实现:
Public txtRow As System.Web.UI.WebControls.TextBox
Public cmdAddRow As System.Web.UI.WebControls.Button
Public tbl As System.Web.UI.WebControls.Table
Protected Sub cmdAddRow_Click(
ByVal sender As System.Object,
ByVal e As System.EventArgs)
Dim cell As New TableCell()
Dim row As New TableRow()
cell.Text = txtRow.Text
row.Cells.Add(cell)
tbl.Rows.Add(row)
End Sub
然而,在往返过程中,控件本身不能自动得到保持,程序将不能按您所期望的那样运行。通常它将导致表中只有一行,原因是在每次往返过程中表都被重新建立。如果您想动态地创建控件,您必须手工记录足够的信息,以便每次都可以完全重新创建它们。在前述的示例中,您至少需要记住字符串列表。下面这个可行版本将字符串存储在查看状态中。
Protected Sub Page_Load(
ByVal Sender As Object,
ByVal e As EventArgs)
Dim s As String
For Each s In RowTexts
AddARow(s)
Next
End Sub
Private Sub AddARow(ByVal s As String)
Dim cell As New TableCell()
Dim row As New TableRow()
cell.Text = s
row.Cells.Add(cell)
tbl.Rows.Add(row)
End Sub
Private ReadOnly Property RowTexts() As ArrayList
Get
Dim o As ArrayList
o = CType(State("rowTexts"), ArrayList)
If IsNothing(o) Then
o = New ArrayList
State("rowTexts") = o
End If
Return o
End Get
End Property
Protected Sub cmdAddRow_Click(
ByVal sender As System.Object,
ByVal e As System.EventArgs)
RowTexts.Add(txtRow.Text)
AddARow(txtRow.Text)
End Sub
注意,由于事件的执行次序,有必要将行添加进 Click 事件和 Page_Load 中。下图显示的是运行时的情况:
图 3. 运行时添加行的表
虽然这个例子主要是增加一个表,您在运行时添加或删除任何控件都将面对同样的问题。并且,当您直接在页面中添加控件的时候,它们可能并不出现在您期望的地方。为了在运行时定位一个控件,您至少需要声明您想放置该控件的父控件。Panel 控件能被用作控件容器。
如果觉得所有这些太烦琐,一个简单备选方案就是使用 DataList 或 DataGrid。这些控件将为您创建一个 Table 对象并记住它的状态。要了解多信息,请参见 Nikhil Kothari 的文章 使用 ASP+ 列表绑定的控件 (英文)。
震撼三: 多页面应用
现在您知道页面在被处理的时候是作为对象而存在的,您可能也想到过创建多页面应用程序与此不太一样。从一个页面调用另一个页面的方法和 GUI 开发中调用一个页面的方法是完全不同的。
作为一个 GUI 开发者,您可能希望创建一个关于 Page 类的实例,设置一些属性,并且将其激活。然而,Web 应用程序并不是那样工作的 — 一个页面不可以直接访问另一个页面。 ASP 开发者非常熟悉页面之间通信的技巧,但 GUI 开发者还有一些知识要学。
转移控制
在页面之间转移控制有两种方法:
超级链接: HTML 通过超级链接来将控制从一个页面转到另一个页面,可以使用 HTML 语言的 <A>;或使用 HyperLink 控件来进行声明。使用超级链接的优点在于可以直接跳转到新的一页而不需要向当前页回传数据。
重新定向: 在服务器上的一些处理事件的代码中,您可以调用响应重新定向的功能,并在 RUL 中传递 (如上)。例如,在更新数据库之后进行此类操作。
这两种方法均涉及在一个全球资源定位地址 (URL) 中进行传递。它们通常是对同一应用程序中的另一个页面的相对参照。您可以通过在 URL 中嵌入字符串来在页面之间传递参数。这些将在下一节中进行描述。
传送信息
Visual Basic 用户可能会尤其觉得从一个页面向另一个页面传送信息相当于倒退到 Visual Basic 3.0 甚至更早版本的时代 — 那时必须经常使用全局数据来进行窗体间通信。这虽然带来一定的麻烦,但收益是应用程序可以被数千人同时使用而不只是一人。
以下是将信息从一个页面传递到另一个页面的方法:
URL 参数: 该方法将字符串嵌入 URL 中。例如,要访问同一目录下的名为 WebForm2.aspx 的页面,并且传送一个值为“Bar”的参数 “Foo”,您可以使用 URL WebForm2.aspx?Foo=Bar。被调用的页面可以用如下代码来获取这个参数:
Dim Foo As String
Foo = CStr(Request.QueryString("Foo"))
会话状态:推荐使用这种方法来保存复杂的或安全性信息。在某些情况下,即使您设计只有一个页面的应用程序,出于安全原因您也应当考虑首先使用 会话状态而不是查看状态。编写代码时,您可以访问 Session 控件,该控件是与用户浏览器相关的值的一个集合。同 State 控件一样,Session 控件也是借助字符串加以识别的控件集合。类似于查看状态,该控件可以存储简单的类型,也可以存储容器类型,诸如 Array、 ArrayList、 Hashtable 和 DataSet。
应用程序状态:为了保存和读取有关您的应用程序的所有当前用户的信息,请使用 Application 控件,它是一个和会话状态和查看状态有着同样接口的控件。
关于安全性的说明
因为 Web 在很大程度上是不安全的,所以在您的应用程序中导致安全漏洞是很容易的事情 (令人不安)。您应当意识到哪些是安全的,哪些是不安全的。
查看状态和 URL 参数两者均不安全: 有些人不仅可以比较容易地看到这些值,有些人还可能将其修改后再传。您应当确信这些值中不含有涉及安全的信息,并且您的数据不会因为有人改变这些值而遭到破坏。
会话状态和应用程序状态是安全的: 因为它们存在于服务器上并且是隐蔽的。除非您用代码加以允许,否则黑客是不可能修改这些值的。安全信息应储存于会话状态或应用程序状态中。
验证是安全的: 这是因为验证检查总是在服务器上再次执行,即使有人想在客户机上将其饶过。服务器验证是一种安全机制,也是针对客户机验证的一种后退机制。
震撼四: 数据绑定
令人惊叹的是数据绑定不再是一个讨厌的字眼了。 在传统的 GUI 编程中,数据绑定经常导致失败。Visual Basic 程序员首先应了解: 数据绑定所带来的麻烦往往要比其自身的价值大。您在使用它创建一个实实在在的应用程序时可能会经常遇到麻烦。这主要用于无须事务控制的原型开发、报表和数据录入。
ASP+ 数据绑定对于实际的商务应用程序是非常有用的,具有讽刺意味的是,其原因它所具有的特性较少。ASP+ 数据绑定和传统的 GUI 数据绑定有以下两点显著不同:
它是显式的:您可以通过访问 Page 类的 DataBind 方法或数据绑定控件来实现数据绑定。传统的数据绑定是自动执行的,从而不易于控制。
它是单向的:在典型的商用事务中,数据绑定非常适用于从数据库中获得数据,但是通常您想进行许多定制过程或调用所存储的函数或业务对象来完成更新。因为在错误的时间自动更新数据通常会产生问题,所以数据绑定将从数据源提取信息,但又由您去进行更新。您这使您拥有很多控制能力,同时依旧可以用于报表和原型开发。
数据绑定模型的另一个优点是它可以用于内存中一些简单信息,如结构数组。在 Visual Basic 中,根据数据来源 (内存或数据库) 的不同,数据绑定的控件通常有两种截然不同的工作方式。在通用语言运行时中,数据绑定通过低级接口起作用,因此,对简单数据结构的处理方式,与对来自传统数据库的数据的处理方式是相同的。
结论
如您所见,Web 开发和 GUI 开发是两个完全不同的事情。本文旨在帮助您在两者之间转换时避免一些最容易犯的错误。理解概念性模式,可能有助于您解决大部分可能遇到的困难。