Skip to content

7. ASP 服务器组件 - 1

7.1. 简介

在本章中,我们将介绍 ASP.NET 推荐用于构建用户界面的技术。我们知道,Web 服务器处理 .aspx 页面时包含两个截然不同的阶段:

  1. 首先,执行页面控制器。该控制器包含的代码可能位于 .aspx 页面本身中(WebMatrix 解决方案),也可能位于单独的文件中(Visual Studio.NET 解决方案)。
  2. 随后,.aspx 页面的呈现代码被执行,并转换为发送给客户端的 HTML 代码。

Image

ASP.NET 提供了三种标签库用于编写页面的呈现代码:

  1. 经典 HTML 标签。这就是我们迄今为止一直在使用的。
  2. 服务器端 HTML 标签
  3. Web Forms 标签

无论使用哪种标签库,页面控制器的作用始终如一。它必须计算呈现代码中出现的动态参数的值。迄今为止,这些动态参数都很简单:它们是 [String] 类型的对象。因此,如果在呈现代码中我们有一个 <%=name%> 标签:

  • 页面控制器会声明一个类型为 [String] 的 [name] 变量并计算其值
  • 当页面控制器完成工作且执行呈现代码以生成 HTML 响应时,<%=name%> 标签会被替换为控制器代码计算出的值

我们知道控制器与视图的分离是任意的,且控制器代码与视图代码可以在同一页面上混合使用。我们已解释过为何不建议采用这种做法,并且我们将继续遵循控制器与视图的分离原则。

[server HTML] 和 [WebForms] 标签允许您在呈现代码中包含比简单的 [String] 对象更复杂的对象。这有时会非常有用。让我们以一个包含列表的表单为例。该列表必须通过类似于以下形式的 HTML 代码呈现给客户端:

<select name="uneListe" size="3">
    <option value="opt1">option1</option>
    <option value="opt2">option2</option>
    <option value="opt3" selected>option3</option>
</select>

列表内容和待选选项是动态元素,因此必须由页面控制器生成。我们已经遇到过这个问题,并通过包含以下标签解决了它

<%=uneListeHTML%>

该标签将被 [uneListeHTML] 变量的 [String] 值替换。该值由控制器计算得出,必须是列表的 HTML 代码,即“<select name=..>...</select>”。这并不难实现,且似乎是一个优雅的解决方案,避免了将生成代码直接放入页面的展示层。 在此情况下,原本需要插入测试逻辑的循环会因此变得“杂乱无章”。尽管如此,这种方法仍存在一个缺点。页面中控制层与展示层的分离,同时也划分了两个职责范围:

  • .NET 开发人员负责页面控制器,
  • 图形设计师负责页面的呈现部分

在此,我们可以看到列表的 HTML 代码生成已被移至控制器中。图形设计师可能希望修改此 HTML 代码以改变列表的“视觉”外观。他们将被迫在 [控制器] 部分进行操作,从而超出其专业领域,并伴随不慎在代码中引入错误的风险。

服务器标记库解决了这一问题。它们提供了一个代表 HTML 列表的对象。例如,[WebForms] 库提供了以下标记:

<asp:ListBox id="uneListe" runat="server"></asp:ListBox>

该标签表示一个 [ListBox] 对象,可由页面控制器进行操作。该对象具有若干属性,用于表示 HTML 列表中的各种选项以及指定所选选项。因此,页面控制器会将相应的值赋给这些属性。当呈现层执行时,该标签

<asp:ListBox id="uneListe" runat="server"></asp:ListBox>

将被替换为表示 [ListBox] 对象的 HTML 代码,即 "<select ..>...</select>"。目前,除了采用面向对象的编码方式这一有趣之处外,与前一种方法并无本质区别。 让我们回到那位需要修改列表“外观”的图形设计师。服务器标签具有样式属性(如 BackColor、Bordercolor、BorderWidth 等),允许您设置对应 HTML 对象的视觉外观。因此我们可以编写:


                <asp:ListBox id="ListBox1" runat="server" BackColor="#ffff99"></asp:ListBox></P>

其优势在于,图形设计师可以直接在呈现代码中进行这些修改。这相较于前一种方法具有明显的优势。因此,服务器端标签库简化了网页呈现层的构建——即我们在前几章中所提到的用户界面。本章的目的是介绍这些标签库。 我们将看到,它们有时会提供诸如日历或与数据源关联的表格等复杂对象。它们具有可扩展性,这意味着用户可以创建自己的标签库。因此,用户可以创建一个在页面上生成横幅的标签。所有使用该标签的页面都将显示相同的横幅。

为某个标签生成的 HTML 会根据客户端浏览器的类型进行适配。当浏览器向 Web 服务器发送请求时,其 HTTP 头部中会包含一个 [User-Agent: xx] 字段,其中 [xx] 代表客户端的标识。以下是一个示例:

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

借助这些信息,Web 服务器可以判断客户端的能力,特别是它能够处理的 HTML 代码类型。随着时间的推移,HTML 语言确实经历了多个版本的演变。 现代浏览器支持该语言的最新版本,而旧版浏览器则不支持。根据客户端发送的 [User-Agent:] HTTP 头,服务器将返回客户端能够理解的 HTML 版本。这是一种实用且有效的方法,因为它意味着开发人员无需担心应用程序所使用的具体客户端浏览器类型。

最后,诸如 Visual Studio.NET、WebMatrix 等高级集成开发环境(IDE)支持以“Windows 风格”设计 Web 界面。虽然这些工具并非必需,但它们能为开发人员提供显著的帮助。 开发人员通过在界面中放置图形化组件来设计 Web 界面。他们可以直接访问每个界面组件的属性,并按需进行配置。这些属性会被转换为界面 HTML 呈现代码中组件 <asp:> 标签的属性。 这对开发人员的好处在于,他们无需记忆每个标签属性的列表或语法。当开发人员尚未完全熟悉 ASP.NET 提供的服务器标签库时,这便是一个显著优势。一旦掌握了该语法,部分开发人员可能会更倾向于直接在页面的呈现代码中编写标签,而跳过图形化设计阶段。 此时便不再需要集成开发环境(IDE),一个简单的文本编辑器即可满足需求。根据您的工作流程,重点将放在组件(使用 IDE)或标签(使用文本编辑器)上。这两个术语是等效的。组件是页面控件代码将要操作的对象。在设计阶段,IDE 允许我们访问其属性。 分配给这些属性的值会立即转换为呈现代码中组件的标签属性。在运行时阶段,页面的控制代码将操作该组件并为其某些属性赋值。呈现代码将通过以下方式生成组件的 HTML 代码:一方面使用设计阶段为对应服务器标签设置的属性,另一方面使用控制代码计算出的组件属性值。

7.2. 示例的执行环境

我们将通过以下主要执行上下文的程序,演示基于服务器组件的 Web 界面设计:

  1. Web 应用程序由单个页面 P 组成,其中包含表单 F,
  2. 客户端将直接向该页面 P 发起首次请求。这将涉及通过浏览器请求页面 P 的 URL。因此,将向该 URL P 发送一个 GET 请求。服务器将返回页面 P 及其所包含的表单 F,
  3. 用户将填写表单并提交,即执行一项操作,促使浏览器将表单 F 提交至服务器。浏览器的 POST 操作始终指向页面 P。服务器将再次返回包含表单 F 的页面 P,其内容可能已被用户的操作修改。
  4. 随后步骤 2 和 3 将重新开始。

这是一个非常具体的执行流程,若脱离此流程,下文讨论的某些概念将不再成立。我们已不再处于 MVC 架构的语境中——在那种架构下,多页面应用程序由我们称为“应用程序控制器”的特定页面进行控制。在这种架构中,表单的 POST 请求是针对控制器而非表单本身。然而,我们将看到,使用服务器端组件构建表单意味着该表单会被提交至其自身。

7.3. Label 组件

7.3.1. 用法

<asp:label> 标签允许您在页面的呈现代码中插入动态文本。因此,它的作用与迄今为止使用的 <%=variable%> 标签并无二致。研究这个首个标签将有助于我们理解服务器标签的工作原理。我们将创建一个包含控件部分 [form1.aspx.vb] 和呈现部分 [form1.aspx] 的页面。目标是显示时间:

Image

本问题已在第 2 章中讨论过,若读者想了解具体实现方式,建议参阅该章节。呈现代码 [form1.aspx] 如下:


<%@ page src="form1.aspx.vb" inherits="form1" AutoEventWireup="false" %>
<HTML>
    <HEAD>
        <title>Webforms</title>
    </HEAD>
    <body>
        <asp:Label Runat="server" ID="lblHeure" />
    </body>
</HTML>

我们将介绍 <asp:label> 标签。在标签库中,必须使用 [runat="server"] 属性。ID 属性用于标识该组件。控制器必须使用此标识符来引用它。控制器代码 [form1.aspx.vb] 如下所示:


Imports System.Web.UI.WebControls
 
Public Class form1
    Inherits System.Web.UI.Page
 
    Protected lblHeure As Label
 
    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        ' saves the current query in request.txt of the page folder
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub
 
    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' sets time in lblHeure
        lblHeure.Text = "Il est " + Date.Now.ToString("T")
    End Sub
 
End Class

控制器必须为类型为 [System.Web.UI.WebControls.Label] 的 [lblHeure] 对象赋值。所有由 <asp:> 标签显示的对象都属于 [System.Web.UI.WebControls] 命名空间。因此,我们可以系统地导入该命名空间:


Imports System.Web.UI.WebControls

[Label] 对象具有多种属性,其中包括 [Text] 属性,该属性表示相应的 <asp:label> 标签将显示的文本。在此,我们将该属性设置为当前时间。 我们将在控制器中的 [Form_Load] 过程内执行此操作,该过程始终会被调用。在 [Form_Init] 过程中(该过程同样始终被调用,但发生在 [Form_Load] 过程之前),我们将客户端的请求存储在应用程序文件夹中的 [request.txt] 文件中。我们将有机会通过分析该文件,来理解使用服务器标签的页面在某些方面的运作机制。

[Label] 对象拥有许多属性、方法和事件。建议读者查阅 [Label] 类的文档以进一步探索这些内容。本书其余部分也将遵循这一原则。对于每个标签,我们仅介绍其中少数几个必需的属性。

7.3.2. 测试

