11. [SimuPaie] 应用程序 – 第 7 版 – ASP.NET / 多视图 / 多页面
推荐阅读:参考文献[1],《ASP.NET 1.1 Web开发》,章节:示例
我们现在正在研究一个版本,其功能与之前讨论的三层 ASP.NET 应用程序 [pam-v4-3tier-nhibernate-multivues-monopage] 完全相同,但我们将对其架构进行如下修改:在之前的版本中,视图由单个 ASPX 页面实现,而在这里,它们将由三个 ASPX 页面实现。
先前应用程序的架构如下:
![]() |
这里采用的是 MVC(模型-视图-控制器)架构:
- [Default.aspx.cs] 包含控制器代码。[Default.aspx] 页面是客户端的唯一交互入口,负责处理来自客户端的所有请求。
- [Saisies、Simulation、Simulations、...] 均为视图。这些视图通过在 [Default.aspx] 页面上使用 [View] 组件来实现。
新版本的架构将如下所示:
![]() |
- 仅 [web] 层发生变更
- 视图(即呈现给用户的内容)保持不变。
- 控制器代码——在上一版本中完全位于 [Default.aspx.cs] 中——现在分布在多个页面中:
- [MasterPage.master]:一个封装了不同视图中通用元素的页面:顶部横幅及其菜单选项
- [Formulaire.aspx]:用于显示模拟表单并管理该表单上操作的页面
- [Simulations.aspx]:显示模拟列表并处理该页面上操作的页面
- [Errors.aspx]:应用程序初始化发生错误时显示的页面。该页面不支持任何操作。
这可以视为多控制器 MVC 架构,而上一版本的架构则是单控制器 MVC 架构。
处理客户端请求遵循以下步骤:
- 客户端向应用程序发起请求。请求会发送到两个页面之一 [Formulaire.aspx, Simulations.aspx]。
- 被请求的页面处理该请求。为此,它可能需要 [业务] 层的协助,而如果需要与数据库交换数据,[业务] 层本身可能又需要 [DAO] 层的协助。应用程序从 [业务] 层接收响应。
- 基于该响应,应用程序选择 (3) 要发送给客户端的视图(即响应),并向其 (4) 提供所需的信息(即模型)。
- 响应被发送给客户端 (5)
11.1. 应用程序的视图
向用户展示的不同视图如下:
- - [VueSaisies] 视图,用于显示模拟表单

- - [VueSimulation] 视图,用于显示详细的模拟结果:

- - [SimulationView] 视图,用于列出客户端执行的模拟

- - [EmptySimulationsView] 视图,表示客户端没有模拟或已无更多模拟:

- [ErrorView] 视图,表示应用程序初始化错误:

11.2. 在多控制器环境中生成视图
在上一版本中,所有视图均由单个 [Default.aspx] 页面生成。该页面包含两个 [MultiView] 组件,而视图由属于这两个 [MultiView] 组件的一个或两个 [View] 组件组合而成。
虽然在视图较少时这种架构很有效,但一旦构成各种视图的组件数量变大,该架构就会达到极限:事实上,每次向单个 [Default.aspx] 页面发送请求时,其所有组件都会被实例化,尽管其中只有一部分会被用于生成给用户的响应。因此,每次新请求都会执行不必要的工作,当页面上的组件总数较大时,这便会成为瓶颈。
一种解决方案是将视图分布在不同的页面上。这就是我们在此要做的。让我们来考察两种不同的视图生成情况:
- 请求发送到页面 P1,由其生成响应
- 向页面 P1 发起请求,该页面请求页面 P2 生成响应
11.2.1. 情况 1:控制器/视图页面
在情况 1 中,我们回归到上一版本的单控制器架构,其中 [Default.aspx] 页面即为页面 P1:
![]() |
- 客户端向页面 P1 发起请求 (1)
- 页面 P1 处理此请求。为此,它可能需要 [业务] 层的协助(2),而该层若需与数据库交换数据,则可能需要 [DAO] 层的支持。应用程序从 [业务] 层接收响应。
- 基于该响应,它选择 (3) 要发送给客户端的视图(即响应),并向客户端 (4) 提供其所需的信息(模型)。这包括选择要在页面 P1 上显示的 [Panel] 或 [View] 组件,并初始化这些组件所包含的内容。
- 响应被发送至客户端(5)
以下是取自所研究应用程序的两个示例:
[Formulaire.aspx 页面]
![]() |
- 在 [1] 中:请求 [Formulaire.aspx] 页面后,用户请求了一次模拟
- 在 [2] 中:[Formulaire.aspx] 页面处理了此请求,并通过显示一个在 [1] 中未显示的 [View] 组件,自行生成了响应
[页面 Simulations.aspx]
![]() |
- 在 [1] 中:请求 [Simulations.aspx] 页面后,用户希望删除一个模拟
- 在 [2] 中:[Simulations.aspx] 页面处理了此请求,并通过重新显示新的模拟列表来生成响应。
![]() |
11.2.2. 情况 2:单控制器页面,控制器/视图页面
情况 2 涵盖多种架构。我们将选择以下方案:
![]() |
- 客户端向页面 P1 发起请求 (1)
- 页面 P1 处理此请求。为此,它可能需要 [业务] 层的协助(2),而该层若需与数据库交换数据,则可能需要 [DAO] 层的支持。应用程序从 [业务] 层接收响应。
- 基于此,它选择(3)要发送给客户端的视图(即响应),并通过提供(4)所需的信息(模板)来完成这一操作。在此情况下,待生成的视图必须由 P1 以外的页面创建——具体而言,是页面 P2。为了执行操作(3)和(4),页面 P1 有两种选择:
- 使用 [Server.Transfer("P2.aspx")] 操作将执行转移到页面 P2。在这种情况下,它可以将面向页面 P2 的模型放置在请求上下文中 [Context.Items["key"]=value] 或用户会话中 [Session.["key"]=value]。 随后页面 P2 将被实例化,当处理其 Load 事件时,例如,它可以使用 [value=(Type)Context.Items["key"] 或 [value=(Type)Session["key"] 操作来检索页面 P1 传递的信息,具体取决于情况,其中 Type 是与该键关联的值的类型。 如果不需要为未来的客户端请求保留模型值,则通过 Context 传输值最为合适。
- 要求客户端使用操作 [Response.Redirect("P2.aspx")] 重定向到页面 P2。 在这种情况下,由于请求上下文会在每次请求结束时被清除,因此页面 P1 会将本应提供给页面 P2 的模型放入会话中。然而,此处的重定向将导致客户端对 P1 的首次请求结束,并触发来自同一客户端的第二次请求,这次是针对 P2 的。存在两个连续的请求。我们知道会话是保留请求之间“记忆”的一种方式。除了会话之外,还有其他解决方案。
- 无论 P2 如何接管,我们随后将回到情况 1:P2 已收到一个请求并将其处理(5),并自行生成响应(6、7)。我们还可以设想,在处理完请求后,页面 P2 会将请求移交给页面 P3,以此类推。
以下是一个取自正在研究的应用程序的示例:
![]() |
- 在 [1] 中:请求 [Formulaire.aspx] 页面的用户要求查看 [2] 中的模拟列表
- 在[2]中:[Formulaire.aspx]页面处理此请求,并将客户端重定向至[Simulations.aspx]页面。正是后者向用户提供了响应。与要求客户端重定向不同,[Formulaire.aspx]页面本可以将客户端的请求转发至[Simulations.aspx]页面。 在[2]中的这种情况下,我们看到的URL将与[1]中相同。事实上,浏览器始终显示最后请求的URL:
- [1] 中请求的操作针对的是 [Formulaire.aspx] 页面。浏览器向该页面发送一个 POST 请求。
- 如果 [Formulaire.aspx] 页面处理该请求后,通过 [Server.Transfer("Simulations.aspx")] 将其转发至 [Simulations.aspx] 页面,则我们仍处于同一请求中。此时,浏览器将在 [2] 中显示 [Formulaire.aspx] 的 URL(即 POST 请求的发送目标)。
- 如果 [Formulaire.aspx] 页面处理请求后,通过 [Response.Redirect("Simulations.aspx")] 将其重定向至 [Simulations.aspx] 页面,则浏览器会发起第二个请求,即向 [Simulations.aspx] 发送的 GET 请求。 此时,浏览器将在 [2] 处显示该 GET 请求所发送的 [Simulations.aspx] 的 URL。这就是上文截图 [2] 所展示的内容。
11.3. [web] 层的 Visual Web Developer 项目
[web] 层的 Visual Web Developer 项目如下:
![]() |
- 在 [1] 中,我们发现:
- 应用程序的配置文件 [Web.config]——其内容与 [pam-v4-3tier-nhibernate-multivues-monopage] 应用程序的配置文件完全一致。
- [Default.aspx] 页面——仅将客户端重定向至 [Formulaire.aspx] 页面
- [Formulaire.aspx] 页面,向用户显示模拟表单并处理与该表单相关的操作
- [Simulations.aspx] 页面,用于显示用户的模拟列表并处理与该页面相关的操作
- [Errors.aspx] 页面,向用户显示一个页面,指示 Web 应用程序启动时遇到的错误。
- 在 [2] 中,我们可以看到项目引用。
让我们回到新项目的架构:
![]() |
与 [pam-v4-3tier-nhibernate-multivues-monopage] 项目相比,仅视图部分发生了变化。这就是为什么新项目复用了该项目中的部分文件:
- 配置文件 [Web.config]
- 引用的 DLL 文件 [pam-dao-nhibernate, pam-metier-dao-nhibernate, Spring.Core, NHibernate]
- 全局应用程序类 [Global.asax]
- 文件夹 [images, resources, pam]
为了与当前正在开发的项目保持一致,我们将确保视图和全局应用程序类的命名空间为 [pam-v7]:
![]() |
11.4. 页面呈现代码
11.4.1. 母版页 [MasterPage.master]
第 11.1 节中介绍的应用程序视图包含一些共同元素,这些元素可以提取到主页面中,在 Visual Studio 中称为“主页面”。以下文中的 [VueSaisies] 和 [VueSimulationsVides] 视图为例,它们分别由页面 [Formulaire.aspx] 和 [Simulations.aspx] 生成:
![]() |
这两个视图共享顶部横幅(标题和菜单选项)。所有将呈现给用户的视图都是如此:它们都将拥有相同的顶部横幅。为了使不同的页面能够共享相同的呈现片段,有多种解决方案,包括以下方法:
- 将此通用片段放置在用户控件中。这是 ASP.NET 1.1 中的主要技术
- 将该通用片段放置在母版页中。该技术始于 ASP.NET 2.0。这也是我们在此采用的方法。
要在 Web 应用程序中创建母版页,请按照以下步骤操作:
- 右键单击项目 / 添加新项 / 母版页:
![]() |
添加母版页后,Web 应用程序中默认会生成三个文件:
- [MasterPage.master]:母版页的布局代码
- [MasterPage.master.cs]:主页面的控件代码
- [MasterPage.Master.Designer.cs]:主页面的组件声明
Visual Studio 在 [MasterPage.master] 中生成的代码如下:
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="MasterPage.master.cs" Inherits="pam_v7.MasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
<asp:ContentPlaceHolder id="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ContentPlaceHolder id="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
- 第 1 行:<%@ Master ... %> 标签用于将该页面定义为主页面。页面的控件代码将位于由 CodeBehind 属性指定的文件中,且该页面将继承自由 Inherits 属性指定的类。
- 第 12–18 行:母版页表单
- 第 14–16 行:一个空容器,在我们的应用程序中,它将容纳其中一个页面 [Form.aspx、Simulations.aspx、Errors.aspx]。 客户端收到的响应始终是同一页面——即母版页——其中 [ContentPlaceHolder1] 容器将接收由 [Form.aspx、Simulations.aspx、Errors.aspx] 中的某个页面提供的 HTML 流。因此,要更改发送给客户端的页面外观,只需更改母版页的外观即可。
- 第 8–9 行:一个空容器,允许“子”页面自定义页眉 <head>...</head>。
该源代码的可视化表示(“设计”选项卡)如下图 (1) 所示。此外,您还可以通过 [标准] 工具栏中的 [ContentPlaceHolder] 组件 (2) 添加任意数量的容器。
![]() |
Visual Studio 在 [MasterPage.master.cs] 中生成的控件代码如下:
using System;
public partial class MasterPage : System.Web.UI.MasterPage
{
protected void Page_Load(object sender, EventArgs e)
{
}
}
- 第 3 行:[MasterPage.master] 页面中 <%@ Master ... %> 指令的 [Inherits] 属性所引用的类继承自 [System.Web.UI.MasterPage] 类
在上文中,我们可以看到 Page_Load 方法的存在,它负责处理母版页的 Load 事件。母版页内部将包含另一个页面。这两个页面的 Load 事件将按什么顺序发生?这是一条通用规则:组件的 Load 事件发生在其容器的 Load 事件之前。因此,在此处,嵌入母版页中的页面的 Load 事件将先于母版页本身的 Load 事件发生。
要在 中生成一个将前面的[MasterPage.master]页面用作其母版页的页面,可以按以下步骤操作:
![]() |
- 在 [1] 中:右键单击母版页,然后选择 [添加内容页]
- 在 [2]:系统将生成一个默认页面,本例中为 [WebForm1.aspx]。
[WebForm1.aspx] 的呈现代码如下:
<%@ Page Title="" Language="C#" MasterPageFile="~/MasterPage.Master" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="pam_v7.WebForm1" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
</asp:Content>
- 第 1 行:Page 指令及其属性
- MasterPageFile:指定该指令所描述页面的母版页文件。~ 符号表示项目文件夹。
- 其余参数均为 ASP 网页的标准参数
- 第 2–3 行:<asp:Content> 标签通过 ContentPlaceHolderID 属性依次与母版页中的 <asp:ContentPlaceHolder> 指令建立关联。上述第 2–3 行之间的组件将在运行时被放置到母版页上的 ContentPlaceHolder1 容器中。
通过重命名以此方式生成的页面 [WebForm1.aspx],我们可以使用 [MasterPage.master] 作为母版页来构建各种页面。
对于我们的 [SimuPaie] 应用程序,母版页的外观将如下所示:
![]() |
编号 | 类型 | 名称 | 角色 |
面板(上方的粉色部分) | 页眉 | 页面页眉 | |
面板(上图为黄色) | 内容 | 页面内容 | |
链接按钮 | LinkButtonRunSimulation | 请求进行模拟计算 | |
链接按钮 | LinkButtonClearSimulation | 清除输入表单 | |
链接按钮 | LinkButtonViewSimulations | 显示已执行的模拟列表 | |
链接按钮 | LinkButtonSimulationForm | 返回输入表单 | |
LinkButton | LinkButtonSaveSimulation | 将当前模拟保存到模拟列表中 | |
链接按钮 | LinkButtonEndSession | 结束当前会话 |
相应的源代码如下:
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="MasterPage.master.cs" Inherits="pam_v7.MasterPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>Application PAM</title>
</head>
<body background="ressources/standard.jpg">
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true" />
<asp:UpdatePanel runat="server" ID="UpdatePanelPam" UpdateMode="Conditional">
<ContentTemplate>
<asp:Panel ID="entete" runat="server" BackColor="#FFE0C0">
<table>
<tr>
<td>
<h2>
Simulateur de calcul de paie</h2>
</td>
<td>
<label>
 </label>
<asp:UpdateProgress ID="UpdateProgress1" runat="server">
<ProgressTemplate>
<img alt="" src="images/indicator.gif" />
<asp:Label ID="Label5" runat="server" BackColor="#FF8000"
EnableViewState="False" Text="Calcul en cours. Patientez ....">
</asp:Label>
</ProgressTemplate>
</asp:UpdateProgress>
</td>
<td>
<asp:LinkButton ID="LinkButtonFaireSimulation" runat="server"
CausesValidation="False">| Faire la simulation<br />
</asp:LinkButton>
<asp:LinkButton ID="LinkButtonEffacerSimulation" runat="server"
CausesValidation="False">| Effacer la simulation<br />
</asp:LinkButton>
<asp:LinkButton ID="LinkButtonVoirSimulations" runat="server"
CausesValidation="False">| Voir les simulations<br />
</asp:LinkButton>
<asp:LinkButton ID="LinkButtonFormulaireSimulation" runat="server"
CausesValidation="False">| Retour au formulaire de simulation<br />
</asp:LinkButton>
<asp:LinkButton ID="LinkButtonEnregistrerSimulation" runat="server"
CausesValidation="False">| Enregistrer la simulation<br />
</asp:LinkButton>
<asp:LinkButton ID="LinkButtonTerminerSession" runat="server"
CausesValidation="False">| Terminer la session<br />
</asp:LinkButton>
</td>
</tr>
</table>
<hr />
</asp:Panel>
<div>
<asp:Panel ID="contenu" runat="server" BackColor="#FFFFC0">
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</asp:Panel>
</div>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
- 第 1 行:请注意母版页类的名称:MasterPage
- 第 8 行:为页面定义背景图像。
- 第 9–64 行:表单
- 第 10 行:实现 Ajax 效果所需的 ScriptManager 组件
- 第 11–63 行:AJAX 容器
- 第 12–62 行:支持 Ajax 的内容
- 第 13–55 行:Panel 组件 [header]
- 第 57–60 行:Panel 组件 [content]
- 第 58–59 行:[ContentPlaceHolder1] 组件,将包含封装的页面 [Formulaire.aspx, Simulations.aspx, Erreurs.aspx]
要构建此页面,我们可以将 [pam-v4-3tier-nhibernate-multivues-monopage] 版本中 [Default.aspx] 页面 [HeaderView] 视图的 ASPX 代码插入到 [header] 面板中,具体内容详见第 8.5.2 节。
11.4.2. [Formulaire.aspx] 页面
要生成此页面,请按照第 11.4.1 节所述的方法操作,并将生成的 [WebForm1.aspx] 页面重命名为 [Form.aspx]。正在构建的 [Form.aspx] 页面的视觉外观如下:
![]() |
[Formulaire.aspx] 页面的视觉外观由两个元素组成:
- [1] 主页面及其 [ContentPlaceHolder1] 容器 (2)
- [2] 放置在 [ContentPlaceHolder1] 容器中的组件。这些组件与上一应用程序中的完全相同。
该页面的源代码如下:
<%@ Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true"
CodeBehind="Formulaire.aspx.cs" Inherits="pam_v7.PageFormulaire" Title="Simulation de calcul de paie : formulaire" %>
<%@ MasterType VirtualPath="~/MasterPage.master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<div>
<table>
<tr>
<td>
Employé
</td>
<td>
Heures travaillées
</td>
<td>
Jours travaillés
</td>
<td>
</td>
</tr>
...
</asp:Content>
- 第 1 行:带有 MasterPageFile 属性的 Page 指令
- 第 4 行:母版页控件类可以公开公共字段和属性。封装的页面可以通过 Master.[field] 或 Master.[property] 语法访问这些字段和属性。页面的 Master 属性将母版页视为 [System.Web.UI.MasterPage] 类型的实例。因此,在我们的示例中,实际上应写为 (MasterPage)(Master).[field] 或 (MasterPage)(Master).[property]。若在页面中插入第 4 行中的 MasterType 指令,即可避免这种类型转换。该指令的 VirtualPath 属性指定了母版页文件。这样,编译器就能识别母版页类(本例中为 [MasterPage] 类型)所公开的公共字段、属性和方法。
- 第 5–22 行:将插入到母版页 [ContentPlaceHolder1] 容器中的内容。
可以通过将该页面的内容(第 6–21 行)设置为第 8.5.3 节中描述的 [VueSaisies] 视图以及第 8.5.4 节中描述的 [VueSimulation] 视图的内容来构建此页面。
11.4.3. [Simulations.aspx] 页面
要生成此页面,请按照第 11.4.1 节所述的方法操作,并将生成的 [WebForm1.aspx] 页面重命名为 [Simulations.aspx]。当前正在构建的 [Simulations.aspx] 页面的外观如下:
![]() |
[Simulations.aspx] 页面的视觉外观由两个元素组成:
- [1] 主页面及其 [ContentPlaceHolder1] 容器
- 在 [2] 处,将组件放置在 [ContentPlaceHolder1] 容器中。这些组件与上一应用程序中的完全相同。
该页面的源代码如下:
<%@ Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true"
CodeBehind="Simulations.aspx.cs" Inherits="pam_v7.PageSimulations" Title="Pam : liste des simulations" %>
<%@ MasterType VirtualPath="~/MasterPage.master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
<asp:MultiView ID="MultiView1" runat="server">
<asp:View ID="View1" runat="server">
<h2>
Liste de vos simulations</h2>
<p>
<asp:GridView ID="GridViewSimulations" runat="server" ...>
...
</asp:GridView>
</p>
</asp:View>
<asp:View ID="View2" runat="server">
<h2>
La liste de vos simulations est vide</h2>
</asp:View>
</asp:MultiView><br />
</asp:Content>
我们可以将该页面的内容(第 5–21 行)设置为第 8.5.5 节中描述的 [VueSimulations] 视图以及第 8.5.6 节中描述的 [VueSimulationsVides] 视图的内容,从而构建此页面。
11.4.4. [Errors.aspx] 页面
要生成此页面,请按照第 11.4.1 节所述的方法操作,并将生成的 [WebForm1.aspx] 页面重命名为 [Errors.aspx]。当前正在构建的 [Errors.aspx] 页面的外观如下:
![]() |
[Errors.aspx] 页面的视觉外观由两个元素组成:
- [1] 主页面及其 [ContentPlaceHolder1] 容器
- [2] 放置在 [ContentPlaceHolder1] 容器中的组件。这些组件与上一应用程序中的完全相同。
该页面的源代码如下:
<%@ Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true"
CodeBehind="Erreurs.aspx.cs" Inherits="pam_v7.PageErreurs" Title="Pam : erreurs" %>
<%@ MasterType VirtualPath="~/MasterPage.master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<h3>Les erreurs suivantes se sont produites au démarrage de l'application</h3>
<ul>
<asp:Repeater id="rptErreurs" runat="server">
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
</asp:Repeater>
</ul>
</asp:Content>
11.5. 页面控件代码
11.5.1. 概述
让我们回到应用程序架构:
![]() |
- [Global] 是负责初始化(步骤 0)应用程序的 [HttpApplication] 对象。该类与上一版本中的完全相同。
- 控制器代码在上一版本中完全位于 [Default.aspx.cs] 中,现在则分布在多个页面中:
- [MasterPage.master]:[Form.aspx、Simulations.aspx、Errors.aspx] 的母版页。其中包含菜单。
- [Formulaire.aspx]:用于显示模拟表单并处理该表单上操作的页面
- [Simulations.aspx]:显示模拟列表并处理该页面上操作的页面
- [Errors.aspx]:应用程序初始化发生错误时显示的页面。该页面不支持任何操作。
处理客户端请求遵循以下步骤:
- 用户向应用程序发起请求。通常,用户会在 [Form.aspx] 或 [Simulations.aspx] 其中一个页面上进行操作,但用户也可以请求 [Errors.aspx] 页面。必须考虑到这种情况。
- 被请求的页面处理该请求(步骤 1)。为此,它可能需要 [业务] 层的协助(步骤 2),而该层若需与数据库交换数据,则可能需要 [DAO] 层的协助。应用程序从 [业务] 层接收响应。
- 基于此,它选择(步骤 3)要发送给客户端的视图(即响应),并为其(步骤 4)提供所需的信息(模型)。我们已经看到了生成此响应的三种可能性:
- 请求的页面(D)即为响应中发送的页面(R)。构建响应模型(R)的过程,即为将页面(D)的某些组件赋予其在响应中必须具备的值。
- 请求的页面 (D) 并非作为响应发送的页面 (R)。此时页面 (D) 可以:
- 使用 Server.Transfer(" R ") 语句将执行流程转移至页面 (R)。随后可通过 Context.Items("key")=value 将模型放入上下文中,或较少见地通过 Session.Items("key")=value 放入会话中
- 使用 Response.redirect(" R ") 语句将客户端重定向至页面 (R)。此时模型可存入会话,但无法存入上下文。
- 响应被发送给客户端(步骤 5)
每个页面 [MasterPage.master, Form.aspx, Simulations.aspx, Errors.aspx] 都会响应以下一个或多个事件:
- Init:页面生命周期中的首个事件
- Load:页面加载时触发
- Click:用户点击母版页菜单中的某个链接时触发
我们依次处理页面,从母版页开始。
11.5.2. [MasterPage.master] 页面的控件代码
11.5.2.1. 类骨架
主页面的控件代码具有以下框架:
using System.Web.UI.WebControls;
namespace pam_v7
{
public partial class MasterPage : System.Web.UI.MasterPage
{
// the menu
public LinkButton OptionFaireSimulation
{
get { return LinkButtonFaireSimulation; }
}
...
// set menu
public void SetMenu(bool boolFaireSimulation, bool boolEnregistrerSimulation, bool boolEffacerSimulation, bool boolFormulaireSimulation, bool boolVoirSimulations, bool boolTerminerSession)
{
....
}
// managing the [End session] option
protected void LinkButtonTerminerSession_Click(object sender, System.EventArgs e)
{
....
}
// init master page
protected void Page_Init(object sender, System.EventArgs e)
{
....
}
}
}
}
- 第 5 行:该类命名为 [MasterPage],并继承自系统类 [System.Web.UI.MasterPage]。
- 第 9–14 行:6 个菜单选项作为该类的公共属性对外暴露
- 第 16–19 行:public SetMenu 方法允许页面 [Formulaire.aspx、Simulations.aspx、Erreurs.aspx] 设置母版页菜单
- 第 22–25 行:处理 [LinkButtonTerminerSession] 链接点击事件的程序
- 第 28–31 行:处理母版页 Init 事件的程序
11.5.2.2. 类的公共属性
using System.Web.UI.WebControls;
namespace pam_v7
{
public partial class MasterPage : System.Web.UI.MasterPage
{
// the menu
public LinkButton OptionFaireSimulation
{
get { return LinkButtonFaireSimulation; }
}
public LinkButton OptionEffacerSimulation
{
get { return LinkButtonEffacerSimulation; }
}
public LinkButton OptionEnregistrerSimulation
{
get { return LinkButtonEnregistrerSimulation; }
}
public LinkButton OptionVoirSimulations
{
get { return LinkButtonVoirSimulations; }
}
public LinkButton OptionTerminerSession
{
get { return LinkButtonTerminerSession; }
}
public LinkButton OptionFormulaireSimulation
{
get { return LinkButtonFormulaireSimulation; }
}
...
}
}
要理解这段代码,你需要回顾构成母版页的各个组件:
![]() |
编号 | 类型 | 名称 | 角色 |
面板(上方的粉色部分) | 页眉 | 页面页眉 | |
面板(上图为黄色) | 内容 | 页面内容 | |
链接按钮 | 链接按钮运行模拟 | 请求模拟计算 | |
链接按钮 | LinkButtonClearSimulation | 清除输入表单 | |
链接按钮 | LinkButtonViewSimulations | 显示已执行的模拟列表 | |
链接按钮 | LinkButtonSimulationForm | 返回输入表单 | |
LinkButton | LinkButtonSaveSimulation | 将当前模拟保存到模拟列表中 | |
链接按钮 | LinkButtonEndSession | 结束当前会话 |
组件 1 至 6 在包含它们的页面之外不可访问。第 9 行至第 37 行中的属性旨在使其可供外部类访问,在本例中即应用程序其他页面的类。
11.5.2.3. SetMenu 方法
公共 SetMenu 方法允许页面 [Formulaire.aspx、Simulations.aspx、Erreurs.aspx] 设置母版页的菜单。其代码非常简单:
// fixer le menu
public void SetMenu(bool boolFaireSimulation, bool boolEnregistrerSimulation, bool boolEffacerSimulation, bool boolFormulaireSimulation, bool boolVoirSimulations, bool boolTerminerSession)
{
// on fixe les options de menu
LinkButtonFaireSimulation.Visible = boolFaireSimulation;
LinkButtonEnregistrerSimulation.Visible = boolEnregistrerSimulation;
LinkButtonEffacerSimulation.Visible = boolEffacerSimulation;
LinkButtonVoirSimulations.Visible = boolVoirSimulations;
LinkButtonFormulaireSimulation.Visible = boolFormulaireSimulation;
LinkButtonTerminerSession.Visible = boolTerminerSession;
}
11.5.2.4. 主页面上的事件处理
母版页将处理两个事件:
- Init 事件,即页面生命周期中的第一个事件
- [LinkButtonTerminerSession] 链接上的 Click 事件
母版页还有其他五个链接:[LinkButtonRunSimulation、LinkButtonSaveSimulation、LinkButtonClearSimulation、LinkButtonViewSimulations、LinkButtonSimulationForm]。以[LinkButtonRunSimulation]链接为例,让我们看看点击该链接时需要执行哪些操作:
- 验证 [Form.aspx] 页面上输入的数据(小时、天数)
- 计算薪资
- 在 [Form.aspx] 页面上显示结果
步骤 1 和 3 需要访问 [Form.aspx] 页面上的控件。但实际情况并非如此。事实上,母版页并不知道可能被插入到其 [ContentPlaceHolder1] 容器中的页面上有哪些控件。 在本示例中,[Formulaire.aspx] 页面需要处理 [LinkButtonFaireSimulation] 链接的点击事件,因为该事件发生时显示的正是该页面。它如何获知此事件?
- 由于 [LinkButtonFaireSimulation] 链接不属于 [Formulaire.aspx] 页面,因此我们无法在 [Formulaire.aspx] 中编写常规的处理程序:
private void LinkButtonFaireSimulation_Click(object sender, System.EventArgs e)
{
...
}
您可以在 [Formulaire.aspx] 中使用以下代码来解决此问题:
using System.Collections.Generic;
...
namespace pam_v7
{
public partial class Formulaire : System.Web.UI.Page
{
// page loading
protected void Page_Load(object sender, System.EventArgs e)
{
// event manager
Master.OptionFaireSimulation.Click += OptFaireSimulation_Click;
Master.OptionEffacerSimulation.Click += OptEffacerSimulation_Click;
Master.OptionVoirSimulations.Click += OptVoirSimulations_Click;
Master.OptionEnregistrerSimulation.Click += OptEnregistrerSimulation_Click;
...
}
// payroll calculation
private void OptFaireSimulation_Click(object sender, System.EventArgs e)
{
....
}
// delete simulation
private void OptEffacerSimulation_Click(object sender, System.EventArgs e)
{
...
}
protected void OptVoirSimulations_Click(object sender, System.EventArgs e)
{
...
}
protected void OptEnregistrerSimulation_Click(object sender, System.EventArgs e)
{
...
}
}
}
- 第 12–15 行:当 [Formulaire.aspx] 页面的 Load 事件发生时,母版页的 [MasterPage] 类已被实例化。其 public Optionxx 属性可被访问,且类型为 LinkButton,这是一个支持 Click 事件的组件。我们将以下方法与这些 Click 事件关联:
- OptFaireSimulation_Click 用于 LinkButtonFaireSimulation 链接的 Click 事件
- OptEffacerSimulation_Click 用于 LinkButtonEffacerSimulation 链接的 Click 事件
- OptVoirSimulations_Click:用于 LinkButtonVoirSimulations 链接的 Click 事件
- OptEnregistrerSimulation_Click:用于 LinkButtonEnregistrerSimulation 链接的 Click 事件
六个菜单链接的点击事件处理将按以下方式分配:
- [Formulaire.aspx] 页面将处理 [LinkButtonRunSimulation、LinkButtonSaveSimulation、LinkButtonClearSimulation、LinkButtonViewSimulations] 链接
- [Simulations.aspx] 页面将处理 [LinkButtonSimulationForm] 链接
- 母版页 [MasterPage.master] 将处理 [LinkButtonEndSession] 链接。对于此事件,它无需知道其封装的是哪个页面。
11.5.2.5. 母版页的 Init 事件
应用程序的三个页面 [Form.aspx、Simulations.aspx、Errors.aspx] 使用 [MasterPage.master] 作为其母版页。我们将母版页称为 M,被封装的页面称为 E。当客户端请求页面 E 时,将按以下顺序发生以下事件:
- E.Init
- M.Init
- E.Load
- M.Load
- ...
我们将利用页面 M 的 Init 事件来执行应尽早运行的代码,无论目标页面 E 是哪个。为了确定这段代码,让我们回顾一下应用程序概述:
![]() |
上文中的 [Global] 是用于初始化应用程序的 [HttpApplication] 对象。该类与 [pam-v4-3tier-nhibernate-multivues-monopage] 版本中的类相同:
using System;
...
namespace pam_v7
{
public class Global : System.Web.HttpApplication
{
// --- static application data ---
public static Employe[] Employes;
public static IPamMetier PamMetier = null;
public static string Msg;
public static bool Erreur = false;
// application startup
public void Application_Start(object sender, EventArgs e)
{
...
}
public void Session_Start(object sender, EventArgs e)
{
...
}
}
}
如果 [Global] 类无法正确初始化应用程序,它会设置两个静态公共变量:
- 第 12 行中的布尔变量 Error 被设置为 true
- 第 11 行上的 `Msg` 变量包含一条提供所遇错误详细信息的消息
当应用程序尚未正确初始化时,如果用户请求了 [Form.aspx] 或 [Simulations.aspx] 中的某个页面,该请求必须被转发或重定向至 [Errors.aspx] 页面,该页面将显示来自 [Global] 类的错误消息。处理这种情况有以下几种方法:
- 在每个页面 [Formulaire.aspx、Simulations.aspx] 的 Init 或 Load 事件处理程序中执行初始化错误检查
- 在上述两个页面的母版页的 Init 或 Load 事件处理程序中执行初始化错误检查。此方法的优势在于将初始化错误检查集中于单一位置。
我们选择在母版页的 Init 事件处理程序中执行初始化错误检查:
protected void Page_Init(object sender, System.EventArgs e)
{
// event manager
LinkButtonTerminerSession.Click += LinkButtonTerminerSession_Click;
// initialization errors?
if (Global.Erreur)
{
// is the encapsulated page the error page?
bool isPageErreurs =...;
// if the error page is displayed, leave it alone, otherwise redirect the client to the error page
if (!isPageErreurs)
Response.Redirect("Erreurs.aspx");
return;
}
}
一旦请求了 [Form.aspx、Simulations.aspx、Errors.aspx] 中的任意一个页面,上述代码就会立即执行。如果请求的页面是 [Form.aspx] 或 [Simulations.aspx],我们只需(第 12 行)将客户端重定向到 [Errors.aspx] 页面,该页面会显示来自 [Global] 类的错误消息。 如果请求的页面是 [Errors.aspx],则不应发生此重定向:必须显示 [Errors.aspx] 页面。因此,我们需要在母版页的 [Page_Init] 方法中确定它所封装的是哪个页面。
让我们重新审视母版页的组件树:
...
<body background="ressources/standard.jpg">
<form id="form1" runat="server">
<asp:Panel ID="entete" runat="server" BackColor="#FFE0C0" Width="1239px" >
...
</asp:Panel>
<div>
<asp:Panel ID="contenu" runat="server" BackColor="#FFFFC0">
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</asp:Panel>
</div>
</form>
</body>
</html>
- 第 1-13 行:ID 为 "form1" 的容器
- 第 4-6 行:ID 为 "entete" 的容器,包含在 ID 为 "form1" 的容器中
- 第 8-11 行:ID 为 "content" 的容器,包含在 ID 为 "form1" 的容器中
- 第 9-10 行:ID 为 "ContentPlaceHolder1" 的容器,包含在 ID 为 "content" 的容器中
嵌入主页面 M 中的页面 E 位于 ID 为 "ContentPlaceHolder1" 的容器内。若要引用该页面 E 上 ID 为 C 的组件,应编写如下代码:
this.FindControl("form1").FindControl("contenu").FindControl("ContentPlaceHolder1").FindControl("C");
[Errors.aspx] 页面的组件树如下:
<%@ Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true"
CodeBehind="Erreurs.aspx.cs" Inherits="pam_v7.PageErreurs" Title="Pam : erreurs" %>
<%@ MasterType VirtualPath="~/MasterPage.master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<h3>Les erreurs suivantes se sont produites au démarrage de l'application</h3>
<ul>
<asp:Repeater id="rptErreurs" runat="server">
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
</asp:Repeater>
</ul>
</asp:Content>
当 [Errors.aspx] 页面与 M 母版页合并时,上述 <asp:Content> 标签(第 5–16 行)的内容会被整合到 M 页面上 ID 为 "ContentPlaceholder1" 的 <asp:ContentPlaceHolder> 标签中,从而形成以下组件树:
- 第 12 行:[rptErrors] 组件可用于判断母版页 M 是否包含 [Errors.aspx] 页面。该组件仅存在于此页面上。
这些说明足以理解母版页中 [Page_Init] 过程的代码:
protected void Page_Init(object sender, System.EventArgs e)
{
// event manager
LinkButtonTerminerSession.Click += LinkButtonTerminerSession_Click;
// initialization errors?
if (Global.Erreur)
{
// is the encapsulated page the error page?
bool isPageErreurs = this.FindControl("form1").FindControl("contenu").FindControl("ContentPlaceHolder1").FindControl("rptErreurs") != null;
// if the error page is displayed, leave it alone, otherwise redirect the client to the error page
if (!isPageErreurs)
Response.Redirect("Erreurs.aspx");
return;
}
}
- 第 4 行:我们将一个事件处理程序与 LinkButtonTerminerSession 链接的 Click 事件关联起来。该处理程序位于 MasterPage 类中。
- 第 6 行:我们检查 [Global] 类是否已设置其 Error 布尔值
- 第 9 行:如果是,则 IsPageErrors 布尔变量表示主页面中封装的页面是否为 [Errors.aspx] 页面
- 第 12 行:如果母版页中封装的页面不是 [Errors.aspx] 页面,则将客户端重定向到该页面;否则,不执行任何操作。
11.5.2.6. [LinkButtonTerminerSession] 链接的 Click 事件
![]() |
当用户点击上文视图 (1) 中的 [End Session] 链接时,必须清除会话中的内容,并显示一个空表单 (2)。
该事件处理程序的代码如下:
protected void LinkButtonTerminerSession_Click(object sender, System.EventArgs e)
{
// quit session
Session.Abandon();
// the [form] view is displayed
Response.Redirect("Formulaire.aspx");
}
- 第 4 行:当前会话被放弃
- 第 6 行:客户端被重定向到 [Form.aspx] 页面
我们可以看到,这段代码并未涉及 [Form.aspx、Simulations.aspx、Errors.aspx] 页面中的任何组件。因此,该事件可由母版页本身进行处理。
11.5.3. [Errors.aspx] 页面的控件代码
[Errors.aspx] 页面的控件代码可以如下所示:
using System.Collections.Generic;
namespace pam_v7
{
public partial class Erreurs : System.Web.UI.Page
{
protected void Page_Load(object sender, System.EventArgs e)
{
// initialization errors?
if (Global.Erreur)
{
// prepare the template for the [errors] page
List<string> erreursInitialisation = new List<string>();
erreursInitialisation.Add(Global.Msg);
// associate the error list with its component
rptErreurs.DataSource = erreursInitialisation;
rptErreurs.DataBind();
}
// set the menu
Master.SetMenu(false, false, false, false, false, false);
}
}
}
请记住,[Errors.aspx] 页面的唯一作用是在发生应用程序初始化错误时显示该错误:
- 第 10 行:检查初始化是否以错误结束
- 第 13–14 行:如果是,则将错误消息 (Global.Msg) 放入列表 [InitializationErrors] 中
- 第 16–17 行:指示 [rptErrors] 组件显示此列表
- 第 20 行:无论是否发生错误,主页面菜单选项均不显示,因此用户无法从该页面发起任何新操作。
如果用户直接请求 [Errors.aspx] 页面(在应用程序的正常使用过程中,用户不应这样做),会发生什么情况?通过查看 [MasterPage.master.cs] 和 [Errors.aspx.cs] 中的代码,我们会发现:
- 如果发生初始化错误,则显示该页面
- 如果没有初始化错误,用户将收到一个仅包含 [MasterPage.master] 中页眉、且不显示任何菜单选项的页面。
11.5.4. [Formulaire.aspx] 页面的控件代码
11.5.4.1. 类骨架
[Form.aspx] 页面的控件代码骨架可能如下所示:
using Pam.Metier.Entites;
...
partial class PageFormulaire : System.Web.UI.Page
{
// page loading
protected void Page_Load(object sender, System.EventArgs e)
{
// event manager
Master.OptionFaireSimulation.Click += OptFaireSimulation_Click;
Master.OptionEffacerSimulation.Click += OptEffacerSimulation_Click;
Master.OptionVoirSimulations.Click += OptVoirSimulations_Click;
Master.OptionEnregistrerSimulation.Click += OptEnregistrerSimulation_Click;
....
}
// payroll calculation
private void OptFaireSimulation_Click(object sender, System.EventArgs e)
{
....
}
// delete simulation
private void OptEffacerSimulation_Click(object sender, System.EventArgs e)
{
...
}
protected void OptVoirSimulations_Click(object sender, System.EventArgs e)
{
....
}
protected void OptEnregistrerSimulation_Click(object sender, System.EventArgs e)
{
...
}
}
[Formulaire.aspx] 页面的控件代码处理五个事件:
- 页面的 Load 事件
- 母版页上 [LinkButtonRunSimulation] 链接的 Click 事件
- 母版页上 [LinkButtonClearSimulation] 链接的 Click 事件
- 母版页上 [LinkButtonEnregistrerSimulation] 链接的 Click 事件
- 母版页上 [LinkButtonViewSimulations] 链接的 Click 事件
11.5.4.2. 页面加载事件
页面加载事件处理程序的骨架代码如下:
protected void Page_Load(object sender, System.EventArgs e)
{
// event manager
Master.OptionFaireSimulation.Click += OptFaireSimulation_Click;
Master.OptionEffacerSimulation.Click += OptEffacerSimulation_Click;
Master.OptionVoirSimulations.Click += OptVoirSimulations_Click;
Master.OptionEnregistrerSimulation.Click += OptEnregistrerSimulation_Click;
// view [entries] display
...
// menu positioning master page
...
// query processing GET
if (!IsPostBack)
{
// loading employee names into the combo
...
// init view [entries] with entries stored in the session if they exist
....
}
}
以下是一个用于阐明第17行注释的示例:
![]() |
![]() |
- 在[1]中,我们要求查看模拟列表。已将条目记录在[A, B, C]中。
- 在[2]中,我们查看该列表
- 在[3]中,我们要求返回表单
- 在[4]中,表单显示状态与离开时完全一致。由于存在两个请求,即(1,2)和(3,4),这意味着:
- 从 [1] 跳转到 [2] 时,[1] 中的输入内容已被保存
- 从 [3] 跳转到 [4] 时,这些数据被恢复。正是 [Form.aspx] 中的 [Page_Load] 过程执行了这一恢复操作。
问题:请参考 [pam-v4-3tier-nhibernate-multivues-monopage] 版本中的注释和代码,完成 Page_Load 过程
11.5.4.3. 处理菜单链接的点击事件
母版页上链接的点击事件处理程序的骨架如下:
// payroll calculation
private void OptFaireSimulation_Click(object sender, System.EventArgs e)
{
// ajax effect
Thread.Sleep(3000);
// valid page?
Page.Validate();
if (!Page.IsValid)
{
// view display [input]
...
}
// the page is validated - inputs are retrieved
...
// we calculate the employee's salary
FeuilleSalaire feuillesalaire;
try
{
feuillesalaire = ...;
}
catch (PamException ex)
{
// we encountered a problem
...
return;
}
// put the result in the session
Session["simulation"] = ...;
// put the entries in the session
...
// display
...
// views display
...
// display menu MasterPage
...
}
// delete simulation
private void OptEffacerSimulation_Click(object sender, System.EventArgs e)
{
// display panel [input]
...
// selection 1st employee
...
}
protected void OptVoirSimulations_Click(object sender, System.EventArgs e)
{
// put the entries in the session
...
// the [simulations] view is displayed
Response.Redirect("simulations.aspx");
}
protected void OptEnregistrerSimulation_Click(object sender, System.EventArgs e)
{
// save the current simulation in the user's session
...
// the [simulations] view is displayed
Response.Redirect("simulations.aspx");
}
问题:请参考 [pam-v4-3tier-nhibernate-multivues-monopage] 版本中的注释和代码,完成上述过程的代码
11.5.5. [Simulations.aspx] 页面的控件代码
[Simulations.aspx] 页面的控件代码框架可如下所示:
using System.Collections.Generic;
using Pam.Web;
using System.Web.UI.WebControls;
partial class PageSimulations : System.Web.UI.Page
{
// simulations
private List<Simulation> simulations;
// page loading
protected void Page_Load(object sender, System.EventArgs e)
{
// event manager
Master.OptionFormulaireSimulation.Click += OptFormulaireSimulation_Click;
GridViewSimulations.RowDeleting += GridViewSimulations_RowDeleting;
// simulations are retrieved from the
simulations = ...;
// are there any simulations?
if (simulations.Count != 0)
{
// first view visible
...
// fill the gridview
...
}
else
{
// second view
...
}
// set the menu
...
}
protected void GridViewSimulations_RowDeleting(object sender, System.Web.UI.WebControls.GridViewDeleteEventArgs e)
{
// simulations are retrieved from the
List<Simulation> simulations = ...;
// delete the designated simulation (e.RowIndex represents the number of the deleted line in the gridview)
..
// are there any simulations left?
if (simulations.Count != 0)
{
// fill the gridview
...
}
else
{
// view [SimulationsVides]
...
}
}
protected void OptFormulaireSimulation_Click(object sender, System.EventArgs e)
{
// the [form] view is displayed
Response.Redirect("formulaire.aspx");
}
}
问题:请参考 [pam-v4-3tier-nhibernate-multivues-monopage] 版本中的注释和代码,完成上述过程的代码
11.5.6. [Default.aspx] 页面的控件代码
您可以在应用程序中包含一个 [Default.aspx] 页面,以便用户在请求应用程序 URL 时无需指定具体页面,如下所示:
![]() |
请求 [1] 收到了页面 [Formulaire.aspx] (2) 作为响应。我们知道请求 (1) 默认由应用程序的 [Default.aspx] 页面处理。要获得 (2),[Default.aspx] 只需将客户端重定向到 [Formulaire.aspx] 页面即可。这可以通过以下代码实现:
partial class _Default : System.Web.UI.Page
{
protected void Page_Init(object sender, System.EventArgs e)
{
// redirects to the input form
Response.Redirect("Formulaire.aspx");
}
}
呈现页面 [Default.aspx] 仅包含将其与 [Default.aspx.cs] 关联的指令:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="pam_v7._Default" Title="Untitled Page" %>

























