7. 应用程序的 Ajax 化 ASP.NET MVC
7.1. AJAX在Web应用中的作用
目前,所研究的示例具有以下架构:
![]() |
要从视图 [Vue1] 切换到视图 [Vue2],浏览器会:
- 向 Web 应用程序发送请求;
- 接收视图 [Vue2] 并将其显示在原视图 [Vue1] 的位置。
这是典型的流程:
- 浏览器发起请求;
- Web 服务器生成视图并作为响应发送给客户端;
- 浏览器显示该新视图。
浏览器与Web服务器之间还存在另一种交互模式:AJAX(异步JavaScript和XML)。这实际上是浏览器显示的视图与Web服务器之间的交互。 浏览器仍继续执行其本职工作——显示视图,但此时它已由显示的视图中嵌入的JavaScript所控制。其工作流程如下:
![]() |
- 在 [1] 中,浏览器显示的页面中发生了一个事件(点击按钮、文本更改等)。该事件被页面中嵌入的 JavaScript(JS)拦截;
- 在 [2] 阶段,JavaScript 代码会像浏览器原本会做的那样发起请求 HTTP。 该请求为异步请求:用户可以继续与页面交互,而不会因等待 HTTP 请求的响应而被阻塞。该请求遵循常规的处理流程。它与常规请求没有任何(或几乎没有)区别;
- 在 [3] 中,会向客户端发送响应 JS。 发送的并非完整的 HTML 视图,而是一个部分视图 HTML,即流 XML 或 JSON (JavaScript 对象表示法);
- 在 [4] 中,JavaScript 获取该响应,并利用它来更新已显示的 HTML 页面中的某个区域。
对用户而言,视图发生了变化,因为其所见内容已更新。但这并非页面完全刷新,而仅仅是对当前显示页面的局部修改。 这有助于提升页面的流畅性和交互性:由于无需完全刷新页面,因此可以处理此前无法处理的事件。例如,当用户在输入框中逐个输入字符时,向其提供选项列表。 每输入一个新字符,就会向服务器发送一个 AJAX 请求,服务器随后返回其他建议。如果没有 Ajax,此类输入辅助功能以前是无法实现的。因为无法在每次输入字符时都重新加载新页面。
7.2. JQuery与JavaScript的基础知识
我们经常在页面中引入 JQuery JavaScript 库。其中包含以下代码行:
<script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
注:请根据您使用的 Visual Studio 版本调整 jQuery 的版本。
ASP.NET 的 Ajax 技术 MVC 使用 JQuery。 我们将亲自编写一些 JQuery 脚本。因此,现在我们将介绍理解本章脚本所需掌握的 JQuery 基础知识。
我们在 [Exemples] 解决方案中创建一个新项目 [Exemple-04]:
![]() |
若要在 ASP.NET 和 MVC 中使用 Ajax,配置文件 [Web.config] 和 [1] 中必须包含以下一行:
<appSettings>
...
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
第 3 行允许在 ASP.NET 视图中使用 Ajax。该行默认已存在。
我们在新项目 [2] 的 [Content] 文件夹中创建文件 HTML [JQuery-01.html]:
![]() |
该文件内容如下:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>JQuery-01</title>
<script type="text/javascript" src="/Scripts/jquery-1.8.2.min.js"></script>
</head>
<body>
<h3>Rudiments de JQuery</h3>
<div id="element1">
Elément 1
</div>
</body>
</html>
- 第 6 行:导入 JQuery(请根据您的 Visual Studio 版本调整版本号);
- 第 10-12 行:ID 为 [element1] 的页面元素。我们将对该元素进行操作。
在 Google Chrome 浏览器中查看此文件 [4] 和 [5]:
![]() |
在 Google Chrome 中按 [Ctrl-Maj-I] 调出开发者工具 [6]。通过 [Console] 和 [7] 标签页可以执行 JavaScript 代码。 下面我们将提供一些需要输入的 JavaScript 命令,并附上说明。
JS | 结果 |
|
:返回所有 id 为 [element1] 的元素集合,通常该集合包含 0 或 1 个元素,因为 HTML 页面中不能存在两个相同的 id。 | ![]() |
|
: 将文本 [blabla] 应用于集合中的所有元素。这将导致页面显示的内容发生变化 | ![]() |
|
隐藏集合中的元素。文本 [blabla] 不再显示。 | ![]() |
|
:重新显示集合。这让我们看到,ID 为 [element1] 的元素具有属性 CSS style='display: none;',这导致该元素被隐藏。 | |
|
:显示集合中的元素。文本 [blabla] 再次出现。这是由属性 CSS style='display : block;' 确保的显示效果。 | ![]() |
|
:为集合中的所有元素设置一个属性。此处的属性为 [style],其值为 [color: red]。文本 [blabla] 变为红色。 | ![]() |
![]() | |
![]() |
需要注意的是,在整个操作过程中,浏览器的URL并未发生变化。没有与Web服务器进行任何交互。所有操作都在浏览器内部完成。现在,让我们查看该页面的源代码:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>JQuery-01</title>
<script type="text/javascript" src="/Scripts/jquery-1.8.2.min.js"></script>
</head>
<body>
<h3>Rudiments de JQuery</h3>
<div id="element1">
Elément 1
</div>
</body>
</html>
这是初始文本。它完全没有反映我们在第10-12行对元素所做的操作。在调试JavaScript时,这一点非常重要。因此,查看已显示页面的源代码通常是徒劳的。要了解当前显示页面的源代码,请按以下步骤操作:
![]() |
我们已掌握足够知识来理解接下来的 JS 脚本。
7.3. 使用数据流更新页面 HTML
7.3.1. 视图
我们将研究以下应用程序:
![]() |
- 在 [1] 中,页面加载时间;
- 在 [2] 中,对两个实数 A 和 B 进行四则运算;
- 在 [3] 中,服务器的响应被写入页面中的某个区域;
- 在 [4] 处,计算时间。该时间与页面加载时间 [5] 不同。 后者的时间等于 [1],表明区域 [6] 未被重新加载。此外,该页面的 URL 和 [7] 时间未发生变化。
7.3.2. 控制器、操作、模型、视图
我们创建一个名为 [Premier] 的控制器:
![]() |
为了显示初始视图,我们创建以下操作 [Action01Get]:
[HttpGet]
public ViewResult Action01Get()
{
ViewModel01 modèle = new ViewModel01();
modèle.HeureChargement = DateTime.Now.ToString("hh:mm:ss");
return View(modèle);
}
- 第 4 行:实例化视图模型;
- 第 5 行:初始化视图加载时间;
- 第 6 行:显示视图 [Action10Get.cshtml] 及其模型。
[ViewModel01] 模板 如下:
![]() |
using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Exemple_04.Models
{
[Bind(Exclude = "AplusB, AmoinsB, AmultipliéparB, AdiviséparB, Erreur, HeureChargement, HeureCalcul")]
public class ViewModel01
{
// 表单
[Required(ErrorMessage="Donnée requise")]
[Display(Name="Valeur de A")]
[Range(0, Double.MaxValue, ErrorMessage = "Tapez un nombre positif ou nul")]
public double A { get; set; }
[Required(ErrorMessage = "Donnée requise")]
[Display(Name = "Valeur de B")]
[Range(0, Double.MaxValue, ErrorMessage="Tapez un nombre positif ou nul")]
public double B { get; set; }
// 结果
public string AplusB { get; set; }
public string AmoinsB { get; set; }
public string AmultipliéparB { get; set; }
public string AdiviséparB { get; set; }
public string Erreur { get; set; }
public string HeureChargement { get; set; }
public string HeureCalcul { get; set; }
}
}
- 第 11-14 行:表单的 A 值;
- 第15-18行:表单中的值B;
- 第21-24行:对A和B进行的四则运算结果;
- 第25行:可能出现的错误文本;
- 第26行:视图在浏览器中加载的时间;
- 第27行:第21-24行字段的计算时间;
- 第 7 行:此视图模板同时也是一个操作模板。其中排除了未由浏览器提交的字段。
视图 [Action01Get.cshtml] 如下所示:
![]() |
@model Exemple_04.Models.ViewModel01
@{
Layout = null;
AjaxOptions ajaxOpts = new AjaxOptions
{
UpdateTargetId = "résultats",
HttpMethod = "post",
Url = Url.Action("Action01Post"),
LoadingElementId = "loading",
LoadingElementDuration = 1000
};
}
<!DOCTYPE html>
<html lang="fr-FR">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-01</title>
<link rel="stylesheet" href="~/Content/Site.css" />
<script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
<script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
<script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
<script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.en-US.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
<script type="text/javascript" src="~/Scripts/myScripts-01.js"></script>
</head>
<body>
<h2>Ajax - 01</h2>
<p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
<h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
@using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
{
<table>
<thead>
<tr>
<th>@Html.LabelFor(m => m.A)</th>
<th>@Html.LabelFor(m => m.B)</th>
</tr>
</thead>
<tbody>
<tr>
<td>@Html.TextBoxFor(m => m.A)</td>
<td>@Html.TextBoxFor(m => m.B)</td>
</tr>
<tr>
<td>@Html.ValidationMessageFor(m => m.A)</td>
<td>@Html.ValidationMessageFor(m => m.B)</td>
</tr>
</tbody>
</table>
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
}
<hr />
<div id="résultats" />
</body>
</html>
- 第 1 行:该视图的模板类型为 [ViewModel01];
- 第 21 行:需要 JQuery 同时用于验证和 Ajax;
- 第22-23行:验证库;
- 第24-26行:国际化库;
- 第27行:Ajax库;
- 第28行:本地JavaScript库;
- 第 33 行:显示视图加载时间;
- 第 35 行:一个 Ajax 表单——我们稍后会再讨论;
- 第40-41行:数字A和B的输入标签;
- 第46-47行:A和B的数值输入框;
- 第 50-51 行:数字 A 和 B 输入的错误提示;
- 第 56 行:提交表单的按钮。该表单将通过 Ajax 请求提交;
- 第 57 行:Ajax 请求期间显示的加载图片;
- 第 58 行:一个通过 Ajax 请求提交表单的链接;
- 第62行:一个ID为[résultats]的<div>标签。我们将在此处放置Web服务器返回的HTML数据流。
此视图显示以下页面:
![]() |
现在让我们查看将表单Ajax化的代码:
...
@{
Layout = null;
AjaxOptions ajaxOpts = new AjaxOptions
{
UpdateTargetId = "résultats",
HttpMethod = "post",
Url = Url.Action("Action01Post"),
LoadingElementId = "loading",
LoadingElementDuration = 1000
};
}
...
@using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
{
....
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
}
...
<div id="résultats" />
- 第 15 行:这里使用的是 [@Ajax.BeginForm],而非 [@Html.BeginForm]。该方法支持多种重载。此处使用的重载具有以下签名:
Ajax.BeginForm(string ActionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string,object> htmlAttributes)
此处使用的实际参数如下:
Action01Post:处理表单中 POST 的操作名称,
null:无需提供路由信息,
ajaxOpts:Ajax调用的选项。这些选项已在第6-10行中定义,
new { id = "formulaire" }:用于将 [id='formulaire'] 属性赋值给生成的 <form> 标签;
使用的 Ajax 选项如下:
- 第 8 行:Ajax 请求的目标;
- 第 7 行:Ajax 请求 HTTP 的方法;
- 第 6 行:将由 Ajax 请求响应更新的页面区域 ID;
- 第 9 行:Ajax 请求期间将显示的页面区域 ID——通常为加载图标。此处将显示第 20 行内容,其中包含一个表示加载状态的动画图像。初始时,该图像被样式 [display : none] 隐藏;
- 第 10 行:动画图片显示前的等待时间(以毫秒为单位),此处为 1 秒。
Ajax表单生成的代码HTML如下:
<form action="/Premier/Action01Post" data-ajax="true" data-ajax-loading="#loading" data-ajax-loading-duration="1000" data-ajax-method="post" data-ajax-mode="replace" data-ajax-update="#résultats" data-ajax-url="/Premier/Action01Post" id="formulaire" method="post"> <table>
...
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="/Content/images/indicator.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
</form>
<hr />
<div id="résultats" />
- 第 1 行:生成的 <form> 标签。请注意 [data-ajax-attr] 属性,它们反映了与 Ajax 请求关联的 [AjaxOptions] 类型对象的字段值。这些属性由 Ajax 库管理。如果没有它们,<form> 标签将变为:
<form action="/Premier/Action01Post" id="formulaire" method="post">
...
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="/Content/images/indicator.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
</form>
此时,我们面对的将是一个经典的 HTML 表单。如果用户在其浏览器上禁用了 JavaScript,则将执行此代码。此时,第 5-6 行将不再被使用。
7.3.3. 操作 [Action01Post]
处理 Ajax 请求 HTTP 的操作 [Action01Post] 如下:
[HttpPost]
public PartialViewResult Action01Post(FormCollection postedData, SessionModel session)
{
// 模拟等待
Thread.Sleep(2000);
// 操作模型实例化
ViewModel01 modèle = new ViewModel01();
// 计算时间
modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
// 模型更新
TryUpdateModel(modèle, postedData);
if (!ModelState.IsValid)
{
// 返回错误
modèle.Erreur = getErrorMessagesFor(ModelState);
return PartialView("Action01Error", modèle);
}
// 每两次模拟一次错误
int val = session.Randomizer.Next(2);
if (val == 0)
{
modèle.Erreur = "[erreur aléatoire]";
return PartialView("Action01Error", modèle);
}
// 计算
modèle.AplusB = string.Format("{0}", modèle.A + modèle.B);
modèle.AmoinsB = string.Format("{0}", modèle.A - modèle.B);
modèle.AmultipliéparB = string.Format("{0}", modèle.A * modèle.B);
modèle.AdiviséparB = string.Format("{0}", modèle.A / modèle.B);
// 视图
return PartialView("Action01Success", modèle);
}
- 第 1 行:该操作仅处理 [POST];
- 第 2 行:该操作支持的操作模板为:
- [FormCollection postedData]:Ajax请求POST提交的所有值,
- [SessionModel session]:会话中的元素。此处采用第4.10节所述的技术;
- 第2行:该操作将返回片段HTML,而非完整的页面HTML;
- 第5行:人为地暂停两秒钟,以模拟一个耗时的Ajax操作;
- 第 7 行:实例化一个类型为 [ViewModel01] 的模型;
- 第 9 行:初始化计算时间;
- 第 11 行:尝试使用提交的值更新类型为 [ViewModel01] 的模型。需要提醒的是,这些值共有两个:数字 A 和 B 的值;
- 第12行:验证此次更新是否成功;
- 第 15 行:若发生错误,则填充模型的 [Erreur] 字段;
- 第16行:基于模板[ViewModel01]生成部分视图[Action01Error.cshtml];
- 第19-24行:每两次中模拟一次错误;
- 第19行:在区间[0,1]内生成一个随机整数。随机数生成器取自当前会话;
- 第20行:若生成的值为0,则模拟错误;
- 第22行:将错误信息放入模板中;
- 第23行:返回利用模板[ViewModel01]生成的部分视图[Action01Error.cshtml];
- 第26-29行:对数字A和B进行算术运算,并将结果以字符串形式放入模型中;
- 第31行:返回利用模板[ViewModel01]生成的子视图[Action01Success.cshtml];
7.3.4. 视图 [Action01Error]
视图 [Action01Error.cshtml] 如下所示:
@model Exemple_04.Models.ViewModel01
<h4>Résultats</h4>
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p style="color: red;">Une erreur s'est produite : @Model.Erreur</p>
需要说明的是,该部分数据流 HTML 将作为对 Ajax 请求 HTTP(类型为 POST)的响应发送,并被放置在页面中 id 为 [résultats] 的区域内。 所有这些信息均来自主页面 [Action01Get.cshtml] 中使用的 Ajax 配置:
@model Exemple_04.Models.ViewModel01
@{
Layout = null;
AjaxOptions ajaxOpts = new AjaxOptions
{
UpdateTargetId = "résultats",
HttpMethod = "post",
Url = Url.Action("Action01Post"),
LoadingElementId = "loading",
LoadingElementDuration = 1000
};
}
以下是一个包含错误的响应示例:
![]() |
7.3.5. 视图 [Action01Success]
视图 [Action01Success.cshtml] 如下所示:
@model Exemple_04.Models.ViewModel01
<h4>Résultats</h4>
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>
同样,这个部分数据流 HTML 将作为对 Ajax 请求 HTTP(类型为 POST)的响应发送,并被放置在页面中 id 为 [résultats] 的区域内:
![]() |
7.3.6. G 会话管理
我们已经看到 [Action01Post] 使用了会话。会话模板是以下 [SessionModel] 类型:
using System;
namespace Exemple_03.Models
{
public class SessionModel
{
public Random Randomizer { get; set; }
}
}
会话在 [Global.asax] 中初始化:
// 会话
protected void Session_Start()
{
SessionModel sessionModel=new SessionModel();
sessionModel.Randomizer=new Random(DateTime.Now.Millisecond);
Session["data"] = sessionModel;
}
会话与模型的关联是在 [Application_Start] 中进行的:
protected void Application_Start()
{
...
// 模型绑定器
ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}
已描述类 [SessionModelBinder]。
7.3.7. 等待图像管理
@model Exemple_04.Models.ViewModel01
@{
Layout = null;
AjaxOptions ajaxOpts = new AjaxOptions
{
...
LoadingElementId = "loading",
LoadingElementDuration = 1000
};
}
...
<body>
...
@using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
{
...
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
}
...
当 Ajax 请求启动时,ID 为 [loading] 的区域(第 7 行)会在 [ligne 8] 显示一秒后出现。该区域即最初被隐藏的第 21 行图像。这将生成以下界面:
![]() |
7.3.8. 链接管理 [Calculer]
让我们来分析主页面 [Action01Get.cshtml] 上的链接 [Calculer]:
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-01</title>
...
<script type="text/javascript" src="~/Scripts/myScripts-01.js"></script>
</head>
<body>
<h2>Ajax - 01</h2>
<p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
<h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
@using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
{
...
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
}
<hr />
<div id="résultats" />
- 第 18 行:点击链接 [Calculer] 会触发函数 JS [postForm] 的执行。 该函数在文件 [myScripts-01.js] 的第 5 行中定义。该脚本如下:
![]() |
function postForm() {
// 使用 JQuery 手动发起 Ajax 请求
var loading = $("#loading");
var formulaire = $("#formulaire");
var résultats = $('#结果');
$.ajax({
url: '/Premier/Action01Post',
type: 'POST',
data: formulaire.serialize(),
dataType: 'html',
begin: loading.show(),
success: function (data) {
loading.hide()
résultats.html(data);
}
})
}
// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
return this.optional(element) ||
!isNaN(Globalize.parseFloat(value));
}
$.validator.methods.date = function (value, element) {
return this.optional(element) ||
!isNaN(Globalize.parseDate(value));
}
jQuery.extend(jQuery.validator.methods, {
range: function (value, element, param) {
//使用全球化插件解析该值
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
第19-37行的函数已在第6.1节中介绍过。它们负责页面的国际化处理。 此处不再赘述。第1-17行,我们手动执行Ajax调用;而在按钮[Calculer]的情况下,该调用原本由项目关联的Ajax库完成。为此,我们使用了项目关联的库JQuery。
- 第3行:引用ID为[loading]的组件。[$("#loading")]返回ID为[loading]的元素集合。该集合仅包含一个元素;
- 第4行:对ID为[formulaire]的组件的引用;
- 第 5 行:对 ID 为 [résultats] 的组件的引用;
- 第 6 行:包含选项的 Ajax 调用;
- 第 7 行:Ajax 调用的目标 URL;
- 第 8 行:使用的 HTTP 方法;
- 第 9 行:提交的数据。[formulaire.serialize] 创建了表单 ID 为 [formulaire] 的 POST 字段的字符串 [A=val1&B=val2];
- 第 10 行:预期返回的数据类型。已知服务器将返回数据流 HTML;
- 第 11 行:请求启动时要执行的方法。此处指定需显示 ID 为 [loading] 的组件。这是加载时的动画图片;
- 第 12 行:Ajax 请求成功时执行的方法。参数 [data] 是服务器的完整响应。已知其为数据流 HTML;
- 第13行:隐藏等待提示;
- 第14行:使用参数[data]中的HTML更新ID为[résultats]的组件。
请读者测试链接 [Calculer]。该链接的功能与按钮 [Calculer] 基本相同,只是存在一个异常情况: 。使用该链接后,即可提交无效的 A 和 B 值:
![]() |
- 在 [1] 和 [2] 中,我们输入了无效值。这些值会被客户端验证器标记;
- 在 [3] 中,我们点击了链接 [Calculer];
- 在 [4] 中,发生了 [POST] 错误,因为返回的响应是 [4]。
当值无效且点击按钮 [Calculer] 时,不会向服务器发送 [POST]。 在相同情况下,使用链接 [Calculer] 时,会向服务器发送 [POST]。 因此,[Calculer]按钮的行为无法通过[Calculer]链接复现。与其现在尝试解决这个问题,我们将其留作后续示例,该示例还将说明另一个客户端验证问题。
7.4. 使用 JSON 数据流更新 HTML 页面
在上一个示例中,Web 服务器对 HTTP Ajax 请求的响应是一个 HTML 数据流。该数据流中包含数据,并附带 HTML 格式化信息。 我们建议沿用前面的示例,但这次使用仅包含数据的响应 JSON(JavaScript 对象表示法)。这样做的好处是传输的字节数更少。
7.4.1. 操作 [Action02Get]
操作 [Action02Get] 将作为新应用程序的入口点。其代码如下:
@model Exemple_04.Models.ViewModel02
@{
Layout = null;
AjaxOptions ajaxOpts = new AjaxOptions
{
HttpMethod = "post",
Url = Url.Action("Action02Post"),
LoadingElementId = "loading",
LoadingElementDuration = 1000,
OnBegin = "OnBegin",
OnFailure = "OnFailure",
OnSuccess = "OnSuccess",
OnComplete = "OnComplete"
};
}
<!DOCTYPE html>
<html lang="fr-FR">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-02</title>
....
<script type="text/javascript" src="~/Scripts/myScripts-02.js"></script>
</head>
<body>
<h2>Ajax - 02</h2>
<p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
<h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
@using (Ajax.BeginForm("Action02Post", null, ajaxOpts, new { id = "formulaire" }))
{
...
<p>
<input type="submit" value="Calculer" />
<img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
<a href="javascript:postForm()">Calculer</a>
</p>
}
<hr />
<div id="entete">
<h4>Résultats</h4>
<p><strong>Heure de calcul : <span id="heureCalcul"/></strong></p>
</div>
<div id="résultats">
<p>A+B=<span id="AplusB"/></p>
<p>A-B=<span id="AmoinsB"/></p>
<p>A*B=<span id="AmultipliéparB"/></p>
<p>A/B=<span id="AdiviséparB"/></p>
</div>
<div id="erreur">
<p style="color: red;">Une erreur s'est produite : <span id="msg"/></p>
</div>
</body>
</html>
- 第 4-14 行:Ajax 调用的选项;
- 第 10 行:请求启动时需执行的 JS 函数。该函数定义在第 24 行引用的 JS 文件中;
- 第 11 行:请求失败时需执行的函数 JS;
- 第 12 行:请求成功时执行的函数 JS;
- 第 13 行:Ajax 请求获得结果(失败或成功)后需执行的函数 JS;
- 第 40-43 行:ID 为 [entete] 的区域;
- 第 44-49 行:ID 为 [résultats] 的区域。该区域将显示四项算术运算的结果;
- 第 50-52 行:ID 为 [erreur] 的区域。该区域将显示可能出现的错误信息。
7.4.2. 操作 [Action02Post]
Ajax请求由以下操作[Action02Post]处理:
[HttpPost]
public JsonResult Action02Post(FormCollection postedData, SessionModel session)
{
// 等待模拟
Thread.Sleep(2000);
// 模型验证
ViewModel02 modèle = new ViewModel02();
// 加载和计算时间
string HeureChargement = DateTime.Now.ToString("hh:mm:ss");
string HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
// 模型更新
TryUpdateModel(modèle, postedData);
if (!ModelState.IsValid)
{
// 返回错误
return Json(new { Erreur = getErrorMessagesFor(ModelState), HeureCalcul = HeureCalcul });
}
// 每两次就会模拟一次错误
int val = session.Randomizer.Next(2);
if (val == 0)
{
// 返回错误
return Json(new { Erreur = "[erreur aléatoire]", HeureCalcul = HeureCalcul });
}
// 计算
string AplusB = string.Format("{0}", modèle.A + modèle.B);
string AmoinsB = string.Format("{0}", modèle.A - modèle.B);
string AmultipliéparB = string.Format("{0}", modèle.A * modèle.B);
string AdiviséparB = string.Format("{0}", modèle.A / modèle.B);
// 返回结果
return Json(new { Erreur = "", AplusB = AplusB, AmoinsB = AmoinsB, AmultipliéparB = AmultipliéparB, AdiviséparB = AdiviséparB, HeureCalcul = HeureCalcul });
}
- 第 2 行:该方法返回类型 [JsonResult],即 JSON 格式的文本;
- 第16行:信息以JSON序列化的匿名类实例形式呈现。[getErrorMessagesFor]方法已在前文介绍过。发送到浏览器的JSON字符串将呈现如下形式:
- 第 31 行:对算术结果采用相同的方法。这次,发送给浏览器的字符串 JSON 将呈现如下形式:
{"Erreur":"","AplusB":"4","AmoinsB":"-2","AmultipliéparB":"3","AdiviséparB":"0,333333333333333","HeureCalcul":"05:52:17"}
7.4.3. 客户端的 JavaScript 代码
回顾一下发送给客户端浏览器的 HTML 页面中 Ajax 调用的配置:
AjaxOptions ajaxOpts = new AjaxOptions
{
HttpMethod = "post",
Url = Url.Action("Action02Post"),
LoadingElementId = "loading",
LoadingElementDuration = 1000,
OnBegin = "OnBegin",
OnFailure = "OnFailure",
OnSuccess = "OnSuccess",
OnComplete = "OnComplete"
};
第 7-10 行(等号右侧)引用的 JS 函数在以下 [myScripts-02.js] 文件中定义:
// 全局数据
var entete;
var loading;
var résultats;
var erreur;
var heureCalcul;
var msg;
var AplusB;
var AmoinsB;
var AmultipliéparB;
var AdiviséparB;
var formulaire;
...
function postForm() {
...
}
// 在加载文档时
$(document).ready(function () {
formulaire = $("#formulaire");
entete = $("#entete");
loading = $("#loading");
erreur = $("#erreur");
résultats = $('#结果');
heureCalcul = $("#heureCalcul");
msg = $("#msg");
AplusB = $("#AplusB");
AmoinsB = $("#AmoinsB");
AmultipliéparB = $("#AmultipliéparB");
AdiviséparB = $("#AdiviséparB");
// 隐藏页面中的某些元素
entete.hide();
résultats.hide();
erreur.hide();
});
// 开始
function OnBegin() {
....
}
// 请求结束
function OnComplete() {
...
}
// 成功
function OnSuccess(data) {
....
}
// 错误
function OnFailure(request, error) {
...
}
- 第 19 行:在浏览器完成页面加载后执行函数 JS;
- 第20-30行:获取页面中所有目标组件的引用。在页面中搜索组件会消耗资源,因此最好只执行一次;
- 第33-35行:隐藏组件[entete]、[résultats]和[loading];
在Ajax请求启动时,将执行以下函数:
// 开始
function OnBegin() {
// 待机指示灯亮起
loading.show();
// 页面中某些元素被隐藏
entete.hide();
résultats.hide();
erreur.hide();
}
- 第 4 行:显示组件 [loading]。这是动画图片;
- 第 6-8 行:[entete]、[résultats] 和 [erreur] 组件被隐藏;
如果 Ajax 请求成功,则执行以下 JS 代码:
// 成功
function OnSuccess(data) {
// 显示结果
heureCalcul.text(data.HeureCalcul);
entete.show();
if (data.Erreur != '') {
msg.text(data.Erreur);
erreur.show();
return;
}
// 无错误
AplusB.text(data.AplusB);
AmoinsB.text(data.AmoinsB);
AmultipliéparB.text(data.AmultipliéparB);
AdiviséparB.text(data.AdiviséparB);
résultats.show();
}
要理解这段代码,需回顾可能作为响应发送给浏览器的两段文本 JSON:
(若发生错误),否则返回字符串:
{"Erreur":"","AplusB":"4","AmoinsB":"-2","AmultipliéparB":"3","AdiviséparB":"0,333333333333333","HeureCalcul":"05:52:17"}
若将该字符串命名为 [data],则字段 [Erreur] 的值可通过 [data.Erreur] 或 [data["Erreur"]] 表示,二者任选其一。 对于字符串 JSON 中的其他字段也是如此。此外,若要将未格式化的文本赋值给 ID 为 X 的组件,则写为 [X.text(chaine)]。让我们回到函数 [OnSuccess] 的代码:
- 第 2 行:[data] 是接收到的字符串 JSON;
- 第 4 行:组件 [heureCalcul] 接收其值;
- 第 5 行:显示组件 [entete];
- 第 6 行:测试字符串 JSON 中的字段 [Erreur];
- 第 7 行:组件 [msg] 获取其值;
- 第 8 行:显示组件 [erreur];
- 第 9 行:错误情况处理完毕;
- 第12行:组件[AplusB]获取其值;
- 第 13 行:组件 [AmoinsB] 获取其值;
- 第 14 行:组件 [AmultipliéparB] 获取其值;
- 第 15 行:组件 [AdiviséparB] 获取其值;
- 第 16 行:显示组件 [résultats]。
如果 Ajax 请求 HTTP 失败,将执行函数 [OnFailure] 。该失败通过服务器返回的代码 HTTP 进行检测。 例如,500 状态码 [Internal Server Error] 表示服务器无法执行该请求。函数 [OnFailure] 如下:
// 错误
function OnFailure(request, error) {
alert("L'erreur suivante s'est produite :" + error);
}
目前仅显示一个包含发生错误的对话框。实际上,我们需要更精确的处理。我们将很快提供另一种解决方案。
最后,无论请求成功还是失败,在请求结束时都会执行函数 [OnComplete]。
// 请求结束
function OnComplete() {
// 等待信号关闭
loading.hide();
}
在此重申,正是视图 [Action02Get.cshtml] 中 Ajax 调用的配置,导致了这些不同函数的调用:
AjaxOptions ajaxOpts = new AjaxOptions
{
...
OnBegin = "OnBegin",
OnFailure = "OnFailure",
OnSuccess = "OnSuccess",
OnComplete = "OnComplete"
};
7.4.4. 链接 [Calculer]
视图 [Action02Get.cshtml] 中链接 [Calculer] 的代码 HTML 如下:
<a href="javascript:postForm()">Calculer</a>
在导入的文件 [myScripts-02.js] 中找到了函数 JS [postForm]:
<script type="text/javascript" src="~/Scripts/myScripts-02.js"></script>
其代码如下:
function postForm() {
// 使用 JQuery 手动发起 Ajax 调用
$.ajax({
url: '/Premier/Action02Post',
type: 'POST',
data: formulaire.serialize(),
dataType: 'json',
beforeSend: OnBegin,
success: OnSuccess,
error: OnFailure,
complete: OnComplete
})
}
我们之前已经遇到过类似的代码。
- 第 4 行:URL 是 Ajax 调用的目标;
- 第 5 行:Ajax 调用使用的命令 HTTP;
- 第 6 行:提交的值。这些值是表单值序列化的结果。该表单由 id [formulaire] 标识,并通过变量 [formulaire] 引用。 [data] 将是一个形式为 [A=val1&B=val2] 的字符串;
- 第 7 行:预期响应的格式类型。这是一个字符串 JSON;
- 第 8 行:Ajax 调用启动时要执行的函数 JS;
- 第 9 行:Ajax 调用成功时要执行的函数 JS;
- 第 10 行:如果 Ajax 调用失败,则执行函数 JS;
- 第 11 行:无论服务器响应成功还是失败,在收到服务器响应后执行函数 JS。
让我们回到处理 Ajax 调用失败情况的 JavaScript 函数(第 10 行)。 Ajax调用会在多种情况下失败,例如当服务器返回错误代码如[403 Forbidden]、[404 Not Found]、[500 Internal Server Error]、[301 Moved Permanently]等时。
在上例中,函数 [OnFailure] 如下所示:
// 错误
function OnFailure(request, error) {
alert("L'erreur suivante s'est produite :" + error);
}
通常情况下,显示对象 [error] 不会提供任何有价值的信息。如果使用基于 JQuery 的 Ajax 调用,可以使用以下 [OnFailure] 方法;
// 错误
function OnFailure(jqXHR) {
alert("Erreur : " + jqXHR.status + " " + jqXHR.statusText);
msg.html(jqXHR.responseText);
erreur.show();
}
对象 JQuery [jqXHR] 的属性包括以下内容:
- responseText:服务器响应的文本;
- status:服务器返回的错误代码;
- statusText:与该错误代码关联的文本。
- 第 3 行:显示错误代码及其对应的说明;
- 第 4 行:将服务器的响应 HTML 放入 ID 为 [msg] 的组件中;
- 第 5 行:显示 ID 为 [erreur] 的区域。
为了测试此错误处理功能,我们将在操作 [Action02Post] 中人为地引发一个异常:
[HttpPost]
public JsonResult Action02Post(FormCollection postedData, SessionModel session)
{
// 为测试Ajax调用的错误处理功能而人为抛出的异常
throw new Exception();
// 模拟等待
Thread.Sleep(2000);
// 表单验证
...
第 5 行抛出异常。现在测试应用程序:
![]() |
我们得到了以下响应:[1] 和 [2]:
![]() |
服务器的响应让我们能够看到错误发生在服务器代码的哪一行。这通常是一条有用的信息。今后我们将使用这种技术来处理 Ajax 调用的错误。
7.5. 单页Web应用程序
Ajax技术可用于构建单页应用程序:
- 首页由浏览器的常规请求生成;
- 后续页面则通过 Ajax 调用获取。因此,最终浏览器不会切换 URL 且不会加载新页面。 此类应用被称为单页应用(APU),英文称为 Single Page Application(SPA)。
以下是一个此类应用程序的基本示例。新应用程序将包含两个视图:
![]() |
![]() |
- 在 [1] 中,通过操作 [Action03Get] 可显示第一页(即第 1 页);
- 在 [2] 中,通过一个链接,我们可以借助 Ajax 调用跳转到第 2 页;
- 在 [3] 中,URL 未发生变化。当前显示的是第 2 页;
- 在 [4] 中,通过一个链接,我们可以借助 Ajax 调用返回第 1 页;
- 在 [5] 中,URL 未发生变化。当前显示的是第 1 页。
操作 [Action03Get] 的代码如下:
[HttpGet]
public ViewResult Action03Get()
{
return View();
}
- 第 4 行:显示视图 [Action03Get.cshtml]。
视图 [Action03Get.cshtml] 如下:
![]() |
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Action03Get</title>
<script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
</head>
<body>
<h3>Ajax - 03 - Single Page Application</h3>
<div id="content">
@Html.Partial("Page1")
</div>
</body>
</html>
- 第16-18行:一个ID为[content]的元素。各个页面将显示在此元素中;
- 第 17 行:默认情况下,首先显示的是页面 [Page1.cshtml]。
页面 [Page1.cshtml] 如下所示:
<h4>Page 1</h4>
<p>
@Ajax.ActionLink("Page 2", "Action04", new { Page = 2 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>
- 第 1 行:页面标题,用于与第 2 页区分;
- 第 3 行:一个 Ajax 链接,包含以下参数:
- 链接标签 [Page 2];
- 链接的目标操作 [Action04];
- 请求的URL参数。此处为[/Premier/Action04?Page=2];
- Ajax调用的选项。此处仅指定需通过服务器响应进行更新的区域ID。对于其他选项,若存在默认值则使用默认值。默认的HTTP方法为GET。
让我们看看点击链接后会发生什么。通过 GET 请求 URL [/Premier/Action04?Page=2]。随后执行 [Action04] 操作:
[HttpGet]
public PartialViewResult Action04(string page = "1")
{
string vue = "Page1";
if (page == "2")
{
vue = "Page2";
}
return PartialView(vue);
}
- 第 2 行:该操作返回一个不完整的 HTML 流;
- 第2行:该操作的模板为字符串 [page]。而我们知道 URL 包含以下信息:[/Premier/Action04?Page=2]。需注意模板不区分大小写;
- 第4-8行:[vue]将接收值[Page2];
- 第 9 行:返回子视图 [Page2.cshtml]。
子视图 [Page2.cshtml] 如下所示:
<h4>Page 2</h4>
<p>
@Ajax.ActionLink("Page 1", "Action04", new { Page = 1 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>
因此,服务器将上述数据流 HTML 作为对 Ajax 调用 GET [/Premier/Action04?Page=2] 的响应返回。 需要注意的是,该 Ajax 调用使用此响应来更新 ID 为 [content] 的区域(见下文第 3 行):
<h4>Page 1</h4>
<p>
@Ajax.ActionLink("Page 2", "Action04", new { Page = 2 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>
这将导致显示以下新内容 [1]:
![]() |
按照同样的逻辑,我们可以看到,点击 [1] 中的链接 [Page 1] 将导致显示 [2]。
让我们回到 ASP.NET MVC 应用程序的总体架构:
![]() |
借助页面 HTML 中嵌入并在浏览器中执行的 JavaScript,可以将代码转移到浏览器端,从而实现以下架构:
![]() |
- 在 [1] 中,Web 层 ASP.NET MVC 已成为访问数据的 Web 接口,这些数据通常存储在数据库中。 生成的视图仅包含数据,不包含任何 HTML 样式,例如 XML 或 JSON 数据流;
- 在 [2] 中:浏览器显示由 Web 服务器提供的静态视图(即非动态生成的视图),该 Web 服务器可能位于与 [1] 服务器相同的机器上,也可能位于其他机器上。 随后,这些静态视图会通过 JavaScript 从 [1] Web 接口获取的数据进行增强;
- 嵌入在 HTML 页面中的 JavaScript 代码可按层级进行组织:
- [présentation]层负责处理用户交互,
- [DAO]层负责通过[1] Web服务器访问数据,
- [métier] 层对应于 [métier] 层,该层此前位于 [1] 服务器上,现已迁移至 [2] 浏览器;
这种架构的优势在于它调动了不同的技能:
- Web 服务器 [1] 的代码需要 .NET 技能,但不需要 Javascript、HTML、CSS 技能;
- 浏览器内嵌代码 [2] 需要掌握 Javascript、HTML、CSS 等技能,但对 Web 服务器技术 [1] 则无所谓。
因此,这种架构有助于具备不同技能的团队进行并行协作。它特别适用于单页应用程序。
7.6. 单页Web应用与客户端验证
我们之前曾提及 Ajax-01 示例中存在的一个异常。现重述其背景:
![]() 在 |
- 在 [1] 和 [2] 中,输入了无效值。这些值被客户端验证器标记;
- 在 [3] 中,点击了 [Calculer] 链接;
- 在 [4] 中,发生了 [POST] 事件,因为最终得到了 [4] 的响应。
当值无效且点击按钮 [Calculer] 时,不会向服务器发送 [POST]。 在相同情况下,使用链接 [Calculer] 时,会向服务器发送 [POST]。 因此,[Calculer]按钮的行为无法通过[Calculer]链接复现。
我们将在此示例的基础上引入新的场景:应用程序将包含多个视图,并采用我们刚刚描述的 [Application à Page Unique] 类型。
7.6.1. 示例中的视图
该示例包含多个视图:
![]() |
- 在 [1] 中,视图 [Action05Get];
- 在 [2] 中,包含子视图 [Formulaire05];
- 在 [3] 中,包含子视图 [Failure05];
![]() |
- 在 [4] 中,部分视图 [Success05]。
该应用程序为单页应用:页面在首次请求时由浏览器加载,随后通过 Ajax 调用进行更新。
前面的页面由以下 [cshtml] 视图生成:
![]() |
初始加载的视图是以下 [Action05Get.cshtml] 视图:
@model Exemple_04.Models.ViewModel05
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="fr-FR">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-05</title>
<link rel="stylesheet" href="~/Content/Site.css" />
<script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
<script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
<script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
<script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.en-US.js"></script>
<script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
<script type="text/javascript" src="~/Scripts/myScripts-05.js"></script>
</head>
<body>
<h2>Ajax - 05, Page unique - Validation formulaire côté client</h2>
<p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
<h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
<img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
<div id="content">
@Html.Partial("Formulaire05", Model)
</div>
</body>
</html>
请注意以下几点:
- 第 1 行:视图的模板类型为 [ViewModel05],我们稍后将对此进行介绍;
- 第 13-19 行:包含实现 Ajax 及客户端验证所需的 JavaScript 脚本;
- 第 20 行:我们将把自定义的 JavaScript 函数添加到 [myScripts-05.js] 中;
- 第27行:加载时的动画图片;
- 第28-30行:一个ID为[content]的标签。[Formulaire05, Success05, Failure05]部分视图将插入到该标签中;
- 第29行:插入部分视图[Formulaire05]。
视图 [Action05Get] 负责显示初始页面中的 [1] 部分:
![]() |
部分视图 [Formulaire05] 将生成上述的 [2] 部分。其代码如下:
@model Exemple_04.Models.ViewModel05
@using (Html.BeginForm("Action05Post", "Premier", FormMethod.Post, new { id = "formulaire" }))
{
<table>
<thead>
<tr>
<th>@Html.LabelFor(m => m.A)</th>
<th>@Html.LabelFor(m => m.B)</th>
</tr>
</thead>
<tbody>
<tr>
<td>@Html.TextBoxFor(m => m.A)</td>
<td>@Html.TextBoxFor(m => m.B)</td>
</tr>
<tr>
<td>@Html.ValidationMessageFor(m => m.A)</td>
<td>@Html.ValidationMessageFor(m => m.B)</td>
</tr>
</tbody>
</table>
<p>
<table>
<tbody>
<tr>
<td><a href="javascript:calculer()">Calculer</a>
</td>
<td style="width: 20px" />
<td><a href="javascript:effacer()">Effacer</a>
</td>
</tr>
</tbody>
</table>
</p>
}
- 第 1 行:该部分视图接受 [ViewModel05] 类型的模板;
- 第3行:由方法[Html.BeginForm]生成的表单。由于该表单将通过Ajax调用提交,因此该方法的前三个参数将被忽略。除非用户在浏览器中禁用了JavaScript。此处我们忽略这种情况。 第四个参数很重要。表单的ID为[formulaire];
- 第 5-22 行:用于输入数字 A 和 B 的表单;
- 第27行:一个JavaScript链接,用于对A和B执行四则运算;
- 第30行:一个JavaScript链接,用于清除输入内容及相关的错误信息。
需要注意的是,该表单没有 [submit] 类型的按钮。我们将需要手动计算输入的 A 和 B 值的 [Post]。
若无错误,将显示结果:
![]() |
上文中的 [4] 部分由以下子视图 [Success05.cshtml] 生成:
@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h4>Résultats</h4>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>
<p>
<a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
- 第 1 行:子视图 [Success05.cshtml] 接收类型为 [ViewModel05] 的模板;
- 第 12 行:一个用于返回输入界面的 JavaScript 链接。
若发生错误,将显示另一个部分视图 [3]:
![]() |
该视图由以下代码 [Failure05.cshtml] 生成:
@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
@foreach (string msg in Model.Erreurs)
{
<li>@msg</li>
}
</ul>
<p>
<a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
- 第 1 行:部分视图 [Failure05.cshtml] 接收类型为 [ViewModel05] 的模板;
- 第14行:一个用于返回输入界面的JavaScript链接。
7.6.2. 视图的模板
上述所有视图均使用相同的模板 [ViewModel05]:
![]() |
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Exemple_04.Models
{
[Bind(Exclude = "AplusB, AmoinsB, AmultipliéparB, AdiviséparB, Erreurs, HeureChargement, HeureCalcul")]
public class ViewModel05
{
// 表单
[Required(ErrorMessage="Donnée A requise")]
[Display(Name="Valeur de A")]
[Range(0, Double.MaxValue, ErrorMessage = "Tapez un nombre A positif ou nul")]
public string A { get; set; }
[Required(ErrorMessage = "Donnée B requise")]
[Display(Name = "Valeur de B")]
[Range(0, Double.MaxValue, ErrorMessage="Tapez un nombre B positif ou nul")]
public string B { get; set; }
// 结果
public string AplusB { get; set; }
public string AmoinsB { get; set; }
public string AmultipliéparB { get; set; }
public string AdiviséparB { get; set; }
public List<string> Erreurs { get; set; }
public string HeureChargement { get; set; }
public string HeureCalcul { get; set; }
}
}
这与之前介绍的 [ViewModel01] 模板基本一致,仅存在细微差异:
- 第 15 行和第 19 行:字段 A 和 B 现采用 [string] 类型,以便在初始显示输入表单时,显示为空的输入字段,而非显示值为 0 的字段;
- 第14行和第18行:这并不妨碍使用[Range]验证器对输入值进行验证;
- 第 26 行:由视图 [Failure05] 显示的错误消息列表。
7.6.3. [Session] 作用域数据
在第 7.3.6 节中,我们看到会话数据被封装在以下 [SessionModel] 模型中:
![]() |
using System;
namespace Exemple_03.Models
{
public class SessionModel
{
// 随机数生成器
public Random Randomizer { get; set; }
}
}
该会话模型已扩展以包含 A 和 B 的值:
using System;
namespace Exemple_03.Models
{
public class SessionModel
{
// 随机数生成器
public Random Randomizer { get; set; }
// A和B的值
public string A { get; set; }
public string B { get; set; }
}
}
实际上,必须将 A 和 B 的值存储在会话中,如下所示:
请求 1
![]() |
请求 2
![]() |
在 [4] 中,我们可以看到 [1] 中输入的数据。然而,这里有两个不同的请求 HTTP。 已知两个请求HTTP之间的数据存储在会话中。为了使第二个请求能够检索到第一个请求提交的值,必须将这些值保存到会话中。
7.6.4. 服务器操作 [Action05Get]
操作 [Action05Get] 是用于显示初始单页的操作。其代码如下:
[HttpGet]
public ViewResult Action05Get()
{
ViewModel05 modèle = new ViewModel05();
modèle.HeureChargement = DateTime.Now.ToString("hh:mm:ss");
return View(modèle);
}
- 第 6 行:已分析过的视图 [Action05Get.cshtml] 通过类型为 [ViewModel05] 的模板进行显示;
7.6.5. 客户端操作 [Calculer]
让我们来分析用户与视图之间的交互:
![]() |
链接 [1] 是一个 JavaScript 链接:
<a href="javascript:calculer()">Calculer</a>
JavaScript 函数 [calculer] 位于文件 [myScripts-05.js] 中:
<script type="text/javascript" src="~/Scripts/myScripts-05.js"></script>
JavaScript 函数 [calculer] 的代码如下:
// 全局数据
var content;
var loading;
function calculer() {
// 首先是关于 DOM 的参考信息
var formulaire = $("#formulaire");
// 随后表单验证
if (!formulaire.validate().form()) {
// 表单无效 - 完成
return;
}
// 手动发起 Ajax 请求
$.ajax({
url: '/Premier/Action05FaireCalcul',
type: 'POST',
data: formulaire.serialize(),
dataType: 'html',
beforeSend: function () {
loading.show();
},
success: function (data) {
content.html(data);
},
complete: function () {
loading.hide();
},
error: function (jqXHR) {
// 显示服务器响应
content.html(jqXHR.responseText);
}
})
}
function retourSaisies() {
...
}
function effacer() {
...
}
// 文档加载时
$(document).ready(function () {
// 获取页面中各个组件的引用
loading = $("#loading");
content = $("#content");
// 缓存动画图片
loading.hide();
});
- 需要提醒的是,JavaScript 代码始终在客户端(即浏览器中)执行;
- 第 44 行:当单页的初始加载完成后,将执行函数 JS;
- 第 46 行:引用 ID 为 [loading] 的动画图像;
- 第47行:引用ID为[content]的区域。该区域用于接收部分视图[Formulaire05, Success05, Failure05];
- 第2-3行:将第46-47行的变量声明为全局变量,以便其他函数能够访问。在页面中搜索元素(第46-47行)会产生性能开销。如果可以避免,则无需重复此搜索;
- 第 5 行:函数 [calculer];
- 第7行:获取表单的引用。部分视图[Formulaire05]为其分配了ID [formulaire];
- 第9行:该语句执行表单的客户端验证逻辑。这正是第183页所发现异常中缺失的部分。该方法由单页使用的库[jquery.unobstrusive-ajax]提供:
<script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
如果表单被判定为无效,该语句将返回 [false];
- 第11行:若表单无效,则不向服务器发起Ajax调用;
- 第14-32行:向服务器发起Ajax调用;
- 第15行:目标URL对应服务器操作[Action05FaireCalcul];
- 第16行:通过[POST]进行请求;
- 第17行:提交的值。即表单中的输入内容,此处为A和B的值;
- 第22-24行:若Ajax调用成功,函数[calculer]将使用服务器发送的流HTML更新ID为[content]的区域。
该数据流 HTML 正是由 Ajax 调用所指向的操作 [Action05FaireCalcul] 发送的。该服务器端操作的代码如下:
[HttpPost]
public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
{
// 模板
ViewModel05 modèle = new ViewModel05();
// 计算时间
modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
// 更新模板
TryUpdateModel(modèle, postedData);
if (!ModelState.IsValid)
{
// 返回错误
modèle.Erreurs = getListOfMessagesFor(ModelState);
return PartialView("Failure05", modèle);
}
...
}
- 第 1 行:该操作仅接受 [post] 请求;
- 第 2 行:该操作返回一个部分视图;
- 第 2 行:该操作接收作为参数的提交值(postedData)和会话模型(session);
- 第 5 行:创建部分视图的模板;
- 第 7 行:用计算时间对其进行更新;
- 第9行:尝试将提交的值应用到模型上。此时将执行该模型的验证器。有人可能会问,既然客户端验证器会在数据无效时阻止POST,为何还要费这个劲? 实际上,我们无法确定 POST 的来源。它可能是由非我们编写的代码生成的。因此,我们必须始终在服务器端进行验证;
- 第10行:检测验证器是否通过;
- 第13行:如果模板无效,则使用错误列表对其进行更新。此处的内部方法[getListOfMessagesFor]与第65页所述的[GetErrorMessagesFor]方法类似,此处不再赘述;
- 第 14 行:显示部分视图 [Failure05] 及其模板。重现该视图的代码;
@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
@foreach (string msg in Model.Erreurs)
{
<li>@msg</li>
}
</ul>
<p>
<a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
- 第7-12行:使用<ul>标签显示模板的错误列表。
需注意,JS 函数(源自 [calculer])作为 [Post] 的源头,在服务器操作 [Action05FaireCalcul] 中,将把该数据流 HTML 放入 ID 为 [content] 的区域。结果如下所示:
![]() |
接下来,我们继续研究 [Action05FaireCalcul] 操作的代码:
[HttpPost]
public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
{
// 模板
ViewModel05 modèle = new ViewModel05();
...
// 将 A 和 B 的值存入会话
session.A = modèle.A;
session.B = modèle.B;
// 目前无错误
List<string> erreurs = new List<string>();
// 每两次运行一次,模拟一次错误
int val = session.Randomizer.Next(2);
if (val == 0)
{
erreurs.Add("[erreur aléatoire]");
}
if (erreurs.Count != 0)
{
modèle.Erreurs = erreurs;
return PartialView("Failure05", modèle);
}
// 计算
double A = double.Parse(modèle.A);
double B = double.Parse(modèle.B);
modèle.AplusB = string.Format("{0}", A + B);
modèle.AmoinsB = string.Format("{0}", A - B);
modèle.AmultipliéparB = string.Format("{0}", A * B);
modèle.AdiviséparB = string.Format("{0}", A / B);
// 视图
return PartialView("Success05", modèle);
}
- 第 7 行:模型已被声明为有效;
- 第8-9行:将输入的值A和B存入会话。目的是在后续请求中能够调用这些值;
- 第11-22行:以二分之一概率随机生成错误;
- 第24-29行:对输入的实数执行四则运算;
- 第31行:返回部分视图[Success05]及其模板。该部分视图如下:
@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h4>Résultats</h4>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>
<p>
<a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
需要提醒的是,JS 函数(即 [calculer] 的,该视图源自服务器操作 [Action05FaireCalcul],将把数据流 HTML 放入 ID 为 [content] 的区域中。结果如下所示:
![]() |
7.6.6. 客户端操作 [Effacer]
JavaScript 链接 [Effacer] 可将表单恢复为初始状态:
![]() | ![]() |
在表单中,链接 JS [Effacer] 的定义如下:
<a href="javascript:effacer()">Effacer</a>
函数 JS [effacer] 在文件 [myScripts-05.js] 中定义如下:
// 全局数据
var content;
var loading;
function calculer() {
...
}
function retourSaisies() {
...
}
function effacer() {
// 首先处理 DOM 中的引用
var formulaire = $("#formulaire");
var A = $("#A");
var B = $("#B");
// 为输入字段分配有效值
A.val("0");
B.val("0");
// 然后提交表单以消除
// 可能出现的错误信息
formulaire.validate().form();
// 随后将空字符串赋值给输入字段
A.val("");
B.val("");
}
// 在加载文档时
$(document).ready(function () {
// 获取页面中各个组件的引用
loading = $("#loading");
content = $("#content");
// 隐藏动画图片
loading.hide();
});
- 第 15-17 行:获取 DOM 文件中各类元素的引用(Document Object Model);
- 第 19-20 行:在数字 A 和 B 的输入字段中填入有效值;
- 第23行:执行客户端验证器。由于A和B的值有效,这将导致可能显示的任何错误消息消失;
- 第 25-26 行:在数字 A 和 B 的输入框中填入空字符串;
7.6.7. 客户端操作 [Retour aux Saisies]
JavaScript 链接 [Retour aux Saisies] 用于在获取结果后返回表单:
![]() | ![]() |
在表单中,链接 JS [Retour aux Saisies] 的定义如下:
<a href="javascript:retourSaisies()">Retour aux saisies</a>
函数 JS [retourSaisies] 在文件 [myScripts-05.js] 中定义如下:
// 全局数据
var content;
var loading;
function calculer() {
...
}
function retourSaisies() {
// 手动发起Ajax请求
$.ajax({
url: '/Premier/Action05RetourSaisies',
type: 'POST',
dataType: 'html',
beforeSend: function () {
loading.show();
},
success: function (data) {
content.html(data);
},
complete: function () {
loading.hide();
// IMPORTANT !! 验证
$.validator.unobtrusive.parse($("#formulaire"));
},
error: function (jqXHR) {
content.html(jqXHR.responseText);
}
})
}
function effacer() {
...
}
// 文档加载时
$(document).ready(function () {
// 获取页面中各个组件的引用
loading = $("#loading");
content = $("#content");
// 缓存动画图片
loading.hide();
});
- 第 11-29 行:一个 Ajax 调用;
- 第 12 行:目标 URL;
- 第 13 行:将由 HTTP 命令调用 POST。这是一个不带 POST 参数的 POST。因此,我们找不到如下形式的行:
;
- 第 14 行:服务器返回的预期数据流为 HTML;
- 第18-20行:该HTML数据流将用于更新ID为[content]的区域;
服务器操作 [Action05RetourSaisies] 如下:
[HttpPost]
public PartialViewResult Action05RetourSaisies(SessionModel session)
{
// 视图
return PartialView("Formulaire05", new ViewModel05() { A = session.A, B = session.B });
}
- 第2行:该操作接收会话模板作为参数,我们此前已在该模板中存储了输入的A和B的值;
- 第 5 行:返回部分视图 [Formulaire05],其中包含类型为 [ViewModel05] 的模型,并确保将字段 A 和 B 初始化为从会话中获取的 A 和 B 的值;
现在让我们回到 JavaScript 函数 [retourSaisies] 的代码:
function retourSaisies() {
// 手动发起Ajax请求
$.ajax({
url: '/Premier/Action05RetourSaisies',
type: 'POST',
dataType: 'html',
beforeSend: function () {
loading.show();
},
success: function (data) {
content.html(data);
},
complete: function () {
loading.hide();
// IMPORTANT !! 验证
$.validator.unobtrusive.parse($("#formulaire"));
},
error: function (jqXHR) {
content.html(jqXHR.responseText);
}
})
}
- 第 13 行:Ajax 调用完成时执行的方法;
- 第14行:隐藏加载动画;
- 第16行:这是我在网上找到的一条对我来说有些晦涩的语句,用于解决以下问题:在通过链接[Retour aux saisies]显示的表单中,客户端验证器不再起作用。 在搜索关于 JS 和 [jquery.unobtrusive-ajax] 库的信息时,我找到了第 16 行中的解决方案。它会解析表单,可能是为了激活客户端验证器。
7.7. 将 ASP.NET 应用程序发布到互联网上
参见第 9.26 节。
7.8. 基于单页应用程序生成 Android 原生应用程序 APU
参见第 9.27 节。

















