我们将文件(form1.aspx、form1.aspx.vb)放置在 <application-path> 文件夹中,并使用参数 (<application-path>,/form1) 启动 Cassini。随后我们请求 URL [http://localhost/form1/form1.aspx]。结果如下:

Image

浏览器接收到的 HTML 代码如下:

<HTML>
    <HEAD>
        <title>Webforms</title>
    </HEAD>
    <body>
        <span id="lblHeure">Il est 19:39:37</span>
    </body>
</HTML>

我们可以看到服务器标签


        <asp:Label Runat="server" ID="lblHeure" />

已被转换为以下 HTML 代码:

        <span id="lblHeure">Il est 19:39:37</span>

位于 <span> 和 </span> 标签之间的正是 [lblHeure] 对象的 [Text] 属性。客户端发出的请求并存储在 [request.txt] 中的内容如下:

GET /form1/form1.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

一切正常。

7.3.3. 使用 WebMatrix 构建应用程序

我们手动编写了 [form1.aspx] 页面的呈现代码。如果您了解标签,这种方法非常有用。 此时,一个简单的文本编辑器就足以构建用户界面。但初学者通常需要结合图形化设计工具和自动代码生成功能,因为你还不熟悉所需标签的语法。现在我们将使用 WebMatrix 工具构建相同的应用程序。启动 WebMatrix 后,我们选择 [文件/新建文件] 选项:

Image

创建一个名为 [form2.aspx] 的 ASP.NET 页面。确认向导操作后,我们将看到 [form2.aspx] 页面的设计窗口:

Image

Image

请注意,WebMatrix 将页面的控件代码和呈现代码都放在同一个文件中,本例中即为 [form2.aspx]。[全部] 选项卡显示了该文本文件的内容。我们可以看到它并非空文件:

Image

此类工具的缺点在于常会生成冗余代码。本例中便是如此:尽管我们并不打算构建表单,WebMatrix 却生成了 HTML <form> 标签……此外,我们还能发现文档缺少 <title> 标签。我们将立即修正这两个问题,从而得到以下更新后的版本:

Image

我们所说的控制器代码将插入在 <script> 和 </script> 标签之间,以确保控制逻辑与界面呈现这两种代码类型之间至少在视觉上有所区分。我们返回 [设计] 选项卡来设计界面。设计窗口左侧的工具窗口中提供了一系列组件:

Image

该工具窗口提供了两类组件:

  • [WebControls] 组件,对应 <asp:> 标签
  • [HTML Elements] 组件,会转换为标准 HTML 标签。不过,您可以在 HTML 标签的属性中添加 [runat="server"] 属性。在这种情况下,控制器可以通过一个对象访问该 HTML 标签及其属性,该对象的属性与它所代表的 HTML 标签的属性相对应。我们之前将这些标签称为服务器端 HTML 标签。

双击 [WebControls] 列表中的 [Label] 组件。在 [Design] 选项卡中,您将看到以下结果:

Image

在 [All] 选项卡中,代码已变为如下所示:

<%@ Page Language="VB" %>
<html>
<head>
    <title>webforms</title>
</head>
<body>
    <asp:Label id="Label1" runat="server">Label</asp:Label>
</body>
</html>

首先,你会注意到 <script> 标签已经消失。系统生成了一个 <asp:label> 标签。它有一个名称 [Label1] 和一个值 [Label]。让我们回到 [设计] 选项卡来修改这两个值。单击 [Label] 组件一次,在右下角调出其属性窗口:

Image

建议读者查看 [Label] 对象的属性。其中有两项值得关注:

  • Text:这是标签应显示的文本——我们将字符串设置为空(即不显示任何内容)
  • ID:这是其标识符——我们将它设为 lblHeure

现在 [设计] 选项卡显示如下:

Image

而 [All] 的代码变为:

<%@ Page Language="VB" %>
<html>
<head>
    <title>webforms</title>
</head>
<body>
    <asp:Label id="lblHeure" runat="server"></asp:Label>
</body>
</html>

页面的布局已经完成。我们还需要编写负责将时间设置到 [lblHeure] 的 [Text] 属性中的控件代码。我们在 [All] 选项卡中添加以下代码:


<%@ Page Language="VB" %>
 
<script runat="server">
    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' sets time in lblHeure
        lblHeure.Text = "Il est " + Date.Now.ToString("T")
    End Sub
</script>
 
<html>
<head>
    <title>webforms</title>
</head>
<body>
    <asp:Label id="lblHeure" runat="server"></asp:Label>
</body>
</html>

请注意,在控制器代码中,[lblHeure] 对象的声明方式与之前不同:


    Protected lblHeure As New System.Web.UI.WebControls.Label

实际上,呈现层中的所有 <asp:> 服务器组件都在控制器代码中被隐式声明。显式声明它们会导致编译错误,表明该对象已被声明。我们已准备好运行代码。我们选择 [View/Start] 选项或按下 [F5] 快捷键。Cassini 将自动启动并带入以下参数:

Image

我们接受这些值。系统默认浏览器会自动打开并请求 URL [http://localhost/form2.aspx]。我们得到以下结果:

Image

从现在起,我们将主要使用 WebMatrix 工具来辅助编写和测试后续的简短程序。

7.4. 字面量组件

7.4.1. 用法

<asp:literal> 标签允许您将动态文本插入到页面的呈现代码中,这与 <asp:label> 标签类似。其主要属性是 [Text],该属性表示将原样插入到页面 HTML 流中的文本。如果您不打算对要插入到 HTML 流中的文本进行格式化,仅使用此标签即可。 事实上,虽然 [Label] 类允许使用 [BorderColor、BorderWidth、Font 等] 属性进行格式设置,但 [Literal] 类却不具备这些属性。读者可以通过将 [Label] 组件替换为 [Literal] 组件,完整地重现前面的示例。

7.5. Button 组件

7.5.1. 用法

<asp:Button> 标签允许您在表单中插入一个 [Submit] 类型的按钮,它带来了类似于 Windows 应用程序中的事件处理机制。这正是我们在此想要进一步探讨的重点。我们创建以下 [form3.aspx] 页面:

Image

此页面由 WebMatrix 构建,包含三个组件:

编号
名称
类型
属性
角色
1
Button1
Button
text=Button1
提交按钮
2
Button2
按钮
text=Button2
提交按钮
3
lblInfo
标签
text=
信息提示

WebMatrix 为本节生成的代码如下:

<%@ Page Language="VB" %>
<script runat="server">
</script>
<html>
<head>
    <title>asp:button</title>
</head>
<body>
    <form runat="server">
        <p>
            <asp:Button id="Button1" runat="server" Text="Bouton1"></asp:Button>
            <asp:Button id="Button2" runat="server" Text="Bouton2"></asp:Button>
        </p>
        <p>
            <asp:Label id="lblInfo" runat="server"></asp:Label>
        </p>
    </form>
</body>
</html>

我们可以看到页面图形设计中使用的 [Button] 和 [Label] 组件位于 <asp:> 标签内。请注意 <form runat="server"> 标签,它是自动生成的。这是一个服务器端的 HTML 标签——即由一个对象表示的标准 HTML 标签,该对象可由控制器进行操作。<form> 标签的 HTML 代码将根据控制器为该对象赋予的值进行生成。

让我们在代码的控制器部分添加 [Page_Init] 过程,该过程负责处理页面的 [Init] 事件。我们将在此处放置代码,用于将客户端的请求保存到 [request.txt] 文件中。我们需要这样做来理解按钮的工作原理。

<script runat="server">
    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) 
        ' saves the current request in request.txt of the page folder
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub
</script>

请注意,我们在 [Page_Init] 过程声明之后没有包含 [Handles MyBase.Init] 子句。这是因为 [Page] 对象的 [Init] 事件有一个名为 [Page_Init] 的默认处理程序。如果我们使用此处理程序名称,则 [Handles Page.Init] 子句就变得没有必要。不过,包含它也不会导致错误。

7.5.2. 测试

我们通过按 [F5] 键在 WebMatrix 中运行应用程序。我们将看到以下页面:

Image

浏览器接收到的 HTML 代码如下:

<html>
<head>
    <title>asp:button</title>
</head>
<body>
    <form name="_ctl0" method="post" action="form3.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwxNTY0NjIwMjUwOzs+2mcnJczeuvF2PEfvmtv7uiUhWUw=" />

        <p>
            <input type="submit" name="Button1" value="Bouton1" id="Button1" />
            <input type="submit" name="Button2" value="Bouton2" id="Button2" />

        </p>
        <p>
            <span id="lblInfo"></span>
        </p>
    </form>
</body>
</html>

请注意以下几点:

  • <form runat="server"> 标签已转换为 HTML 标签
    <form name="_ctl0" method="post" action="form3.aspx" id="_ctl0">

已设置两个属性:[method="post"] 和 [action="form3.aspx"]。我们可以为这些属性赋予不同的值吗?稍后我们将对此进行说明。 请注意,表单将提交至 URL [form3.aspx]。另外还设置了两个属性 [name, id]。大多数情况下,这些属性会被忽略。但是,如果页面包含浏览器端的 JavaScript 代码,<form> 标签的 [name] 属性就会派上用场。

  • <asp:button> 标签已转换为 HTML [submit] 按钮标签。因此,点击其中任何一个按钮都会触发将 [_ctl10] 表单“提交”至 URL [form3.aspx]。
  • <asp:label> 标签已变为 HTML <span> 标签
  • 系统生成了一个名为 [__VIEWSTATE] 的隐藏字段,其值较为特殊:
<input type="hidden" name="__VIEWSTATE" value="dDwxNTY0NjIwMjUwOzs+2mcnJczeuvF2PEfvmtv7uiUhWUw=" />

该字段以编码形式表示发送给客户端的表单状态。该状态代表了表单中所有组件的值。 由于 [__VIEWSTATE] 是表单的一部分,其值将与其他内容一同提交至服务器。这将使服务器能够识别哪个表单组件的值发生了变化,并在必要时做出相应决策。这些决策将以事件的形式呈现,例如“名称为某某的 TextBox 已更改其值”。

7.5.3. 客户端请求

当 [form3.aspx] 页面在浏览器中加载完成后,让我们再次请求该页面,并分析浏览器为获取该页面而发送的请求。请注意,我们的应用程序将此请求存储在应用程序文件夹内的 [request.txt] 文件中:

GET /form3.aspx HTTP/1.1
Connection: keep-alive
Keep-Alive: 300
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

这是一个标准的 GET 请求。现在,让我们在浏览器中点击页面上的 [Button1] 按钮。看起来似乎什么也没发生。但是,我们知道表单已经提交了。页面的 HTML 代码证实了这一点。这通过 [request.txt] 的新内容得到了证实:

POST /form3.aspx HTTP/1.1
Connection: keep-alive
Keep-Alive: 300
Content-Length: 80
Content-Type: application/x-www-form-urlencoded
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
Referer: http://localhost/form3.aspx
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

__VIEWSTATE=dDwxNTY0NjIwMjUwOzs%2B2mcnJczeuvF2PEfvmtv7uiUhWUw%3D&Button1=Bouton1

第一个 HTTP 头明确表明客户端向 URL [/form3.aspx] 发送了 POST 请求。最后一行显示了提交的值:

  • 隐藏字段 __VIEWSTATE 的值
  • 被点击的按钮的值

如果我们点击 [Button2],浏览器提交的值如下:

__VIEWSTATE=dDwxNTY0NjIwMjUwOzs%2B2mcnJczeuvF2PEfvmtv7uiUhWUw%3D&Button2=Bouton2

隐藏字段的值始终相同,但被提交的是 [Button2] 的值。因此,服务器可以确定使用了哪个按钮。它将利用这一点触发一个事件,该事件可在页面加载完成后由页面进行处理。

7.5.4. 处理按钮对象的 Click 事件

让我们回顾一下 .aspx 页面的工作原理。它是一个从 [Page] 类派生的对象。我们将这个派生类称为 [aPage]。当服务器收到对该页面的请求时,会通过 new unePage(...) 操作实例化一个 [unePage] 类型的对象。随后,服务器会按顺序生成两个名为 [Init] 和 [Load] 的事件。 [unePage] 对象可以通过提供 [Page_Init] 和 [Page_Load] 事件处理程序来处理这些事件。其他事件将在后续生成,我们稍后会有机会重新探讨它们。如果客户端的请求是 POST 请求,服务器将为触发此 POST 的按钮生成 [Click] 事件。如果 [unePage] 类为此事件提供了处理程序,该处理程序将会被调用。 让我们通过 WebMatrix 来探索这一机制。在 [form3.aspx] 的 [设计] 选项卡中,双击 [Button1] 按钮。随后系统会自动跳转到 [代码] 选项卡,进入名为 [Button1_Click] 的过程主体。为了更好地理解,让我们转到 [全部] 选项卡并查看完整的代码。已进行以下更改:

<%@ Page Language="VB" %>
<script runat="server">

...
    Sub Button1_Click(sender As Object, e As EventArgs)

    End Sub

</script>
<html>
...
<body>
    <form runat="server">
...
            <asp:Button id="Button1" onclick="Button1_Click" runat="server" Text="Bouton1"></asp:Button>
    </form>
</body>
</html>

<asp:Button> 标签中为 [Button1] 添加了一个新属性 [onclick="Button1_Click"]。该属性指定了负责处理 [Button1] 对象 [Click] 事件的存储过程,在本例中即 [Button1_Click] 存储过程。接下来只需编写该存储过程:

    Sub Button1_Click(sender As Object, e As EventArgs)
        ' click on button 1
        lblInfo.Text="Vous avez cliqué sur [Bouton1]"
    End Sub

该过程会在 [lblInfo] 标签中显示一条信息。我们以同样的方式处理 [Button2] 按钮,从而得到以下新页面 [form3.aspx]:

<%@ Page Language="VB" %>
<script runat="server">

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs)
        ' sauve la requte courante dans request.txt du dossier de la page
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub

    Sub Button1_Click(sender As Object, e As EventArgs)
        ' clic sur bouton 1
        lblInfo.Text="Vous avez cliqué sur [Bouton1]"
    End Sub

    Sub Button2_Click(sender As Object, e As EventArgs)
        ' clic sur bouton 2
        lblInfo.Text="Vous avez cliqué sur [Bouton2]"
    End Sub

</script>
<html>
<head>
    <title>asp:button</title>
</head>
<body>
    <form runat="server">
        <p>
            <asp:Button id="Button1" onclick="Button1_Click" runat="server" Text="Bouton1"></asp:Button>
            <asp:Button id="Button2" onclick="Button2_Click" runat="server" Text="Bouton2" BorderStyle="None"></asp:Button>
        </p>
        <p>
            <asp:Label id="lblInfo" runat="server"></asp:Label>
        </p>
    </form>
</body>
</html>

我们按下 [F5] 运行代码,得到以下页面:

Image

如果查看收到的 HTML 代码,我们会发现它与该页面的上一版本相比没有变化:

<html>
<head>
    <title>asp:button</title>
</head>
<body>
    <form name="_ctl0" method="post" action="form3.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwxNTY0NjIwMjUwOzs+2mcnJczeuvF2PEfvmtv7uiUhWUw=" />

        <p>
            <input type="submit" name="Button1" value="Bouton1" id="Button1" />
            <input type="submit" name="Button2" value="Bouton2" id="Button2" />
        </p>
        <p>
            <span id="lblInfo"></span>
        </p>
    </form>
</body>
</html>

如果点击 [Button1],我们会得到以下响应:

Image

此响应收到的 HTML 代码如下:

<html>
<head>
    <title>asp:button</title>
</head>
<body>
    <form name="_ctl0" method="post" action="form3.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwxNTY0NjIwMjUwO3Q8O2w8aTwxPjs+O2w8dDw7bDxpPDU+Oz47bDx0PHA8cDxsPFRleHQ7PjtsPFZvdXMgYXZleiBjbGlxdcOpIHN1ciBbQm91dG9uMV07Pj47Pjs7Pjs+Pjs+Pjs+4oO98Vd244kj0lPMXReWOwJ1WW0=" />

        <p>
            <input type="submit" name="Button1" value="Bouton1" id="Button1" />
            <input type="submit" name="Button2" value="Bouton2" id="Button2" />
        </p>
        <p>
            <span id="lblInfo">Vous avez cliqué sur [Bouton1]</span>
        </p>
    </form>
</body>
</html>

我们可以看到隐藏字段 [__VIEWSTATE] 的值发生了变化。这反映了 [lblInfo] 控件值的改变。

7.5.5. ASP.NET 应用程序生命周期中的事件

ASP.NET 文档列出了应用程序生命周期中由服务器生成的事件:

  • 当应用程序收到第一个请求时,应用程序的 [HttpApplication] 对象会触发 [Start] 事件。该事件可通过应用程序 [global.asax] 文件中的 [Application_Start] 过程进行处理。

接下来,将有一系列事件在每次接收请求时重复发生:

  • 如果请求未发送会话令牌,则会启动一个新会话,并触发与该请求关联的 [Session] 对象上的 [Start] 事件。该事件可通过应用程序 [global.asax] 文件中的 [Session_Start] 过程进行处理。
  • 服务器在 [HttpApplication] 对象上生成 [BeginRequest] 事件。该事件可由应用程序 [global.asax] 文件中的 [Application_BeginRequest] 过程处理。
  • 服务器加载请求所指向的页面。它将实例化一个 [Page] 对象,并在此对象上生成两个事件:[Init] 和 [Load]。这两个事件可由页面的 [Page_Init] 和 [Page_Load] 过程进行处理。
  • 根据接收到的表单提交值,服务器将生成其他事件:对于值发生变化的 [TextBox] 控件,生成 [TextChanged] 事件;对于值发生变化的单选按钮,生成 [CheckedChanged] 事件;对于所选项目发生变化的列表,生成 [SelectedIndexChanged] 事件,等等。在介绍后续的各个服务器控件时,我们将有机会提及它们的主要事件。 对象 O 上的每个事件 E 均可由名为 O_E 的过程进行处理。
  • 上述事件的处理顺序无法保证。因此,处理程序不得对该顺序做任何假设。不过,我们可以确定,触发 POST 请求的按钮的 [Click] 事件将最后被处理。
  • 页面准备就绪后,服务器将把它发送给客户端。在此之前,它会生成 [PreRender] 事件,该事件可由页面的 [Page_PreRender] 过程处理。
  • 一旦 HTML 响应发送给客户端,页面将从内存中卸载。此时将生成两个事件:[Unload] 和 [Disposed]。页面可以利用这些事件来释放资源。

应用程序还可以在客户端请求之外接收事件:

  • 应用程序 [Session] 对象上的 [End] 事件发生于会话结束时。这可能是由页面代码的显式请求引发的,也可能是因为会话的生命周期已到期。[global.asax] 文件中的 [Session_End] 过程负责处理此事件。它通常会释放 [Session_Start] 中获取的资源。
  • 应用程序 [HttpApplication] 对象上的 [End] 事件在应用程序终止时触发。例如,当 Web 服务器关闭时就会发生这种情况。[global.asax] 文件中的 [Application_End] 过程处理此事件。它通常用于释放 [Application_Start] 中获取的资源。

应注意以下几点:

  • 上述事件模型依赖于标准的客户端-服务器 HTTP 交互。这在检查交换的 HTTP 头部时显而易见。
  • 上述事件的处理始终在服务器端进行。当然,点击按钮可以由服务器端的 JavaScript 脚本处理。但这并非服务器事件,且我们所涉及的技术与 ASP.NET 无关。

事件何时被处理?是在服务器端(与服务器组件相关的事件),还是由 JavaScript 脚本在浏览器端处理?

让我们以下拉列表为例。当用户更改其中选中的项目时,该事件(选中项的变更)可能被处理,也可能不被处理;如果被处理,其处理时间也可能各不相同。

  • 若需立即处理,有两种解决方案:
    • 可以通过 JavaScript 脚本由浏览器处理。这种情况下服务器不参与处理。要实现这一点,必须能够利用页面上的现有值来重建页面。
    • 由服务器进行处理。对此,仅有一种解决方案:必须将表单提交至服务器进行处理。因此我们需要执行 [submit] 操作。我们将看到,在此情况下,需使用名为 [DropDownList] 的服务器组件,并将其 [AutoPostBack] 属性设置为 [true]。这意味着,如果下拉列表中的选中项发生变化,表单必须立即提交至服务器。 此时,服务器会为 [DropDownList] 对象生成 HTML 代码,该代码关联了一个 JavaScript 函数,该函数负责在“选中项发生变化”事件发生时立即执行 [submit] 操作。此 [submit] 操作将表单提交至服务器,并且提交内容中会包含隐藏字段,以表明此次 [post] 是由下拉列表中的选项变化触发的。 随后,服务器将触发 [SelectedIndexChanged] 事件,页面可对此事件进行处理。
    • 如果您希望处理该事件但不想立即处理,请将 [DropDownList] 服务器组件的 [AutoPostBack] 属性设置为 [false]。在这种情况下,服务器会为 [DropDownList] 对象生成 <select> 列表的标准 HTML 代码,但不包含相关的 JavaScript 函数。 当用户在下拉列表中更改选择时,不会发生任何变化。但是,当用户通过 [submit] 按钮提交表单时,服务器将能够识别到已发生选择更改。 我们确实看到,发送到服务器的表单中包含一个隐藏字段 [__VIEWSTATE],它以编码形式表示提交表单中所有元素的状态。当服务器收到客户端发布的新表单时,它可以验证下拉列表中的选定项是否已更改。如果是,它将触发 [SelectedIndexChanged] 事件,页面随后可以处理该事件。 为了将此机制与前一种机制区分开来,一些作者指出,当“选择更改”事件在浏览器中发生时,它会被“缓存”。只有当浏览器将表单提交给服务器时(通常是在点击 [submit] 按钮之后),服务器才会处理该事件。
    • 最后,若您不希望处理该事件,请将 [DropDownList] 服务器组件的 [AutoPostBack] 属性设置为 [false],并避免为其 [SelectedIndexChanged] 事件编写处理程序。

一旦理解了事件处理机制,开发人员就不会再像设计 Windows 应用程序那样设计 Web 应用程序了。事实上,虽然 Windows 应用程序中组合框的选择更改可以用来立即更新包含它的表单的外观,但在 Web 应用程序中,如果处理此事件涉及向服务器“提交”表单——从而导致客户端与服务器之间的往返通信——开发人员往往会犹豫是否立即处理该事件。 这就是为什么服务器端组件的 [AutoPostBack] 属性默认设置为 [false]。此外,[AutoPostBack] 机制依赖于 Web 服务器在发送给客户端的表单中自动生成的 JavaScript 脚本,只有在确定客户端浏览器已启用 JavaScript 脚本执行时才能使用。因此,表单通常按以下方式构建:

  • 表单的服务器端组件将其 [AutoPostBack] 属性设置为 [false]
  • 表单包含一个或多个负责执行 [POST] 操作的按钮
  • 在页面的控制器代码中,仅为需要处理的事件编写处理程序,通常是某个按钮的 [Click] 事件。

7.6. TextBox 组件

7.6.1. 用法

<asp:TextBox> 标签允许您在页面的呈现代码中插入一个输入字段。我们创建一个页面 [form4.aspx] 以实现以下布局:

Image

1234此页面使用 WebMatrix 构建,包含以下组件:

编号
姓名
类型
属性
角色
1
TextBox1
文本框
AutoPostback=true
Text=
输入字段
2
文本框2
文本框
自动回发=false
Text=
输入字段
3
lblInfo1
标签
text=
关于 [TextBox1] 内容的提示信息
3
lblInfo2
标签
text=
关于 [TextBox2] 内容的信息提示

WebMatrix 为该部分生成的代码如下:

<%@ Page Language="VB" %>
<script runat="server">
</script>
<html>
<head>
    <title>asp:textbox</title>
</head>
<body>
    <form runat="server">
        <p>
            Texte 1 :
            <asp:TextBox id="TextBox1" runat="server" AutoPostBack="True"></asp:TextBox>
        </p>
        <p>
            Texte 2 :
            <asp:TextBox id="TextBox2" runat="server"></asp:TextBox>
        </p>
        <p>
            <asp:Label id="lblInfo1" runat="server"></asp:Label>
        </p>
        <p>
            <asp:Label id="lblInfo2" runat="server"></asp:Label>
        </p>
    </form>
</body>
</html>

在 [设计] 选项卡上,双击 [TextBox1] 控件。随后将生成该对象的 [TextChanged] 事件处理程序的骨架(在 [全部] 选项卡上):

<%@ Page Language="VB" %>
<script runat="server">

    Sub TextBox1_TextChanged(sender As Object, e As EventArgs)
    End Sub
</script>
<html>
...
<body>
...
            <asp:TextBox id="TextBox1" runat="server" AutoPostBack="True" OnTextChanged="TextBox1_TextChanged"></asp:TextBox>
        </p>
....
    </form>
</body>
</html>

已在 <asp:TextBox id="TextBox1"> 标签中添加了 [OnTextChanged="TextBox1_TextChanged"] 属性,用于指定 [TextBox1] 上 [TextChanged] 事件的处理程序。现在,我们将编写 [TextBox1_Changed] 过程。

    Sub TextBox1_TextChanged(sender As Object, e As EventArgs)
      ' text change
      lblInfo1.text=Date.now.Tostring("T") + ": evt [TextChanged] sur [TextBox1]. Texte 1=["+textbox1.Text+"]"
    End Sub

在该过程内,我们在 [lblInfo1] 标签中写入一条消息,以指示该事件并显示 [TextBox1] 的内容。对于 [TextBox2],我们也采用同样的处理方式。此外,我们还加入了时间信息,以便更好地追踪事件处理过程。[form4.aspx] 的最终代码如下:

<%@ Page Language="VB" %>
<script runat="server">

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs)
        ' sauve la requte courante dans request.txt du dossier de la page
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub

    Sub TextBox1_TextChanged(sender As Object, e As EventArgs)
      ' changement de texte
      lblInfo1.text=Date.now.Tostring("T") + ": evt [TextChanged] sur [TextBox1]. Texte 1=["+textbox1.Text+"]"
    End Sub

    Sub TextBox2_TextChanged(sender As Object, e As EventArgs)
      ' changement de texte
      lblInfo2.text=Date.now.Tostring("T") + ": evt [TextChanged] sur [TextBox2]. Texte 2=["+textbox2.Text+"]"
    End Sub

</script>
<html>
<head>
    <title>asp:textbox</title>
</head>
<body>
    <form runat="server">
        <p>
            Texte 1 :
            <asp:TextBox id="TextBox1" runat="server" AutoPostBack="True" OnTextChanged="TextBox1_TextChanged"></asp:TextBox>
        </p>
        <p>
            Texte 2 :
            <asp:TextBox id="TextBox2" runat="server" OnTextChanged="TextBox2_TextChanged"></asp:TextBox>
        </p>
        <p>
            <asp:Label id="lblInfo1" runat="server"></asp:Label>
        </p>
        <p>
            <asp:Label id="lblInfo2" runat="server"></asp:Label>
        </p>
    </form>
</body>
</html>

我们添加了 [Page_Init] 过程来存储客户端的请求,与前面的示例一样。

7.6.2. 测试

我们通过按 [F5] 键在 WebMatrix 中启动应用程序。随后将显示以下页面:

Image

浏览器接收到的 HTML 代码如下:

<html>
<head>
    <title>asp:textbox</title>
</head>
<body>
    <form name="_ctl0" method="post" action="form4.aspx" id="_ctl0">
<input type="hidden" name="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" value="dDwtMTY4MDc0MTUxOTs7PoqpeSYSCX7lCiWZvw5p7u+/OrTD" />

<script language="javascript">
<!--
    function __doPostBack(eventTarget, eventArgument) {
        var theform = document._ctl0;
        theform.__EVENTTARGET.value = eventTarget;
        theform.__EVENTARGUMENT.value = eventArgument;
        theform.submit();
    }
// -->
</script>

        <p>
            Texte 1 :
            <input name="TextBox1" type="text" id="TextBox1" onchange="__doPostBack('TextBox1','')" language="javascript" />
        </p>
        <p>
            Texte 2 :
            <input name="TextBox2" type="text" id="TextBox2" />
        </p>
        <p>
            <span id="lblInfo1"></span>

        </p>
        <p>
            <span id="lblInfo2"></span>
        </p>
    </form>
</body>
</html>

此代码中包含许多由服务器自动生成的元素。请注意以下几点:

  • 这里有三个隐藏字段:我们已经见过的 [__VIEWSTATE]、[__EventTarget] 以及 [__EventArgument]。后两个字段用于处理输入字段 [TextBox1] 上的浏览器“change”事件
  • <asp:textbox> 服务器标记生成了与输入字段对应的 HTML 标签 <input type="text" ...>
  • 服务器标记 <asp:textbox id="TextBox1" AutoPostBack="true" ...> 生成了一个带有 [onchange="__doPostBack('TextBox1','')"] 属性的 <input type="text" ...> 标签。 该属性指定,如果 [TextBox1] 的内容发生变化,则必须执行 JavaScript 函数 [_doPostBack(...)]。具体如下:
<script language="javascript">
<!--
    function __doPostBack(eventTarget, eventArgument) {
        var theform = document._ctl0;
        theform.__EVENTTARGET.value = eventTarget;
        theform.__EVENTARGUMENT.value = eventArgument;
        theform.submit();
    }
// -->
</script>

上述函数的作用是什么?它为两个隐藏字段 [__EventTarget] 和 [__EventArgument] 分别赋值,然后提交表单。因此,表单会被发送至服务器。这就是 [AutoPostBack] 效果。浏览器的“change”事件触发了代码“__doPostBack('TextBox1','')”的执行。 我们可以推断,在提交的表单中,隐藏字段 [__EventTarget] 的值为 'TextBox1',而隐藏字段 [__EventArgument] 的值为 ''。这使得服务器能够识别触发 POST 请求的组件。

  • 服务器标记 <asp:textbox id="TextBox2"...> 生成了一个标准的 <input type="text" ...> 标记,因为其 [AutoPostBack] 属性未设置为 [true]。
  • <form> 标签表示该表单将提交至 [form4.aspx]:
    <form name="_ctl0" method="post" action="form4.aspx" id="_ctl0">

让我们进行首次测试。在第一个输入框中输入一些文本:

Image

然后将光标移至第二个输入框。随即,新页面便会显示出来:

Image

发生了什么?当光标离开第一个输入框时,浏览器检查了其内容是否发生变化。内容确实发生了变化。因此,在浏览器端,这触发了 HTML 字段 [TextBox1] 的 [change] 事件。 随后我们看到一个 JavaScript 函数被执行,并将表单提交至 [form4.aspx]。该页面随后由服务器重新加载。表单提交的值使服务器识别到服务器端 [TextBox1] 标签的内容已发生变化。因此,服务器端执行了 [TextBox1_Changed] 过程,并在 [lblInfo1] 标签中显示了一条消息。 该过程完成后,[form4.aspx] 被发回浏览器。这就是为什么现在 [lblInfo1] 中有文本的原因。话虽如此,[TextBox1] 输入字段中仍有内容这一现象可能令人惊讶。事实上,没有任何服务器端过程为该字段赋值。这是 ASP.NET Web 表单的一般机制:服务器会以接收时的状态返回表单。 为此,服务器会将客户端提交的值重新赋给各个组件。对于某些组件,客户端并未提交任何值。例如,<asp:label> 组件就是这种情况,它们会被渲染为 HTML <span> 标签。请记住,表单有一个隐藏字段 [__VIEWSTATE],它代表表单在发送给客户端时的状态。 该状态是表单所有组件状态的总和,包括任何 <asp:label> 组件。由于隐藏字段 [__VIEWSTATE] 由客户端浏览器提交,服务器能够恢复表单所有组件的先前状态。剩下的工作只是修改那些值已被 POST 请求更改的组件。

现在让我们查看 [request.txt] 文件中浏览器发出的请求:

POST /form4.aspx HTTP/1.1
Connection: keep-alive
Keep-Alive: 300
Content-Length: 137
Content-Type: application/x-www-form-urlencoded
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
Referer: http://localhost/form4.aspx
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

__EVENTTARGET=TextBox1&__EVENTARGUMENT=&__VIEWSTATE=dDwtMTY4MDc0MTUxOTs7PoqpeSYSCX7lCiWZvw5p7u%2B%2FOrTD&TextBox1=premier+texte&TextBox2=

我们可以清楚地看到POST请求以及正在发送的参数。让我们回到浏览器,在第二个输入框中输入一些文本:

Image

现在回到第一个输入框输入新文本:这次没有任何反应。为什么?因为 [TextBox2] 输入框的 [AutoPostBack] 属性未设置为 [true],因此为其生成的 <input type="text"...> 标签不会处理 [Change] 事件,如下所示的 HTML 代码所示:

            <input name="TextBox2" type="text" id="TextBox2" />

因此,当你离开输入框 #2 时不会触发任何事件。现在让我们在输入框 #1 中输入新文本:

Image

现在离开输入字段 #1。该字段的 [Change] 事件立即被检测到,表单被提交至服务器,并返回以下页面:

Image

发生了什么?首先,浏览器提交了表单。这体现在存储于 [request.txt] 中的客户端请求中:

POST /form4.aspx HTTP/1.1
....
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

__EVENTTARGET=TextBox1&__EVENTARGUMENT=&__VIEWSTATE=dDwtMTY4MDc0MTUxOTt0PDtsPGk8MT47PjtsPHQ8O2w8aTwxPjtpPDU%2BOz47bDx0PHA8cDxsPFRleHQ7PjtsPHByZW1pZXIgdGV4dGU7Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPDE4OjQyOjI5OiBldnQgW1RleHRDaGFuZ2VkXSBzdXIgW1RleHRCb3gxXS4gVGV4dGUgMT1bcHJlbWllciB0ZXh0ZV07Pj47Pjs7Pjs%2BPjs%2BPjs%2BxLOermpUUUz5rTAa%2FFsjda6lVmo%3D&TextBox1=troisi%C3%A8me+texte&TextBox2=second+texte

服务器首先使用客户端发送的隐藏字段 [__VIEWSTATE] 将组件恢复到之前的值。然后,利用提交的字段 [TextBox1] 和 [TextBox2],将提交的值赋给组件 [TextBox1] 和 [TextBox2]。正是通过这种机制,客户端才能获取到表单提交时的原始状态。 随后,服务器再次利用提交的字段 [__VIEWSTATE]、[TextBox1] 和 [TextBox2],检测到输入字段 [TextBox1] 和 [TextBox2] 的值已发生变化。因此,它会触发这两个对象的 [TextChanged] 事件。 此时将执行 [TextBox1_TextChanged] 和 [TextBox2_TextChanged] 过程,标签 [labelInfo1] 和 [labelInfo2] 将获得新值。随后,修改后的 [form4.aspx] 页面会被发回客户端。

现在让我们再次修改第 1 个输入字段:

Image

当我们将光标移出字段 1 时,浏览器中会触发 [Change] 事件。随后,之前已解释过的事件序列(从浏览器向服务器提交请求、……、发送服务器的响应)便会发生。我们得到以下响应:

Image

由于每条消息都显示了时间戳,我们可以看到服务器上仅执行了 [TextBox1_Changed] 过程。由于 [TextBox2] 的值未发生变化,因此 [TextBox2_TextChanged] 过程未被执行。最后,让我们在字段 #2 中输入新文本:

Image

然后将光标移至第 1 号字段,再移回第 2 号字段。页面没有变化。为什么?因为我们没有更改第 1 号字段的值,离开该字段时不会触发浏览器的 [Change] 事件。因此,表单不会提交至服务器。 因此,页面上没有任何变化。正是 [lblInfo2] 的内容没有改变这一事实,向我们表明没有发生 POST 操作。如果发生了 POST 操作,服务器会检测到 [TextBox2] 的内容已发生变化,并应将其反映在 [lblInfo2] 中。

此示例的启示是:将 [TextBox] 的 [AutoPostBack] 属性设置为 [true] 毫无意义。这在大多数情况下会导致不必要的客户端-服务器往返通信。

7.6.3. __VIEWSTATE 字段的作用

我们已经看到,服务器会在生成的表单中系统地放置一个名为 __VIEWSTATE 的隐藏字段。我们注意到,该字段代表表单的状态,如果将此隐藏字段发回服务器,服务器便能重建表单的先前值。 表单的状态是其所有组件状态的总和。每个组件都有一个 [EnableViewState] 属性,其布尔值用于指示该组件的状态是否应被放入隐藏的 [__VIEWSTATE] 字段中。默认情况下,该属性设置为 [true],这意味着表单中所有组件的状态都会被放入 [__VIEWSTATE] 中。有时,这并非我们所期望的。

让我们进行一些测试,以更好地理解 [EnableViewState] 属性的作用。我们将两个输入字段的该属性均设置为 [false]:

...
            <asp:TextBox id="TextBox1" runat="server" OnTextChanged="TextBox1_TextChanged" AutoPostBack="True" EnableViewState="False"></asp:TextBox>
...
            <asp:TextBox id="TextBox2" runat="server" OnTextChanged="TextBox2_TextChanged" EnableViewState="False"></asp:TextBox>
...

现在让我们运行应用程序,在字段 #1 中输入一些文本,然后切换到字段 #2。随后会向服务器发送一个 POST 请求,并收到以下响应:

Image

在字段 #2 中输入一些文本,修改字段 #1 中的文本,然后返回字段 #2(按此顺序)。一个新的 POST 请求将发送至服务器,我们收到以下响应:

Image

目前,一切与之前相同。现在,让我们修改字段 #1 的内容,然后转到字段 #2。一个新的 POST 请求被发送。服务器的响应如下:

Image

这次出现了变化。由于 [lblInfo2] 的值被修改,服务器检测到了字段 #2 的 [TextChanged] 事件。然而,实际上并没有发生任何变化。这是由于 [TextBox2] 的 [EnableViewState=false] 属性所致。这会导致服务器不会将 [TextBox2] 的先前状态存储在表单的 [__VIEWSTATE] 字段中。 这相当于将空字符串赋值为先前状态。当由 [TextBox1] 内容变化触发的 POST 请求发生时,服务器将 [TextBox2] 的当前值 [two] 与其先前值(空字符串)进行了比较。服务器据此判定 [TextBox2] 的值已发生变化,并为 [TextBox2] 生成了 [TextChanged] 事件。 我们可以通过在 [TextBox2] 中输入空字符串来验证这种行为。根据刚才的解释,服务器本不应为 [TextBox2] 生成 [TextChanged] 事件。让我们试一试:

Image

结果正是如此。[lblInfo2] 显示的时间和内容表明,[TextBox2_TextChanged] 过程并未被执行。基于此,让我们检查这四个表单组件的 [EnableViewState] 属性:

TextBox1
我们需要保留该控件的状态,以便服务器知道它是否发生了变化
TextBox2
同上
lblInfo1
我们不希望保留此组件的状态。我们希望每次新的 POST 请求时都重新计算文本。如果未重新计算,则该文本必须为空。所有这些效果均可通过 [EnableViewState=false] 实现
lblInfo2 
同上

我们的主页现在看起来像这样:

...
            <asp:TextBox id="TextBox1" runat="server" OnTextChanged="TextBox1_TextChanged" AutoPostBack="True"></asp:TextBox>
...
            <asp:TextBox id="TextBox2" runat="server" OnTextChanged="TextBox2_TextChanged"></asp:TextBox>
....
            <asp:Label id="lblInfo1" runat="server" enableviewstate="False"></asp:Label>
...
            <asp:Label id="lblInfo2" runat="server" enableviewstate="False"></asp:Label>
...

让我们运行与之前相同的一系列测试。在之前我们得到了如下屏幕

版本 1

Image

,现在得到:

版本 2

Image

在此步骤中,我们更改了字段 1 的内容,但未更改字段 2 的内容,从而确保 [TextBox2_TextChanged] 过程未在服务器端执行,这意味着 [lblInfo2] 字段未接收新值。因此,它显示的是其先前值。在版本 1 [EnableViewState=true] 中,该先前值即为输入的值。 在版本 2 [EnableViewState=false] 中,该先前值是空字符串。

有时没有必要保留组件的先前状态。与其为每个组件设置 [EnableViewState=false],不如指定页面不应保持其状态。这可以在呈现代码的 [Page] 指令中完成:

<%@ Page Language="VB" EnableViewState="False" %>

在这种情况下,无论组件的 [EnableViewState] 属性值为何,其状态都不会存储在隐藏字段 [__VIEWSTATE] 中。此时,所有组件的行为都将视其先前状态为空字符串。

现在,让我们使用 [curl] 客户端来演示其他机制。首先,我们请求 URL [http://localhost/form4.aspx]:

dos>curl --include --url http://localhost/form4.aspx

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Sun, 04 Apr 2004 17:51:14 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 1077
Connection: Close


<html>
<head>
    <title>asp:textbox</title>
</head>
<body>
    <form name="_ctl0" method="post" action="form4.aspx" id="_ctl0">
<input type="hidden" name="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" value="dDwtMTY4MDc0MTUxOTs7PoqpeSYSCX7lCiWZvw5p7u+/OrTD" />

<script language="javascript">
<!--
    function __doPostBack(eventTarget, eventArgument) {
        var theform = document._ctl0;
        theform.__EVENTTARGET.value = eventTarget;
        theform.__EVENTARGUMENT.value = eventArgument;
        theform.submit();
    }
// -->
</script>

        <p>
            Texte 1 :
            <input name="TextBox1" type="text" id="TextBox1" onchange="__doPostBack('TextBox1','')" language="javascript" />
        </p>
        <p>
            Texte 2 :
            <input name="TextBox2" type="text" id="TextBox2" />
        </p>
        <p>
            <span id="lblInfo1"></span>
        </p>
        <p>
            <span id="lblInfo2"></span>
        </p>
    </form>
</body>
</html>

我们从服务器接收到了 [form4.aspx] 的 HTML 代码。它与浏览器接收到的内容并无二致。请回想一下浏览器提交表单时发出的请求:

POST /form4.aspx HTTP/1.1
Connection: keep-alive
...
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

__EVENTTARGET=TextBox1&__EVENTARGUMENT=&__VIEWSTATE=dDwtMTY4MDc0MTUxOTs7PoqpeSYSCX7lCiWZvw5p7u%2B%2FOrTD&TextBox1=premier+texte&TextBox2=

现在执行相同的 POST 请求,但不发送 [__VIEWSTATE] 字段:

dos>curl --include --url http://localhost/form4.aspx --data __EVENTTARGET=TextBox1 --data __EVENTARGUMENT= --data TextBox1=premier+texte --data TextBox2=

...................
        <p>
            Texte 1 :
            <input name="TextBox1" type="text" value="premier texte" id="TextBox1" onchange="__doPostBack('TextBox1','')" language="javascript" />
        </p>
        <p>
            Texte 2 :
            <input name="TextBox2" type="text" id="TextBox2" />
        </p>
        <p>
            <span id="lblInfo1">19:57:48: evt [TextChanged] sur [TextBox1]. Texte 1=[premier texte]</span>
        </p>
        <p>
            <span id="lblInfo2"></span>
        </p>
..............

请注意以下几点:

  • 服务器检测到了 [TextBox1] 的 [TextChanged] 事件,因为它生成了文本 [lblInfo1]。即使没有 [__VIEWSTATE],也不会阻止此操作。在没有 [__VIEWSTATE] 的情况下,它会假设输入字段的先前值为空字符串。
  • 它能够将 [TextBox1] 提交的文本恢复到 [TextBox1] 标签的 [value] 属性中,从而使 [TextBox1] 字段重新显示为输入的值。为此,它不需要 [__VIEWSTATE],只需 [TextBox1] 提交的值即可

现在,让我们在不做任何更改的情况下再次发出相同的请求。我们得到了以下新的响应:

dos>curl --include --url http://localhost/form4.aspx --data __EVENTTARGET=TextBox1 --data __EVENTARGUMENT= --data TextBox1=premier+texte --data TextBox2=


        <p>
            Texte 1 :
            <input name="TextBox1" type="text" value="premier texte" id="TextBox1" onchange="__doPostBack('TextBox1','')" language="javascript" />
        </p>
        <p>
            Texte 2 :
            <input name="TextBox2" type="text" id="TextBox2" />
        </p>
        <p>
            <span id="lblInfo1">20:05:47: evt [TextChanged] sur [TextBox1]. Texte 1=[premier texte]</span>
        </p>
        <p>
            <span id="lblInfo2"></span>
        </p>

在没有 [__VIEWSTATE] 的情况下,服务器无法识别 [TextBox1] 字段的值并未发生变化。因此,它会将之前的值视为空字符串。结果,它在此处为 [TextBox1] 触发了 [TextChanged] 事件。让我们再次运行相同的请求,这次让 [TextBox1] 字段为空,而 [TextBox2] 不为空:

dos>curl --include --url http://localhost/form4.aspx --data __EVENTTARGET=TextBox1 --data __EVENTARGUMENT= --data TextBox2=second+texte --data TextBox1=
......
        <p>
            Texte 1 :
            <input name="TextBox1" type="text" id="TextBox1" onchange="__doPostBack('TextBox1','')" language="javascript" />
        </p>
        <p>
            Texte 2 :
            <input name="TextBox2" type="text" value="second texte" id="TextBox2" />
        </p>
        <p>
            <span id="lblInfo1"></span>
        </p>
        <p>
            <span id="lblInfo2">20:11:54: evt [TextChanged] sur [TextBox2]. Texte 2=[second texte]</span>
        </p>
......

在没有 [__VIEWSTATE] 的情况下,[TextBox1] 的先前值被视为空字符串。由于 [TextBox1] 的提交值也是空字符串,因此 [TextBox1] 上的 [TextChanged] 事件未被触发。[TextBox1_TextChanged] 过程未被执行,因此 [lblInfo1] 字段未收到新值。 我们知道,在这种情况下,组件会保留其旧值。然而,此处情况并非如此;[lblInfo1] 已丢失其先前值。这是因为该值是从 [__VIEWSTATE] 中检索的。由于该字段缺失,因此将空字符串赋值给 [lblInfo1]。对于 [TextBox2],服务器将其提交值 [second text] 与其先前值进行了比较。 在缺少 [__VIEWSTATE] 的情况下,该先前值等于空字符串。由于 [TextBox2] 的提交值不同于空字符串,因此触发了 [TextBox2] 上的 [TextChanged] 事件。执行了 [TextBox2_TextChanged] 过程,[lblInfo2] 字段获得了新值。

有人可能会质疑 [__EVENTTARGET] 和 [__EVENTARGUMENT] 参数是否真的有用。如果不发送这些参数,服务器将无法知道是哪一个事件触发了 [submit]。让我们试一试:

dos>curl --include --url http://localhost/form4.aspx --data TextBox2=second+texte --data TextBox1=premier+texte
..............................
        <p>
            Texte 1 :
            <input name="TextBox1" type="text" id="TextBox1" onchange="__doPostBack('TextBox1','')" language="javascript" />
        </p>
        <p>
            Texte 2 :
            <input name="TextBox2" type="text" id="TextBox2" />
        </p>
        <p>
            <span id="lblInfo1"></span>
        </p>
        <p>
            <span id="lblInfo2"></span>
        </p>
    </form>
.....................

我们可以看到,没有处理任何 [TextChanged] 事件。此外,提交字段 [TextBox1] 和 [TextBox2] 未能获取其提交的值。实际上,一切表现得就像是发出了一个 GET 请求。如果将 [__EVENTTARGET] 字段包含在提交字段中,即使它没有值,一切也会恢复正常:

dos>curl --include --url http://localhost/form4.aspx --data __EVENTTARGET= --data TextBox2=second+texte --data TextBox1=premier+texte
.......
        <p>
            Texte 1 :
            <input name="TextBox1" type="text" value="premier texte" id="TextBox1" onchange="__doPostBack('TextBox1','')" language="javascript" />
        </p>
        <p>
            Texte 2 :
            <input name="TextBox2" type="text" value="second texte" id="TextBox2" />
        </p>
        <p>
            <span id="lblInfo1">20:34:14: evt [TextChanged] sur [TextBox1]. Texte 1=[premier texte]</span>
        </p>
        <p>
            <span id="lblInfo2">20:34:14: evt [TextChanged] sur [TextBox2]. Texte 2=[second texte]</span>
        </p>
........

7.6.4. TextBox 组件的其他属性

[TextBox] 服务器组件还允许您生成 HTML 标签 <input type="password"..> 和 <textarea>..</textarea>,即分别对应密码字段和多行输入字段的标签。此生成操作由 [TextBox] 组件的 [TextMode] 属性控制。该属性有三个可能的值:

生成的 HTML 标签
SingleLine
<input type="text" ...>
MultiLine
<textarea>...</textarea>
密码
<input type="password ...>

我们将通过以下示例 [form5.aspx] 来探讨这些属性的用法:

Image

编号
name
type
属性
角色
1
btnAdd
按钮
 
[提交] 按钮 - 用于将 [TextBox1] 的内容添加到 [TextBox2] 中
2
TextBox1
文本框
TextMode=Password
文本=
受密码保护的输入框
3
TextBox2
文本框
文本模式=多行
文本=
合并[TextBox1]中输入的内容

页面的 [EnableViewState] 属性设置为 [false]。在服务器端,我们处理 [btnAjouter] 按钮的点击事件:

Sub btnAjouter_Click(sender As Object, e As EventArgs)
  ' the contents of [textBox1] are added to those of [TextBox2]
  textbox2.text=textbox2.text + textbox1.text+controlchars.crlf
End Sub

要理解这段代码,您需要回顾表单的 POST 请求是如何处理的。首先执行 [Page_Init] 和 [Page_Load] 过程。接下来是所有缓存的事件过程。最后,执行处理触发 [POST] 请求的事件的过程——在本例中即 [btnAjouter_Click] 过程。 当事件处理程序运行时,POST请求中包含值的所有页面组件都已采用该值。其余组件若其[EnableViewState]属性设置为[true],则恢复为之前的值;若设置为[false],则恢复为设计时值。 在此,[TextBox1] 和 [TextBox2] 字段的值将作为客户端发送的 POST 请求的一部分。因此,在上述代码中,[textbox1.text] 将包含客户端提交的值,[textbox2.text] 也是如此。 [btnAjouter_Click] 过程将 [TextBox2] 字段的值设置为:[TextBox2] 提交的值加上 [TextBox1] 提交的值,再加上一行换行符 [ControlChars.CrLf](该字符在 [Microsoft.VisualBasic] 命名空间中定义)。无需导入此命名空间,因为 Web 服务器默认已导入它。

[form5.aspx] 的最终代码如下:

<%@ Page Language="VB" EnableViewState="False" %>
<script runat="server">

    Sub btnAjouter_Click(sender As Object, e As EventArgs)
      ' on ajoute le contenu de [textBox1] à celui de [TextBox2]
      textbox2.text+=textbox1.text+controlchars.crlf
    End Sub

</script>
<html>
<head>
</head>
<body>
    <form runat="server">
        <p>
            <asp:Button id="btnAjouter" onclick="btnAjouter_Click" runat="server" Text="Ajouter" EnableViewState="False"></asp:Button>
            <asp:TextBox id="TextBox1" runat="server" TextMode="Password" Width="353px" EnableViewState="False"></asp:TextBox>
        </p>
        <p>
            <asp:TextBox id="TextBox2" runat="server" TextMode="MultiLine" Width="419px" Height="121px" EnableViewState="False"></asp:TextBox>
        </p>
    </form>
</body>
</html>

稍早前,我们提供了一张运行截图。

7.7. DropDownList 组件

<asp:DropDownList> 标签允许您在页面的呈现代码中插入下拉列表。我们创建了一个页面 [form6.aspx] 来生成以下布局:

Image

编号
名称
类型
属性
角色
1
下拉列表1
下拉列表
自动回发=true
启用视图状态=true
下拉列表
2
lblInfo
标签
EnableViewState=false
信息提示

生成的呈现代码如下:

Page Language="VB" %>
<script runat="server">

</script>
<html>
<head>
</head>
<body>
    <form runat="server">
        <p>
            <asp:DropDownList id="DropDownList1" runat="server" OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" AutoPostBack="True"></asp:DropDownList>
        </p>
        <p>
            <asp:Label id="lblInfo" runat="server" enableviewstate="False"></asp:Label>
        </p>
    </form>
</body>
</html>

目前,下拉列表中尚无任何项目。我们将在 [Page_Load] 过程 中为其填充数据。为此,我们需要了解 [DropDownList] 类的一些属性和方法:

Items
一个类型为 [ListItemCollection] 的集合,包含下拉列表中的项目。该集合的成员类型为 [ListItem]。
Items.Count
[Items] 集合中的项目数量
Items(i)
列表中的第 i 个元素——类型为 [ListItem]
Items.Add
向 [Items] 集合添加一个新的 [ListItem] 元素
Items.Clear
用于从 [Items] 集合中删除所有项目
Items.RemoveAt(i)
用于从 [Items] 集合中移除编号为 i 的项目
SelectedItem
[Items] 集合中第一个 [Selected] 属性为 true 的 [ListItem]
SelectedIndex
[Items] 集合中 [SelectedItem] 元素的索引

[DropDownList] 类的 [Items] 集合中的项目类型为 [ListItem]。每个 [ListItem] 都会生成一个 HTML <option> 标签:

<option value="V" [selected="selected"]>T</option>

下面介绍 [ListItem] 类的某些属性和方法:

ListItem(String text, String value)
构造函数——创建一个具有 [text] 和 [value] 属性的 [ListItem] 元素。一个 ListItem(T,V) 元素将生成 HTML 标签 <option value="V">T</option>。因此,[ListItem] 类允许我们描述 HTML 列表的元素
Selected
布尔型。若为 true,HTML 列表中的对应选项将具有 [selected="selected"] 属性。该属性告知浏览器,该元素在 HTML 列表中应显示为已选中
文本
HTML 选项 <option value="V" [selected="selected"]>T</option> 中的文本 T
Value
HTML 选项 <option value="V" [selected="selected"]>T</option> 的 [Value] 属性的值 V

我们已掌握足够的信息,可以在页面的 [Page_Load] 过程 中编写代码来填充 [DropDownList1] 下拉列表:

    Sub Page_Load(sender As Object, e As EventArgs)
        ' fill in the combo if it's the 1st time you've been called
        if not IsPostBack then
          dim valeurs() as String = {"1","2","3","4"}
            dim textes() as  String = {"un","deux","trois","quatre"}

            dim i as integer
            for i=0 to valeurs.length-1
                DropDownList1.Items.Add(new ListItem(textes(i),valeurs(i)))
            next
        end if
    end sub

一旦 [DropDownList1] 组件初始化完成,其 HTML 将如下所示:

<select name="DropDownList1" id="DropDownList1" onchange="__doPostBack('DropDownList1','')" language="javascript">

    <option value="1">un</option>
    <option value="2">deux</option>
    <option value="3">trois</option>
    <option value="4">quatre</option>

</select>

我们知道,每次加载 [form6.aspx] 页面时,都会执行 [Page_Load] 过程。它第一次通过 GET 请求被调用,之后每次用户从下拉列表中选择新项目时,则通过 POST 请求被调用。 填充此列表的代码是否应每次都在 [Page_Load] 中执行?答案取决于 [DropDownList1] 控件的 [EnableViewState] 属性。如果该属性设置为 true,则 [DropDownList1] 控件的状态将在隐藏字段 [__VIEWSTATE] 中跨请求保存。该状态包含两部分:

  • 下拉列表中的所有值列表
  • 该列表中已选项的值

将 [DropDownList1] 组件的 [EnableViewState] 属性设置为 [true] 似乎是个不错的选择,这样就无需重新计算要放入列表中的值。然而,问题在于,由于 [Page_Load] 过程会在每次请求 [form6.aspx] 页面时执行,这些值仍会被重新计算。 [form6.aspx] 作为其实例的 [Page] 对象,具有一个布尔类型的 [IsPostBack] 属性。如果该属性为 true,则表示页面是通过 POST 请求的;如果为 false,则表示页面是通过 GET 请求的。在我们的客户端-服务器往返系统中,客户端总是向服务器请求同一个页面 [form6.aspx]。 首次请求使用 GET 方法;后续请求则使用 POST 方法。由此可知,[IsPostBack] 属性可用于检测客户端的首次 GET 请求。我们仅在首次请求期间生成下拉列表的值。对于后续请求,这些值将由 [VIEWSTATE] 机制生成。 在其他情况下,列表的内容可能因请求而异,因此必须针对每次请求重新计算。此时,我们将列表的 [EnableViewState] 属性设置为 [false],以避免对列表内容进行不必要的重复计算——除非我们需要了解列表中先前选中的项目,因为这些信息存储在 [VIEWSTATE] 中。

[DropDownList1] 列表的 [AutoPostBack] 属性已设置为 true。这意味着,一旦浏览器检测到下拉列表中的“选中项已更改”事件,就会立即提交表单。 服务器则会利用 [VIEWSTATE] 和提交的值,检测到 [DropDownList1] 组件中的选定项已发生变化。随后,它将触发该组件上的 [SelectedIndexChanged] 事件。我们将通过以下过程来处理该事件:

    Sub DropDownList1_SelectedIndexChanged(sender As Object, e As EventArgs)
      ' selection change
      lblInfo.text="Elément sélectionné : texte="+dropdownlist1.selecteditem.text+ _
        " valeur=" + dropdownlist1.selecteditem.value + _
        " index="+ dropdownlist1.selectedindex.tostring
    End Sub

当此过程运行时,[DropDownList1] 对象已通过 [VIEWSTATE] 检索到其 [ListItem] 元素。此外,其中一个 [ListItem] 元素的 [Selected] 属性被设置为 true——即浏览器提交了其值的那个元素。访问该元素有几种方法:

DropDownList1.SelectedItem
是列表中第一个 [Selected] 属性设置为 true 的 [ListItem]
DropDownList1.SelectedItem.Text
对应用户所选 <option value="...">text</option> 元素的 HTML 标签中的 [text] 部分
DropDownList1.SelectedItem.Value
对应于用户所选 <option value="...">text</option> 元素的 HTML 标签中的 [value] 部分
DropDownList1.SelectedIndex
在 [DropDownList1.Items] 集合中,第一个 [ListItem] 元素的索引,该元素的 [Selected] 属性为 true

[form6.aspx] 的最终代码如下:

<%@ Page Language="VB" %>
<script runat="server">

    Sub Page_Load(sender As Object, e As EventArgs)
        ' on remplit le combo si c'est la 1ère fois qu'on est appelé
        if not IsPostBack then
          dim valeurs() as String = {"1","2","3","4"}
            dim textes() as  String = {"un","deux","trois","quatre"}

            dim i as integer
            for i=0 to valeurs.length-1
                DropDownList1.Items.Add(new ListItem(textes(i),valeurs(i)))
            next
        end if
    end sub

    Sub DropDownList1_SelectedIndexChanged(sender As Object, e As EventArgs)
      ' changement de sélection
      lblInfo.text="Elément sélectionné : texte="+dropdownlist1.selecteditem.text+ _
        " valeur=" + dropdownlist1.selecteditem.value + _
        " index="+ dropdownlist1.selectedindex.tostring
    End Sub

</script>
<html>
<head>
</head>
<body>
    <form runat="server">
        <p>
            <asp:DropDownList id="DropDownList1" runat="server" OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" AutoPostBack="True"></asp:DropDownList>
        </p>
        <p>
            <asp:Label id="lblInfo" runat="server" enableviewstate="False"></asp:Label>
        </p>
    </form>
</body>
</html>

7.8. ListBox 组件

<asp:ListBox> 标签允许您在页面的呈现代码中插入一个列表。我们创建 [form7.aspx] 以获得以下布局:

Image

编号
名称
类型
属性
角色
1
txtInput
文本框
EnableViewState=false
输入字段
2
btnAdd
按钮
 
[提交] 按钮,若 txtSaisie 字段不为空,则将其内容传输至列表 1。
3
ListBox1
ListBox
EnableViewState=true
选择模式=单选
单选值列表
4
ListBox2
ListBox
EnableViewState=true
选择模式=多选
多选值列表
5
btn1to2
按钮
 
[提交] 按钮,用于将选中的项目从 [列表 1] 转移到 [列表 2]
6
btn2to1
按钮
 
[提交] 按钮,用于将 [列表 2] 中的选定项目转移到 [列表 1]

生成的呈现代码如下:

<%@ Page Language="VB" %>
<script runat="server">
</script>
<html>
<head>
</head>
<body>
    <form runat="server">
        <p>
            Tapez un texte pour l'inclure dans Liste 1 :
            <asp:TextBox id="txtSaisie" runat="server" EnableViewState="False"></asp:TextBox>
        </p>
        <p>
            <asp:Button id="btnAjouter" onclick="btnAjouter_Click" runat="server" Text="Ajouter"></asp:Button>
        </p>
        <p>
            <table>
                <tbody>
                    <tr>
                        <td>
                            <p align="center">
                                Liste 1
                            </p>
                        </td>
                        <td>
                        </td>
                        <td>
                            <p align="center">
                                Liste 2
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <asp:ListBox id="ListBox1" runat="server"></asp:ListBox>
                        </td>
                        <td>
                            <p>
                                <asp:Button id="btn1vers2" onclick="btn1vers2_Click" runat="server" Text="-->"></asp:Button>
                            </p>
                            <p>
                                <asp:Button id="btn2vers1" onclick="btn2vers1_Click" runat="server" Text="<--"></asp:Button>
                            </p>
                        </td>
                        <td>
                            <p>
                                <asp:ListBox id="ListBox2" runat="server" SelectionMode="Multiple"></asp:ListBox>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <p align="center">
                                <asp:Button id="btnRaz1" onclick="btnRaz1_Click" runat="server" Text="Effacer"></asp:Button>
                            </p>
                        </td>
                        <td>
                        </td>
                        <td>
                            <p align="center">
                                <asp:Button id="btnRaz2" onclick="btnRaz2_Click" runat="server" Text="Effacer"></asp:Button>
                            </p>
                        </td>
                    </tr>
                </tbody>
            </table>
        </p>
    </form>
</body>
</html>

[ListBox] 类与前面讨论过的 [DropDownList] 类一样,都是从 [ListControl] 类派生而来的。它包含了 [DropDownList] 中的所有属性与方法,因为这些实际上都属于 [ListControl]。出现了一个新属性:

SelectionMode
用于设置该组件生成的 HTML <select> 列表的选择模式。若 SelectionMode=Single,则只能选择单个项目;若 SelectionMode=Multiple,则可选择多个项目。为实现此功能,HTML 列表的 <select> 标签中将生成 [multiple="multiple"] 属性。

接下来处理事件。点击 [Add] 按钮将由以下 [btnAdd_Click] 过程处理:

    Sub btnAjouter_Click(sender As Object, e As EventArgs)
      ' added to list 1
      dim texte as string=txtSaisie.text.trim
      if texte<> "" then ListBox1.Items.Add(New ListItem(texte))
      ' raz txtSaisie
      txtSaisie.text=""
    End Sub

如果 [txtSaisie] 中输入的文本不是空字符串,则向 [ListBox1] 列表中添加一个新项目。 我们知道需要添加一个 [ListItem] 类型的项目。此前,我们曾使用 [ListItem(T as String, V as String)] 构造函数来执行类似任务。此类项目会生成 HTML 标签 [<option value="V">T</option>]。 在此,我们使用构造函数 [ListItem(T as String)],它会生成 HTML 标签 [<option value="T">T</option>],即选项的文本 [T] 被用作该选项的值。一旦 [txtSaisie] 的内容被添加到 [ListBox1] 列表中,[txtSaisie] 字段就会被清空。

点击 [Clear] 按钮将由以下过程处理:

    Sub btnRaz1_Click(sender As Object, e As EventArgs)
      ' raz list 1
      ListBox1.Items.Clear
    End Sub

    Sub btnRaz2_Click(sender As Object, e As EventArgs)
      ' raz list 2
      ListBox2.Items.Clear
    End Sub

列表之间的切换按钮点击事件由以下过程处理:

    Sub btn1vers2_Click(sender As Object, e As EventArgs)
      ' transfer the item selected in list 1 to list 2
      transfert(ListBox1,ListBox2)
    End Sub

    Sub btn2vers1_Click(sender As Object, e As EventArgs)
      ' transfer of item selected in list 2 to list 1
      transfert(ListBox2,ListBox1)
    End Sub

    sub transfert(l1 as listbox, l2 as listbox)
      ' transfer items selected in l1 to l2
      ' anything to do?
      if l1.selectedindex=-1 then return
      dim i as integer
      ' we start at the end
      for i=l1.items.count-1 to 0 step -1
        ' selected?
        if l1.items(i).selected then
            ' more selected
            l1.items(i).selected=false
          ' transfer to l2
          l2.items.add(l1.items(i))
          ' deletion in l1
          l1.items.removeAt(i)
        end if
      next
    end sub

由于这两个按钮执行相同的任务——将项目从一个列表转移到另一个列表——我们可以将其简化为一个带有两个参数的转移过程:

  • l1 类型为 [ListBox],即源列表
  • l2 类型为 [ListBox],即目标列表

首先,我们检查列表 l1 中是否至少有一个选中项;如果没有,则无需执行任何操作。为此,我们检查属性 [l1.selectedindex],该属性表示列表中第一个选中项的索引。 若无选中项,其值为 -1。若 l1 中至少有一个选中项,则将其转移至 l2。为此,我们遍历 l1 中的所有项目,并逐一检查其 [selected] 属性是否为 true。 如果是,则将其 [selected] 属性设为 [false],随后将其复制到 l2 列表中,最后从 l1 列表中移除。这种移除操作会导致 l1 列表中的元素重新编号。这就是为什么需要按反向顺序遍历 l1 列表中的元素。 如果按正向遍历并移除第 10 个元素,第 11 个元素将变为第 10 个,第 12 个元素将变为第 11 个。处理完第 10 个元素后,正向循环将处理第 11 个元素——正如刚才所解释的,它实际上是原来的第 12 个元素。而原本是第 11 个、现在变为第 10 个的元素则会被忽略。通过反向遍历 l1 列表中的元素,我们可以避免这个问题。

7.9. CheckBox 和 RadioButton 组件

<asp:RadioButton> 和 <asp:CheckBox> 标签分别允许您在页面的呈现代码中插入单选按钮和复选框。我们创建一个页面 [form8.aspx] 以获得以下布局:

Image

名称
类型
属性
角色
1
单选按钮1
单选按钮2
单选按钮3
单选按钮
RadioButton1.Checked = true
RadioButton1.Text = 1
RadioButton2.Checked = false
RadioButton2.Text = 2
RadioButton3.Checked = false
RadioButton3.Text = 3
对于所有 3 个按钮:GroupName = radio
单选按钮
2
复选框A
复选框B
复选框C
复选框
所有复选框的 Checked 属性均为 false
CheckBoxA.Text = A
CheckBoxB.Text = BCheckBoxC.Text = C
复选框
3
btnSend
按钮
 
[提交] 按钮
4
lstInfo
列表框
 
信息列表

为了确保浏览器将这三个单选按钮视为互斥选项,必须将它们组合在单选按钮组中。这可以通过 [RadioButton] 类的 [GroupName] 属性来实现。本应用程序无需维护页面状态,因此我们在页面上设置了 [EnableViewState="false"] 属性。呈现代码如下:

<html>
<head>
</head>
<body>
    <form id="frmControls" runat="server">
        <h3>Cases à cocher 
        </h3>
        <p>
            <asp:RadioButton id="RadioButton1" runat="server" Checked="True" EnableViewState="False" GroupName="radio" Text="1"></asp:RadioButton>
            <asp:RadioButton id="RadioButton2" runat="server" EnableViewState="False" GroupName="radio" Text="2"></asp:RadioButton>
            &nbsp;<asp:RadioButton id="RadioButton3" runat="server" EnableViewState="False" GroupName="radio" Text="3"></asp:RadioButton>
        </p>
        <p>
            <asp:CheckBox id="CheckBoxA" runat="server" EnableViewState="False" Text="A"></asp:CheckBox>
            <asp:CheckBox id="CheckBoxB" runat="server" EnableViewState="False" Text="B"></asp:CheckBox>
            <asp:CheckBox id="CheckBoxC" runat="server" EnableViewState="False" Text="C"></asp:CheckBox>
        </p>
        <p>
            <asp:Button id="btnEnvoyer" onclick="btnEnvoyer_Click" runat="server" Text="Envoyer"></asp:Button>
            <asp:Button id="btnTree" onclick="btnTree_Click" runat="server" Text="Contrôles"></asp:Button>
        </p>
        <p>
            <asp:ListBox id="lstInfos" runat="server" EnableViewState="False" Rows="6" Height="131px"></asp:ListBox>
        </p>
    </form>
</body>
</html>

我们需要编写 [btnEnvoyer_Click] 过程来处理该按钮的 [Click] 事件。单选按钮或复选框的状态由其 [Checked] 属性决定,如果框被选中,该属性为 true;否则为 false。 因此,我们只需将六个单选按钮和复选框的 [Checked] 属性值写入 [lstInfos] 列表中。由于这并不难,让我们尝试一些稍微不同的内容:

<script runat="server">

    Sub btnEnvoyer_Click(sender As Object, e As EventArgs)
      ' place information in the listbox
      for each c as control in FindControl("frmControls").controls
          ' is the control derived from CheckBox?
          if TypeOf(c) is CheckBox then
              lstInfos.Items.Add(c.ID + " : " + Ctype(c,CheckBox).Checked.ToString)
          end if
      next
    End Sub

</script>

可以将页面视为控件的树形结构。在本例中,我们的页面包含文本和服务器控件。文本被视为一种名为 [LiteralControl] 的特殊控件。任何文本都会创建此控件,即使是两个控件之间的连续空格也不例外。每个控件都有一个用于标识它的 ID 属性。标签中显示的就是该 ID 属性:

            <asp:CheckBox id="CheckBoxA" runat="server" ></asp:CheckBox>

若忽略 [LiteralControl] 控件,该页面包含以下控件:

  • [HtmlForm],即表单 [ID=frmControls]。该表单本身是控件的容器,其中包含以下控件:

-- [ID=RadioButton1],类型为 [RadioButton]

-- 类型为 [RadioButton] 的 [ID=RadioButton2]

-- 类型为 [RadioButton] 的 [ID=RadioButton3]

-- 类型为 [CheckBox] 的 [ID=CheckBoxA]

-- [ID=CheckBoxA],类型为 [CheckBox]

-- [ID=CheckBoxA] 类型为 [CheckBox]

-- [ID=btnEnvoyer] 类型为 [Button]

控件具有以下属性:

[Control].Controls
返回 [Control] 的子控件集合(如有)
[Control].FindControl(ID)
返回位于 [Control] 子控件树根节点处、由 ID 标识的控件。在上例中:Page.FindControl("frmControls") 指代 [HtmlForm] 容器。要访问单选按钮 [RadioButton1],必须编写
Page.FindControl("frmControls").FindControl("RadioButton1")
[Control].ID
[Control] 标识符

让我们回到 [btnEnvoyer_Click] 过程的代码:

    Sub btnEnvoyer_Click(sender As Object, e As EventArgs)
      ' place information in the listbox
      for each c as control in FindControl("frmControls").controls
          ' is the control derived from CheckBox?
          if TypeOf(c) is CheckBox then
              lstInfos.Items.Add(c.ID + " : " + Ctype(c,CheckBox).Checked.ToString)
          end if
      next
    End Sub

我们希望在窗体上显示单选按钮和复选框的状态。我们遍历窗体上的所有控件。如果当前控件的类型是从 [CheckBox] 派生的,则显示其 [Checked] 属性。由于 [RadioButton] 类是从 [CheckBox] 类派生的,因此该条件适用于这两种类型的控件。上图所示的屏幕截图展示了输出结果的示例。

7.10. CheckBoxList 和 RadioButtonList 组件

有时,我们希望允许用户在页面设计时尚未确定的值之间进行选择。这些选项来自配置文件、数据库等,且仅在运行时才确定。针对此问题已有解决方案,我们也曾遇到过。 当用户只能进行单一选择时,单选列表是合适的选择;而当用户可以进行多项选择时,则应使用多选列表。从美学角度考虑,且若选项数量不多,您可能希望使用单选按钮代替单选列表,或使用复选框代替多选列表。这可以通过 [CheckBoxList] 和 [RadioButtonList] 来实现。

[CheckBoxList] 和 [RadioButtonList] 类与前面讨论过的 [DropDownList] 和 [ListBox] 类一样,都是从 [ListControl] 类派生而来的。因此,我们会发现其中包含那些类中出现过的某些属性和方法——具体来说,就是那些实际上属于 [ListControl] 的属性和方法。

Items
一个类型为 [ListItemCollection] 的集合,其中包含下拉列表中的项目。该集合的成员类型为 [ListItem]。
Items.Count
[Items] 集合中的项目数量
Items(i)
列表中的第 i 个元素——类型为 [ListItem]
Items.Add
向 [Items] 集合添加一个新的 [ListItem] 元素
Items.Clear
用于从 [Items] 集合中删除所有项目
Items.RemoveAt(i)
用于从 [Items] 集合中移除编号为 i 的项目
SelectedItem
[Items] 集合中第一个 [Selected] 属性为 true 的 [ListItem]
SelectedIndex
[Items] 集合中上一个元素的索引

某些属性仅适用于 [CheckBoxList] 和 [RadioButtonList] 类:

RepeatDirection
[horizontal] 或 [vertical],用于水平或垂直列表。

[Items] 集合中的元素类型为 [ListItem]。每个 [ListItem] 元素将根据其所属对象是 [CheckBoxList] 还是 [RadioButtonList] 生成不同的标签:

<input type="checkbox" [selected="selected"] value="V">Texte

<input type="radio" [selected="selected"] value="V">Texte

下面介绍 [ListItem] 类的部分属性和方法:

ListItem(String text, String value)
构造函数——创建一个具有 text 和 value 属性的 [ListItem] 元素。ListItem(T,V) 元素将根据具体情况生成相应的 HTML 标签 <input type="checkbox" value="V">T 或 <input type="radio" value="V">T。
Selected
布尔值。若为 true,HTML 列表中的对应选项将具有 [selected="selected"] 属性。该属性告知浏览器,该元素在 HTML 列表中应显示为已选中
文本
HTML 选项 <input type=".." value="V" [selected="selected"]>T 的文本 T
HTML 选项 <input type=".." value="V" [selected="selected"]> 的 Value 属性的值

我们建议构建以下页面 [form8b.aspx]:

Image

编号
姓名
类型
属性
角色
1
单选按钮列表1
单选按钮列表
启用视图状态=true
重复方向=水平
单选按钮列表
2
复选框列表1
复选框列表
EnableViewState=true
重复方向=水平
复选框列表
3
btnSend
按钮
 
[提交] 按钮,用于在 [4] 中显示从两个列表中选中的项目列表
4
lstInfo
列表框
EnableViewState=false
值列表

页面布局代码如下:

<%@ Page Language="VB" autoeventwireup="false" %>
<script runat="server">
...
</script>
<html>
<head>
</head>
<body>
    <form id="frmControls" runat="server">
        <h3>Listes de cases à cocher
        </h3>
        <p>
            <asp:RadioButtonList id="RadioButtonList1" runat="server" RepeatDirection="Horizontal"></asp:RadioButtonList>
        </p>
        <p>
            <asp:CheckBoxList id="CheckBoxList1" runat="server" RepeatDirection="Horizontal"></asp:CheckBoxList>
        </p>
        <p>
            <asp:Button id="btnEnvoyer" onclick="btnEnvoyer_Click" runat="server" Text="Envoyer"></asp:Button>
        </p>
        <p>
            <asp:ListBox id="lstInfos" runat="server" EnableViewState="False" Rows="6"></asp:ListBox>
        </p>
    </form>
</body>
</html>

控件代码如下:

<script runat="server">

    Sub Page_Load(sender As Object, e As EventArgs) handles MyBase.Load
        ' fill in the lists if it's the 1st time you're called
        if not IsPostBack then
            ' texts for the RadioButton list
          dim textesRadio() as String = {"1","2","3","4"}
          ' texts for the CheckBox list
          dim textesCheckBox() as  String = {"un","deux","trois","quatre"}
                ' radio list filling
            dim i as integer
            for i=0 to textesRadio.length-1
                RadioButtonList1.Items.Add(new ListItem(textesRadio(i)))
            next
            ' selection item no. 1
            RadioButtonList1.SelectedIndex=1
            ' checkbox list filling
            for i=0 to textesCheckBox.length-1
                CheckBoxList1.Items.Add(new ListItem(textesCheckBox(i)))
            next
        end if
    end sub


    Sub btnEnvoyer_Click(sender As Object, e As EventArgs)
      ' place information in the lstinfos listbox
      affiche(RadioButtonList1)
      affiche(CheckBoxList1)
    End Sub

    sub affiche(l1 as ListControl)
      ' displays the values of selected elements of l1
      ' anything to do?
      if l1.selectedindex=-1 then return
      dim i as integer
      ' we start at the end
      for i= 0 to l1.items.count-1
        ' selected?
        if l1.items(i).selected then
          lstInfos.Items.Add("["+TypeName(l1)+"] ["+l1.items(i).text+"] sélectionné")
        end if
      next
    end sub

</script>

在 [Page_Load] 过程(该过程在每次页面加载时运行)中,初始化了这两个列表。为避免每次都进行初始化,我们使用 [IsPostBack] 属性仅在首次加载时执行初始化。在后续加载时,列表将由 [VIEWSTATE] 机制自动重新生成。页面显示后,用户勾选某些复选框并点击 [Submit] 按钮。 随后,表单值将提交至表单本身。在 [Page_Load] 执行完毕后,将执行 [btnEnvoyer_Click] 过程。该过程调用 [affiche] 过程来填充 [lstInfos] 列表。 该过程接收一个 [ListControl] 对象作为参数,因此可以接受 [RadioButtonList] 对象或 [CheckBoxList] 对象——这两者都是从 [ListControl] 派生的类。由于 [lstInfos] 列表的状态无需在请求之间保持,因此可以将其 [EnableViewState] 属性设置为 [false]。

7.11. Panel 和 LinkButton 组件

<asp:panel> 标签允许您在页面中插入一个控件容器。该容器的优势在于,其某些属性适用于其中包含的所有控件。其 [Visible] 属性便是如此。该属性适用于每个服务器端控件。 如果容器具有 [Visible=false] 属性,则其中的每个控件将由其自身的 [Visible] 属性控制。如果容器具有 [Visible=false] 属性,则该容器及其包含的所有内容均不会显示。这比管理容器中每个控件的 [Visible] 属性更为简便。

<asp:LinkButton> 标签允许您在页面的呈现代码中插入链接。其作用与 [Button] 控件类似。它通过关联的 JavaScript 函数触发客户端 POST 请求。我们创建一个页面 [form9.aspx] 以生成以下布局:

Image

否。
name
类型
属性
角色
1
Panel1
面板
EnableViewState=true
控件容器
2
ListBox1
ListBox
EnableViewState=true
包含三个值的列表
3
lnkHide
链接按钮
EnableViewState=false
用于隐藏容器的链接

当容器被隐藏时,会出现一个新链接:

Image

名称
类型
属性
角色
4
lnkView
链接按钮
EnableViewState=false
用于显示容器的链接

页面布局代码如下:

<html>
<head>
</head>
<body>
    <form runat="server">
        <p>
            <asp:Panel id="Panel1" runat="server" BorderStyle="Ridge" BorderWidth="1px">
                <p>
                    Conteneur 
                </p>
                <p>
                    <asp:ListBox id="ListBox1" runat="server">
                        <asp:ListItem Value="1">un</asp:ListItem>
                        <asp:ListItem Value="2">deux</asp:ListItem>
                        <asp:ListItem Value="3" Selected="True">trois</asp:ListItem>
                    </asp:ListBox>
                </p>
            </asp:Panel>
        </p>
        <p>
            <asp:LinkButton id="lnkVoir" onclick="lnkVoir_Click" runat="server">Voir le conteneur</asp:LinkButton>
        </p>
        <p>
            <asp:LinkButton id="lnkCacher" onclick="lnkCacher_Click" runat="server">Cacher le conteneur</asp:LinkButton>
        </p>
    </form>
</body>
</html>

请注意,此代码为 [ListBox1] 列表初始化了三个值。两个链接的 [Click] 事件处理程序如下:

<%@ Page Language="VB" %>
<script runat="server">

    Sub Page_Load(sender As Object, e As EventArgs)
...
    end sub

    Sub lnkVoir_Click(sender As Object, e As EventArgs)
        ' displays container 1
        panel1.Visible=true
        ' changing links
        lnkVoir.visible=false
        lnkCacher.visible=true
    End Sub

    Sub lnkCacher_Click(sender As Object, e As EventArgs)
        ' hides container 1
        panel1.Visible=false
        ' changing links
        lnkVoir.visible=true
        lnkCacher.visible=false
    End Sub
</script>

我们将使用 [Page_Load] 过程来初始化表单。这将在首次请求时执行(IsPostBack=false):

<%@ Page Language="VB" %>
<script runat="server">

    Sub Page_Load(sender As Object, e As EventArgs)
        ' the 1st time
        if not IsPostBack then
            ' the container is shown
            lnkVoir_Click(nothing,nothing)
        end if
    end sub
.....
</script>

7.12. 继续阅读...

前面的段落介绍了许多服务器端组件。每个部分仅涵盖了其中少数几个属性。若要更深入地探索这些组件,读者可以采取以下几种方式:

  • 使用 WebMatrix 等集成开发环境 (IDE) 探索组件的属性。该工具会以表单形式展示所用组件的主要属性
  • 查阅 .NET 文档以探索与每个服务器组件对应的所有类。这是全面掌握该组件的首选方法。在那里,您将找到通向这些组件的类层次结构,以及每个组件的属性、方法、构造函数和事件。此外,文档有时还会提供示例。

在本章中,我们采用了 [WebMatrix] 的一体化方法,即把页面的呈现代码和控件代码放在同一个文件中。一般而言,我们不推荐这种方法,而是推荐之前使用的 [codebehind] 方法,该方法将这两类代码分别放在两个独立的文件中。 值得注意的是,这种分离的优势在于:无需运行 Web 应用程序即可编译控件代码。此外,正如本章开头所述,我们的示例具有非常特定的结构:它们由单个页面组成,该页面作为表单在客户端与服务器之间通过连续的请求-响应循环进行交互,其中首次客户端请求为 GET 请求,后续请求均为 POST 请求。

7.13. 服务器组件与应用程序控制器

在前几章中,我们构建了几个 Web 应用程序。它们均采用 MVC(模型-视图-控制器)架构构建,该架构将应用程序划分为独立的模块,从而便于维护。此前,我们使用标准的 HTML 标签构建用户界面。鉴于我们刚刚学到的内容,现在自然会想要使用服务器组件。让我们重新审视一个我们已经深入研究过的问题:税费计算。其 MVC 架构如下:

Image

该应用程序包含两个视图:[form.aspx] 和 [errors.aspx]。当首次请求 URL [main.aspx] 时,将显示 [form.aspx] 视图:

Image

用户填写表单:

Image

并点击 [Calculate] 按钮以获得以下响应:

Image

在 MVC 应用程序中,每个请求都必须经过控制器,本例中即 [main.aspx]。这意味着一旦用户填写完 [form.aspx] 表单,该表单必须提交至 [main.aspx] 而非 [form.aspx]。如果我们使用 ASP 服务器组件构建 [form.aspx] 用户界面,这将根本无法实现。 为了验证这一点,让我们使用 <asp:button> 组件构建一个 [formtest.aspx] 表单:

<%@ Page Language="VB" EnableViewState="false"%>
<html>
<head>
    <title>test</title>
</head>
<body>
    <form action="main.aspx" runat="server">
        <p>
            <asp:Button id="btnTest" runat="server" EnableViewState="false" Text="Test"></asp:Button>
        </p>
    </form>
</body>
</html>

请注意 <form> 标签中的 [action="main.aspx"] 属性。让我们运行这个应用程序。显示页面上仅显示一个按钮:

Image

让我们查看服务器发送的 HTML 代码:

<html>
<head>
    <title>test</title>
</head>
<body>
    <form name="_ctl0" method="post" action="formtest.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwtNTMwNzcxMzI0Ozs+" />

        <p>
            <input type="submit" name="btnTest" value="Test" id="btnTest" />
        </p>

    </form>
</body>
</html>

我们可以看到,表单的 POST 请求指向表单本身 [action="formtest.aspx"],而我们在 [formtest.aspx] 中编写了服务器端的 HTML 标签:

    <form action="main.aspx" runat="server">

在使用服务器端组件时,<form> 标签必须包含 [runat="server"] 属性。如果不包含此属性,将会引发编译错误。包含此属性后,<form> 标签的 [action] 属性将被忽略。服务器会自动生成一个指向表单本身的 [action] 属性。 由此可知,在 MVC 应用程序中,我们无法使用通过 <form ... runat="server"> 标签构建的表单。然而,对于所有需要获取用户输入的 ASP 服务器组件而言,该标签是必不可少的。 换言之,我们无法在 MVC 应用程序中使用 ASP 服务器表单。这是一个重大的发现。事实上,ASP.NET 的主要卖点之一就是能够像构建 Windows 应用程序那样构建 Web 应用程序。如果您的应用程序不遵循 MVC 架构,这一点确实成立;否则则不然。然而,MVC 架构似乎是现代 Web 开发中难以忽视的一个基本概念。

对于视图较少的应用程序,可以通过以下变通方法将 MVC 架构与 ASP 组件表单结合使用:

  • 应用程序由一个充当控制器的单页组成
  • 视图通过该页面上的不同容器呈现,每个视图对应一个容器。要显示某个视图时,只需将其对应的容器设为可见,并隐藏其余容器

这是一个优雅的解决方案,接下来我们将通过几个示例进行实现

7.14. 使用 ASP 服务器组件的 MVC 应用程序示例

7.14.1. 示例 1

在这个第一个示例中,我们将实现前面介绍的服务器组件。[form10.aspx] 页面将如下所示:

上图左侧的屏幕截图显示了用户看到的表单。用户填写表单后,点击 [Submit] 按钮提交。服务器返回一个视图,显示所输入值的列表(右侧屏幕截图)。通过一个链接,用户可以返回表单。他们看到的表单内容与提交时完全一致。[form10.aspx] 的呈现代码如下:

<html>
<head>
    <title>Exemple</title> <script language="javascript">
        function effacer(){
            alert("Vous avez cliqué sur [Effacer]")
        }
    </script>
</head>
<body>
    <p>
        Gestion d'un formulaire
    </p>
    <p>
        <hr />
    </p>
    <form runat="server">
        <p>
            <asp:Panel id="panelinfo" runat="server" EnableViewState="False">
                <p>
                    Liste des valeurs obtenues
                </p>
                <p>
                    <asp:ListBox id="lstInfos" runat="server" EnableViewState="False"></asp:ListBox>
                </p>
                <p>
                    <asp:LinkButton id="LinkButton1" onclick="LinkButton1_Click" runat="server">Retour au formulaire</asp:LinkButton>
                </p>
                <p>
                    <hr />
                </p>
            </asp:Panel>
        </p>
        <p>
            <asp:Panel id="panelform" runat="server" >
                <table>
                    <tbody>
                        <tr>
                            <td>
                                Etes-vous marié(e)</td>
                            <td>
                                <asp:RadioButton id="rdOui" runat="server"  GroupName="rdmarie"></asp:RadioButton>
                                Oui<asp:RadioButton id="rdNon" runat="server"  GroupName="rdmarie" Checked="True"></asp:RadioButton>
                                Non</td>
                        </tr>
                        <tr>
                            <td>
                                Cases à cocher</td>
                            <td>
                                <asp:CheckBox id="chk1" runat="server"></asp:CheckBox>
                                1<asp:CheckBox id="chk2" runat="server"></asp:CheckBox>
                                2<asp:CheckBox id="chk3" runat="server"></asp:CheckBox>
                                3</td>
                        </tr>
                        <tr>
                            <td>
                                Champ de saisie</td>
                            <td>
                                <asp:TextBox id="txtSaisie" runat="server"  MaxLength="20" Columns="20"></asp:TextBox>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                Mot de passe</td>
                            <td>
                                <asp:TextBox id="txtmdp" runat="server"  MaxLength="10" Columns="10" TextMode="Password"></asp:TextBox>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                Boîte de saisie</td>
                            <td>
                                <asp:TextBox id="txtArea" runat="server"  Columns="20" TextMode="MultiLine" Rows="3"></asp:TextBox>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                Liste déroulante</td>
                            <td>
                                <asp:DropDownList id="cmbValeurs" runat="server"></asp:DropDownList>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                Liste à choix unique</td>
                            <td>
                                <asp:ListBox id="lstSimple" runat="server"></asp:ListBox>
                                <asp:Button id="btnRazSimple" onclick="btnRazSimple_Click" runat="server" EnableViewState="False" Text="Raz"></asp:Button>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                Liste à choix multiple</td>
                            <td>
                                <asp:ListBox id="lstMultiple" runat="server" SelectionMode="Multiple"></asp:ListBox>
                                <asp:Button id="razMultiple" onclick="razMultiple_Click" runat="server" EnableViewState="False" Text="Raz"></asp:Button>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                Champ caché</td>
                            <td>
                                <asp:Label id="lblSecret" runat="server" visible="False"></asp:Label></td>
                        </tr>
                        <tr>
                            <td>
                                Bouton simple</td>
                            <td>
                                <input id="btnEffacer" onclick="effacer()" type="button" value="Effacer" /></td>
                        </tr>
                        <tr>
                            <td>
                                Bouton [reset]</td>
                            <td>
                                <input id="btnReset" type="reset" value="Rétablir" /></td>
                        </tr>
                        <tr>
                            <td>
                                Bouton [submit]</td>
                            <td>
                                <asp:Button id="btnEnvoyer" onclick="btnEnvoyer_Click" runat="server" EnableViewState="False" Text="Envoyer"></asp:Button>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </asp:Panel>
        </p>
    </form>
</body>
</html>

该页面包含两个容器,每个容器对应一个视图:[panelform] 用于表单视图,[panelinfo] 用于信息视图。[panelForm] 容器中的组件列表如下:

名称
类型
属性
角色
panelform
面板
EnableViewState=true
表单视图
rdYes
rdNo
单选按钮
EnableViewState=true
组名=rdmarie
单选按钮
chk1
chk2
chk3
复选框
EnableViewState=true
复选框
txtInput
文本框
EnableViewState=true
输入字段
txtPassword
文本框
EnableViewState=true
受保护的输入字段
txtArea
文本框
EnableViewState=true
多行文本框
cmbValues
下拉列表
EnableViewState=true
下拉列表
lstSimple
ListBox
EnableViewState=true
选择模式=单选
单选列表
btnRazSimple
按钮
EnableViewState=false
取消选中 lstSimple 中的所有项目
lstMultiple
列表框
EnableViewState=true
SelectionMode=Multiple
多选列表
btnClearMultiple
按钮
EnableViewState=false
取消选择 lstMultiple 中的所有项目
lblSecret
标签
EnableViewState=true
Visible=false
隐藏字段
btnClear
标准 HTML
 
显示一个提示框
btnSend
按钮
EnableViewState=false
表单上的 [提交] 按钮
btnReset
标准 HTML
 
表单 [reset] 按钮

在此处,[VIEWSTATE] 属性对组件的作用至关重要。除按钮外,所有组件都必须设置 [EnableViewState=true] 属性。要理解原因,我们需要回顾应用程序的工作原理。假设 [txtSaisie] 字段设置了 [EnableViewState=false] 属性:

  1. 客户端首次请求 [form10.aspx] 页面。它接收表单视图
  2. ,填写后通过 [Submit] 按钮提交。随后输入字段被提交,服务器将提交的值或其 [VIEWSTATE] 赋值给服务器端组件(如果存在的话)。因此,[txtSaisie] 字段被赋予用户输入的值。 因此,在此步骤中,其 [VIEWSTATE] 毫无用处。操作完成后,[informations] 视图被发送——实际上这仍然是 [form10.aspx] 页面,只是显示容器不同。
  3. 用户查看此新视图,并使用 [返回表单] 链接返回该视图。随后向 [form10.aspx] 发送了一个 POST 请求。最多只有一个提交值:用户从信息列表中选定的值,该值后续不会被使用。无论如何,[txtSaisie] 字段都没有被提交。
  4. 服务器接收该 POST 请求,并将提交的值赋给服务器控件,或赋给其 [VIEWSTATE](如果存在的话)。在此处,[txtNom] 没有提交的值。如果其 [EnableViewState] 属性设置为 [false],则会将其赋值为空字符串。由于我们希望它具有用户输入的值,因此必须设置 [EnableViewState=true]。

[panelinfo] 容器包含以下控件:

name
类型
属性
角色
panelinfo
面板
EnableViewState=false
信息视图
lstInfos
列表框
EnableViewState=false
汇总用户输入值的信息列表
LinkButton1
LinkButton
EnableViewState=false
返回表单的链接

在测试过程中,如果查看上述呈现代码生成的 HTML 代码,我们可能会对隐藏字段 [lblSecret] 生成的代码感到惊讶:

                    <tr>
                        <td>
                            Champ caché</td>
                        <td></td>
                    </tr>

由于 [lblSecret] 组件具有 [Visible=false] 属性,因此不会渲染为 HTML。但是,由于它具有 [EnableViewState=true] 属性,其值仍将存储在隐藏字段 [__VIEWSATE] 中。因此,正如后续测试将展示的那样,我们将能够检索该值。

我们还需要编写事件处理程序。在 [Page_Load] 中,我们将初始化表单:

Sub page_Load(sender As Object, e As EventArgs)
         ' the 1st time, we initialize the elements
         ' subsequent times, they are reset to their values by VIEWSTATE
         if IsPostBack then return
         ' init form
         ' panelinfo not displayed
         panelinfo.visible=false
         ' paneform displayed
         panelform.visible=true
         ' radio buttons
         rdNon.Checked=true
         ' checkboxes
         chk2.Checked=true
         ' input field
         txtSaisie.Text="qqs mots"
         ' password field
         txtMdp.Text="ceciestsecret"
         ' input box
         txtArea.Text="ligne"+ControlChars.CrLf+"ligne2"+ControlChars.CrLf
         ' combo
         dim i as integer
         for i=1 to 4
             cmbValeurs.Items.Add(new ListItem("choix"+i.ToString,i.ToString))
         next
         cmbValeurs.SelectedIndex=1
         ' simple selection list
         for i=1 to 7
             lstSimple.Items.Add(new ListItem("simple"+i.ToString,i.ToString))
         next
         lstSimple.SelectedIndex=0
         ' multiple selection list
         for i=1 to 10
             lstMultiple.Items.Add(new ListItem("multiple"+i.ToString,i.ToString))
         next
         lstMultiple.Items(0).Selected=true
         lstMultiple.Items(2).Selected=true
         ' hidden field
         lblSecret.Text="secret"
End Sub

单击 [lstRazSimple] 和 [lstMultiple] 按钮:

Sub btnRazSimple_Click(sender As Object, e As EventArgs)
    ' raz single list
    lstSimple.SelectedIndex=-1
End Sub

Sub razMultiple_Click(sender As Object, e As EventArgs)
    ' raz multiple list
    lstMultiple.SelectedIndex=-1
End Sub

点击 [发送] 按钮:

Sub btnEnvoyer_Click(sender As Object, e As EventArgs)
    ' the info panel is made visible and the form panel is hidden
  panelinfo.Visible=true
  panelform.visible=false
  ' we retrieve the posted values and put them in lstInfos
  ' radio buttons
  dim info as string="état marital : "+iif(rdoui.checked,"marié"," non marié")
  affiche(info)
  ' checkboxes
  info=" cases cochées : "+iif(chk1.checked,"1 oui","1 non")+","+ _
      iif(chk2.checked,"2 oui","2 non")+","+iif(chk3.checked,"3 oui","3 non")
  affiche(info)
  ' input field
  affiche("champ de saisie : " + txtSaisie.Text.Trim)
  ' password
  affiche("mot de passe : " + txtMdp.Text.Trim)
  ' input box
  dim lignes() as String
  lignes=new Regex("\r\n").Split(txtArea.Text.Trim)
  dim i as integer
  for i=0 to lignes.length-1
      lignes(i)="["+lignes(i).Trim+"]"
  next
  affiche("Boîte de saisie : " + String.Join(",",lignes))
  ' combo
  affiche("éléments sélectionnés dans combo : "+selection(cmbValeurs))
  ' simple list
  affiche("éléments sélectionnés dans liste simple : "+selection(lstSimple))
  ' multiple list
  affiche("éléments sélectionnés dans liste multiple : "+selection(lstMultiple))
  ' hidden field
  affiche ("Champ caché : " + lblSecret.Text)
End Sub

sub affiche(msg as String)
    ' displays msg in lstInfos
    lstInfos.Items.Add(msg)
end sub

       function selection(liste as ListControl) as string
           ' browse list elements
           ' to find those selected
           dim i as integer
           dim info as string=""
           for i=0 to liste.Items.Count-1
               if liste.Items(i).Selected then info+="[" + liste.Items(i).Text + "]"
           next
           return info
       end function

最后,点击 [返回表单] 链接:

Sub LinkButton1_Click(sender As Object, e As EventArgs)
    ' display the form and hide the info panel
    panelform.visible=true
    panelinfo.visible=false
End Sub

7.14.2. 示例 2

在此,我们将重新审视之前介绍过的一个使用标准 HTML 表单的应用程序。该应用程序允许用户进行税费计算模拟。它依赖于一个 [impot] 类,此处不再赘述。该类需要从 OLEDB 数据源中检索数据。在本示例中,我们将使用一个 ACCESS 数据源。

7.14.2.1. 应用程序的 MVC 结构

该应用程序的 MVC 结构如下:

Image

这三个视图将作为容器被整合到控制器 [main.aspx] 的呈现代码中。因此,该应用程序仅包含一个页面 [main.aspx]。

7.14.2.2. Web 应用程序的视图

[form]视图是用于输入计算用户税费所需信息的表单:

Image

用户填写表单:

Image

他们点击[提交]按钮请求税额计算。随后看到以下[模拟]视图:

Image

用户通过上方链接返回表单。表单显示为他们填写时的状态。用户可能出现数据输入错误:

Image

这些错误会在 [错误] 视图中被标记:

Image

用户通过上方链接返回表单。表单将保持其上次填写时的状态。用户可以进行新的模拟:

Image

随后,用户将看到包含一项新增模拟的 [模拟] 视图:

Image

最后,如果数据源不可用,系统将在 [错误] 视图中通知用户:

Image

7.14.2.3. 应用程序的呈现代码

请记住,[main.aspx] 页面整合了所有视图。它是一个包含三个容器的单一表单:

  • 用于 [表单] 视图的 [panelform]
  • [panelerrors] 用于 [errors] 视图
  • [panelsimulations] 用于 [simulations] 视图

我们将呈现代码和控件代码再次分离到两个独立的文件中。前者位于 [main.aspx],后者位于 [main.aspx.vb]。[main.aspx] 的代码如下:


<%@ page codebehind="main.aspx.vb" inherits="vs.main" AutoEventWireUp="false" %>
<HTML>
    <HEAD>
        <title>Calcul d'impôt </title>
    </HEAD>
    <body>
        <P>Calcul de votre impôt</P>
        <HR width="100%" SIZE="1">
        <FORM id="Form1" runat="server">
            <asp:panel id="panelform" Runat="server">
                <TABLE id="Table1" cellSpacing="1" cellPadding="1" border="0">
                    <TR>
                        <TD height="19">Etes-vous marié(e)</TD>
                        <TD height="19">
                            <asp:RadioButton id="rdOui" runat="server" GroupName="rdMarie"></asp:RadioButton>Oui
                            <asp:RadioButton id="rdNon" runat="server" GroupName="rdMarie" Checked="True"></asp:RadioButton>Non</TD>
                    </TR>
                    <TR>
                        <TD>Nombre d'enfants</TD>
                        <TD>
                            <asp:TextBox id="txtEnfants" runat="server" MaxLength="3" Columns="3"></asp:TextBox></TD>
                    </TR>
                    <TR>
                        <TD>Salaire annuel (euro)</TD>
                        <TD>
                            <asp:TextBox id="txtSalaire" runat="server" MaxLength="10" Columns="10"></asp:TextBox></TD>
                    </TR>
                </TABLE>
                <P>
                    <asp:Button id="btnCalculer" runat="server" Text="Calculer"></asp:Button>
                    <asp:Button id="btnEffacer" runat="server" Text="Effacer"></asp:Button></P>
            </asp:panel>
          <asp:panel id="panelerreurs" runat="server">
                <P>Les erreurs suivantes se sont produites :</P>
                <P>
                    <asp:Literal id="erreursHTML" runat="server"></asp:Literal></P>
                <P></P>
                <asp:LinkButton id="lnkForm1" runat="server">Retour au formulaire</asp:LinkButton>
            </asp:panel>
          <asp:panel id="panelsimulations" runat="server">
                <P>
                    <TABLE>
                        <TR>
                            <TH>
                                Marié</TH>
                            <TH>
                                Enfants</TH>
                            <TH>
                                Salaire annuel</TH>
                            <TH>
                                Impôt à payer (euro)</TH></TR>
                        <asp:Literal id="simulationsHTML" runat="server"></asp:Literal></TABLE>
                    <asp:LinkButton id="lnkForm2" runat="server">Retour au formulaire</asp:LinkButton></P>
            </asp:panel>
      </FORM>
    </body>
</HTML>

我们已经定义了这三个容器。请注意,它们都位于 <form runat="server"> 标签内。这是必须的,因为要利用服务器组件,必须将它们放置在这样的标签内。需要理解的关键点是,这里有一个单一的表单,将在客户端和 Web 服务器之间进行交换。因此,我们正在使用本章中关于服务器组件的配置。让我们来分解每个容器的组件:

[panelform] 容器:

名称
类型
属性
角色
panelform
面板
EnableViewState=true
表单视图
rdYes
rdNo
单选按钮
EnableViewState=true
组名=rdmarie
单选按钮
txtChildren
文本框
EnableViewState=true
子女数量
txtSalary
文本框
EnableViewState=true
年薪
btnCalculate
按钮
 
表单上的 [提交] 按钮——开始计算税款
btnClear
按钮
 
表单上的 [提交] 按钮 - 清空表单

[errorPanel] 容器:

名称
类型
属性
角色
面板错误
面板
EnableViewState=true
错误视图
lnkForm1
LinkButton
EnableViewState=true
表单链接
HTML 错误
字面量
 
错误列表的HTML代码

[panelsimulations] 容器:

名称
类型
属性
角色
面板模拟
面板
EnableViewState=true
模拟视图
lnkForm2
链接按钮
EnableViewState=true
表单链接
simulationsHTML
字面量
 
用于 HTML 表格中模拟列表的 HTML 代码

7.14.2.4. 应用程序的控件代码

应用程序的控制代码分布在 [global.asax.vb] 和 [main.aspx.vb] 文件中。[global.asax] 文件的定义如下:

<%@ Application src="Global.asax.vb" Inherits="Global" %>

[global.asax.vb] 文件内容如下:


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration
Imports System.Collections
 
Public Class Global
    Inherits System.Web.HttpApplication
 
    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' create an impot object
        Dim objImpot As impot
        Try
            objImpot = New impot(New impotsOLEDB(ConfigurationSettings.AppSettings("chaineConnexion")))
            ' put the object in the application
            Application("objImpot") = objImpot
            ' no error
            Application("erreur") = False
        Catch ex As Exception
            'there has been an error, we note it in the application
            Application("erreur") = True
            Application("message") = ex.Message
        End Try
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' start of session - create a list of empty simulations
        Session.Item("simulations") = New ArrayList
    End Sub
End Class

当应用程序启动时(即向应用程序发出第一个请求时),将执行 [Application_Start] 过程。该过程会尝试通过从 OLEDB 数据源检索数据来创建一个 [impot] 类型的对象。如果读者已忘记该类,建议查阅定义了该类的第 5 章。如果数据源不可用,[impot] 对象的创建可能会失败。 在这种情况下,错误信息将存储在应用程序中,以便后续的所有请求都知道它无法正确初始化。如果创建成功,生成的 [impot] 对象也会存储在应用程序中。所有税费计算请求都将使用该对象。 当客户端发出首次请求时,[Application_Start] 过程会为其创建一个会话。该会话旨在存储用户将执行的各种税费计算模拟。这些模拟将存储在一个与会话键 "simulations" 关联的 [ArrayList] 对象中。会话启动时,该键会关联一个空的 [ArrayList] 对象。应用程序所需的信息存放在其配置文件 [wenConfig] 中:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="chaineConnexion" value="Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=D:\data\serge\devel\aspnet\poly\webforms\vs\impots5\impots.mdb" />
    </appSettings>
</configuration>

[connectionString] 键指定了连接到 OLEDB 数据源的连接字符串。其余的控件代码位于 [main.aspx.vb] 中:


Imports System.Collections
Imports Microsoft.VisualBasic
Imports st.istia.univangers.fr
Imports System
 
Public Class main
    Inherits System.Web.UI.Page
 
    Protected WithEvents rdOui As System.Web.UI.WebControls.RadioButton
    Protected WithEvents rdNon As System.Web.UI.WebControls.RadioButton
    Protected WithEvents txtEnfants As System.Web.UI.WebControls.TextBox
    Protected WithEvents txtSalaire As System.Web.UI.WebControls.TextBox
    Protected WithEvents btnCalculer As System.Web.UI.WebControls.Button
    Protected WithEvents btnEffacer As System.Web.UI.WebControls.Button
    Protected WithEvents panelform As System.Web.UI.WebControls.Panel
    Protected WithEvents lnkForm1 As System.Web.UI.WebControls.LinkButton
    Protected WithEvents lnkForm2 As System.Web.UI.WebControls.LinkButton
    Protected WithEvents panelerreurs As System.Web.UI.WebControls.Panel
    Protected WithEvents panelsimulations As System.Web.UI.WebControls.Panel
    Protected WithEvents simulationsHTML As System.Web.UI.WebControls.Literal
    Protected WithEvents erreursHTML As System.Web.UI.WebControls.Literal
 
    ' local variables
 
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
    End Sub
 
    Private Sub afficheFormulaire()
...
    End Sub
 
    Private Sub afficheSimulations(ByRef simulations As ArrayList, ByRef lien As String)
...
    End Sub
 
    Private Sub btnCalculer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCalculer.Click
...
    End Sub
 
    Private Function checkData() As ArrayList
...
    End Function
 
    Private Sub lnkForm1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkForm1.Click
....
    End Sub
 
    Private Sub lnkForm2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkForm2.Click
...
    End Sub
 
    Private Sub btnEffacer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEffacer.Click
...
    End Sub
 
    Private Sub razForm()
...
    End Sub
End Class
 
Le premier événement traité par le code est [Page_Load] :
 
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' first, we look at the state of the application
        If CType(Application("erreur"), Boolean) Then
            ' the application failed to initialize
            ' the error view is displayed
            Dim erreurs As New ArrayList
            erreurs.Add("Application momentanément indisponible (" + CType(Application("message"), String) + ")")
            afficheErreurs(erreurs, "")
            Exit Sub
        End If
        ' no errors - on the 1st request, the form is presented
        If Not IsPostBack Then afficheFormulaire()
    End Sub

请记住,当 [Page_Load] 过程在客户端 POST 请求时运行,所有表单控件都有一个值:要么是客户端提交的值(如果存在),要么是通过 [VIEWSTATE] 获取的控件先前值。在此表单中,所有控件都具有 [EnableViewState=true] 属性。 在处理请求之前,我们会确保应用程序已正确初始化。若未初始化,则使用 [displayErrors] 过程显示 [errors] 视图。若为首次请求(IsPostBack=false),则使用 [displayForm] 显示 [form] 视图。

显示 [errors] 视图的程序如下:


    Private Sub afficheErreurs(ByRef erreurs As ArrayList, ByRef lien As String)
        ' displays the error container
        panelerreurs.Visible = True
        Dim i As Integer
        erreursHTML.Text = ""
        For i = 0 To erreurs.Count - 1
            erreursHTML.Text += "<li>" + erreurs(i).ToString + "</li>" + ControlChars.CrLf
        Next
        lnkForm1.Text = lien
        ' the other containers are hidden
        panelform.Visible = False
        panelsimulations.Visible = False
    End Sub

该过程有两个参数:

  • [errors] 中的错误消息列表
  • [link] 中的链接文本

用于生成错误列表的 HTML 代码放置在 [errorsHTML] 字面量中。链接文本放置在视图中 [LinkButton] 对象的 [Text] 属性中。

显示 [form] 视图的存储过程如下:


    Private Sub afficheFormulaire()
        ' displays the form
        panelform.Visible = True
        ' the other containers are hidden
        panelerreurs.Visible = False
        panelsimulations.Visible = False
    End Sub

此过程仅将 [panelform] 容器设为可见。组件将显示其提交值或上一次的值(VIEWSTATE)。

当用户在 [form] 视图中单击 [Calculate] 按钮时,会向 [main.aspx] 发送一个 POST 请求。系统将执行 [Page_Load] 过程,随后执行 [btnCalculate_Click] 过程:


    Private Sub btnCalculer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCalculer.Click
        ' check the validity of the data entered
        Dim erreurs As ArrayList = checkData()
        ' if there are errors, we report them
        If erreurs.Count <> 0 Then
            ' the error page is displayed
            afficheErreurs(erreurs, "Retour au formulaire")
            Exit Sub
        End If
        ' no errors - tax is calculated
        Dim impot As Long = CType(Application("objImpot"), impot).calculer( _
        rdOui.Checked, CType(txtEnfants.Text, Integer), CType(txtSalaire.Text, Long))
        ' the result is added to existing simulations
        Dim simulation() As String = New String() {CType(IIf(rdOui.Checked, "oui", "non"), String), _
         txtEnfants.Text.Trim, txtSalaire.Text.Trim, impot.ToString}
        ' the result is added to existing simulations
        Dim simulations As ArrayList = CType(Session.Item("simulations"), ArrayList)
        simulations.Add(simulation)
        ' put simulations in session and context
        Session.Item("simulations") = simulations
        ' the result page is displayed
        afficheSimulations(simulations, "Retour au formulaire")
    End Sub

该过程首先使用 [checkData] 过程验证表单字段,该过程会返回一个包含错误消息的 [ArrayList]。如果列表不为空,则显示 [errors] 视图并终止该过程。如果输入的数据有效,则使用启动时存储在应用程序中的 [tax] 对象计算税额。 此新模拟将添加到已执行的模拟列表中,并存储在会话中。

[CheckData] 函数用于验证数据的有效性。它返回一个包含错误消息的 [ArrayList],若数据有效,该列表为空:


    Private Function checkData() As ArrayList
        ' initially no errors
        Dim erreurs As New ArrayList
        ' no. of children
        Try
            Dim nbEnfants As Integer = CType(txtEnfants.Text, Integer)
            If nbEnfants < 0 Then Throw New Exception
        Catch
            erreurs.Add("Le nombre d'enfants est incorrect")
        End Try
        ' salary
        Try
            Dim salaire As Long = CType(txtSalaire.Text, Long)
            If salaire < 0 Then Throw New Exception
        Catch
            erreurs.Add("Le salaire annuel est incorrect")
        End Try
        ' return the list of errors
        Return erreurs
    End Function

最后,通过以下 [displaySimulations] 过程显示 [simulations] 视图:


    Private Sub afficheSimulations(ByRef simulations As ArrayList, ByRef lien As String)
        ' displays the simulations view
        panelsimulations.Visible = True
        ' the other containers are hidden
        panelerreurs.Visible = False
        panelform.Visible = False
        ' contents of simulations view
        ' each simulation is an array of 4 string elements
        Dim simulation() As String
        Dim i, j As Integer
        simulationsHTML.Text = ""
        For i = 0 To simulations.Count - 1
            simulation = CType(simulations(i), String())
            simulationsHTML.Text += "<tr>"
            For j = 0 To simulation.Length - 1
                simulationsHTML.Text += "<td>" + simulation(j) + "</td>"
            Next
            simulationsHTML.Text += "</tr>" + ControlChars.CrLf
        Next
        ' link
        lnkForm2.Text = lien
    End Sub

该过程有两个参数:

  • [simulations] 中的模拟列表
  • [link] 中的链接文本

用于生成模拟列表的 HTML 代码存储在 [simulationsHTML] 常量中。链接文本存储在视图中 [LinkButton] 对象的 [Text] 属性中。

当用户点击 [form] 视图中的 [Clear] 按钮时,将执行 [btnClear_click] 过程(始终在 [Page_Load] 之后):


    Private Sub btnEffacer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEffacer.Click
        ' displays the empty form
        razForm()
        afficheFormulaire()
    End Sub
 
    Private Sub razForm()
        ' empty the form
        rdOui.Checked = False
        rdNon.Checked = True
        txtEnfants.Text = ""
        txtSalaire.Text = ""
    End Sub

上面的代码非常简单,无需添加注释。我们还需要处理 [errors] 和 [simulations] 视图链接的点击事件:


    Private Sub lnkForm1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkForm1.Click
        ' displays the form
        afficheFormulaire()
    End Sub
 
    Private Sub lnkForm2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkForm2.Click
        ' displays the form
        afficheFormulaire()
    End Sub

这两个过程仅用于显示 [表单] 视图。我们知道,表单的字段将接收一个值,该值要么是为其提交的值,要么是其之前的值。由于在此情况下,客户端的 POST 请求未发送任何表单字段的值,因此这些字段将恢复为之前的值。因此,表单将显示用户输入的值。我们记得,在没有服务器组件的版本中,我们曾自行执行过这种恢复操作。

7.14.2.5. 测试

应用程序所需的所有文件都放置在名为 <application-path> 的文件夹中:
[bin] 文件夹中包含应用程序所需的 [import]、[impotsData] 和 [impotsOLEDB] 类所在的 DLL 文件:

读者如有需要,可重温第 5 章,其中介绍了如何创建上述的 [impot.dll] 文件。完成此操作后,使用参数 (<application-path>,/impots5) 启动 Cassini 服务器。我们使用浏览器访问 URL [http://impots5/main.aspx]:

Image

如果我们将 ACCESS 文件 [impots.mdb] 重命名为 [impots1.mdb],将看到以下页面:

Image

7.14.3. 示例 3

通过这两个示例,我们展示了使用服务器端组件构建遵循 MVC 架构的 Web 应用程序是可行的。最后一个示例表明,服务器端组件的解决方案比使用标准 HTML 标签的解决方案更为简单。 前两个示例仅包含一个页面,且多个视图位于同一页面内。只要这些表单将自身值提交回自身不会造成问题,您就可以构建包含多个服务器端 ASP 表单的 MVC 架构。这在带有菜单的应用程序中非常常见。让我们来看以下示例:

Image

我们将迄今为止编写的所有应用程序的链接汇集在单一页面上。此类应用程序非常适合采用 MVC 架构。唯一的区别在于,控制器不再仅有一个,而是有多个。

Image

[main.aspx] 控制器充当主控制器,它是应用程序主页上链接所调用的对象。它将执行所有可能操作的通用处理,然后执行与所用链接关联的特定操作。随后,它将控制权移交给负责执行该操作的某个次级控制器。从这一刻起,通信将在客户端与该特定控制器之间进行。 我们不再通过主控制器 [main.aspx] 进行处理。因此,我们不再处于由单一控制器过滤所有请求的 MVC 框架中。如前所述,上述每个控制器均可利用容器机制在单个页面内呈现多个视图。

不再由单一控制器决定向客户端发送哪个视图,这确实存在弊端。以错误处理为例。应用程序暴露的每个操作都可能需要显示错误视图。每个控制器 [applix.aspx] 都会拥有自己的 [errors] 视图,因为这仅仅是控制器页面内的一个特定容器。 无法通过单一的 [errors] 视图来满足所有应用程序的需求。实际上,此类视图通常包含返回出错表单的链接,而该表单必须恢复到经过验证的状态,以便用户更正错误。这种恢复是通过 [VIEWSTATE] 机制实现的,但该机制无法在不同控制器之间生效。 如果应用程序由不同人员开发,则存在因用户选择的操作不同而导致错误页面外观不一致的风险,这会破坏整个应用程序的一致性。稍后我们将看到,ASP.NET 为共享视图这一具体问题提供了解决方案。这可以通过我们自己构建的一个新服务器组件来实现。 只需在各个应用程序中使用该组件,即可确保整个应用程序的一致性。更难管理的是操作顺序问题。当所有请求都通过单个控制器时,该控制器可以验证所请求的操作是否与前一个操作兼容。这种验证代码位于单一位置。而在这里,它需要分布在各个控制器中,从而增加了整个应用程序维护的复杂性。

让我们回到上面的应用程序。其入口页面是一个标准的 HTML 页面 [home.htm]:

<html>
    <head>
        <TITLE>Composants ASP Serveur</TITLE>
        <meta name="pragma" content="no-cache">
    </head>
    <frameset rows="130,*" frameborder="0">
        <frame name="banner" src="bandeau.htm" scrolling="no">
        <frameset cols="200,*">
            <frame name="contents" src="options.htm">
            <frame name="main" src="main.htm">
        </frameset>
        <noframes>
            <p id="p1">
                Ce jeu de frames HTML affiche plusieurs pages Web. Pour afficher ce jeu de 
                frames, utilisez un navigateur Web qui prend en charge HTML 4.0 et version 
                ultérieure.
            </p>
        </noframes>
    </frameset>
</html>

该主页由三个名为bannercontentsmain的框架组成:

放置在 [banner] 框架中的页面 [bandeau.htm] 如下:

Image

其HTML代码如下:


<html>
    <head>
        <META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE" />
        <title>bandeau</title>
    </head>
    <body>
        <P>
            <TABLE>
                <TR>
                    <TD><IMG alt="logo université d'angers" src="univ01.gif"></TD>
                    <TD>Composants serveurs ASP</TD>
                </TR>
            </TABLE>
        </P>
        <HR>
    </body>
</html>

[options.htm] 页面位于 [contents] 横幅中。它是一组链接:


<html>
    <head>
        <meta http-equiv="pragma" content="no-cache" />
        <title>选项</title>
    </head>
    <body bgcolor="Gold">
        
            
                <a href="main.aspx?action=label" 
                 target="main">标签</a>
            
                <a href="main.aspx?action=button" 
                 target="main">按钮
            
                <a href="main.aspx?action=textbox1" 
                 target="main">文本框-1"
            
                <a href="main.aspx?action=textbox2" 
                 target="main">文本框-2
            
                <a href="main.aspx?action=dropdownlist" 
                 target="main">下拉列表
            
                <a href="main.aspx?action=listbox" 
                 target="main">列表框
            
                <a href="main.aspx?action=checkbox" 
                  target="main">复选框" 
                单选按钮
            
                <a href="main.aspx?action=listecasesacocher" 
                  target="main">复选框列表和
                单选按钮列表
            
                <a href="main.aspx?action=panel" 
                  target="main">面板
        
    </body>
</html>

各种链接均指向主控制器 [main.aspx],并通过 [action] 参数指定要执行的操作。我们指定链接目标应在 [main] 框架中显示(target="main")。

在 [main] 框架中显示的首个页面是 [main.htm]:


<html>
    <head>
        <title>main</title>
    </head>
    <body>
        <P>选择一个选项,以测试并了解某类服务器组件...</P>
    </body>
</html>

主控制器 [main.aspx, main.aspx.vb] 如下所示:

[main.aspx]

<%@ page src="main.aspx.vb" inherits="main" autoeventwireup="false" %>

[main.aspx.vb]

Public Class main

Inherits System.Web.UI.Page

Private Sub Page\_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    &#x27; 获取待执行的操作

    Dim action As String

    If Request.QueryString(&quot;action&quot;) Is Nothing Then

        action = &quot;label&quot;

    Else

        action = Request.QueryString(&quot;action&quot;).ToString.ToLower

    End If

    &#x27; 执行操作

    根据 action 进行判断

        Case &quot;label&quot;

            Server.Transfer(&quot;form2.aspx&quot;)

        Case &quot;button&quot;

            Server.Transfer(&quot;form3.aspx&quot;)

        Case &quot;textbox1&quot;

            Server.Transfer(&quot;form4.aspx&quot;)

        “textbox2”字段

            Server.Transfer(&quot;form5.aspx&quot;)

        Case &quot;dropdownlist&quot;

            Server.Transfer(&quot;form6.aspx&quot;)

        Case &quot;listbox&quot;

            Server.Transfer(&quot;form7.aspx&quot;)

        Case &quot;checkbox&quot;

            Server.Transfer(&quot;form8.aspx&quot;)

        Case &quot;复选框列表&quot;

            Server.Transfer(&quot;form8b.aspx&quot;)

        Case &quot;面板&quot;

            Server.Transfer(&quot;form9.aspx&quot;)

        Case Else

            Server.Transfer(&quot;form2.aspx&quot;)

    End Select

End Sub

End Class

我们的控制器非常简单。它根据 [action] 参数的值,将请求处理转交给相应的页面。与包含链接的 HTML 页面相比,它并没有提供额外的价值。不过,只需添加一个身份验证页面,就能展示其作用。如果用户需要通过身份验证(用户名、密码)才能访问应用程序,那么 [main.aspx] 控制器就是验证身份验证是否完成的好地方。