7. 将 ASP.NET MVC 应用程序 Ajax 化
7.1. AJAX在Web应用程序中的作用
目前,我们所学习的示例具有以下架构:
![]() |
要从视图 [View1] 切换到视图 [View2],浏览器会:
- 向 Web 应用程序发送请求;
- 接收视图 [View2] 并将其显示在视图 [View1] 的位置上。
这是经典模式:
- 浏览器发出请求;
- Web 服务器生成视图并响应给客户端;
- 浏览器显示该新视图。
浏览器与 Web 服务器之间还有另一种交互模式:AJAX(异步 JavaScript 和 XML)。这涉及浏览器显示的视图与 Web 服务器之间的交互。浏览器继续做它最擅长的事情——显示 HTML 视图——但现在它由嵌入在所显示 HTML 视图中的 JavaScript 控制。该过程如下:
![]() |
- 在[1]中,浏览器显示的页面上发生了一个事件(点击按钮、文本变化等)。该事件被页面中嵌入的JavaScript(JS)拦截;
- 在 [2],JavaScript 代码会像浏览器原本会做的那样发起一个 HTTP 请求。该请求是异步的:用户可以在等待 HTTP 响应时继续与页面交互,而不会被阻塞。该请求遵循标准的处理流程。它与标准请求没有任何(或几乎没有)区别;
- 在 [3],响应被发送给 JavaScript 客户端。通常发送的并非完整的 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>
注意:请确保 jQuery 版本与您的 Visual Studio 版本相匹配。
ASP.NET MVC 的 Ajax 技术使用 jQuery。我们将自己编写一些 jQuery 脚本。因此,现在让我们来了解一些 jQuery 的基础知识,这些知识对于理解本章中的脚本是必要的。
我们在 [Examples] 解决方案中创建一个新项目 [Example-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-Shift-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] 及其模型。
![]() |
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
{
// form
[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; }
// results
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 行:验证和 Ajax 都需要 jQuery;
- 第 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 为 [results] 的 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 行:我们不再使用 [@Html.BeginForm],而是使用 [@Ajax.BeginForm]。该方法支持多种重载。此处使用的重载具有以下签名:
Ajax.BeginForm(string ActionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string,object> htmlAttributes)
在此,我们使用以下实际参数:
Action01Post:处理表单 POST 请求的操作名称,
null:无需提供路由信息,
ajaxOpts:Ajax 调用的选项。这些选项在第 6–10 行中定义,
new { id = "form" }:用于为生成的 <form> 标签赋予 [id='form'] 属性;
使用的 Ajax 选项如下:
- 第 8 行:Ajax HTTP 请求的目标 URL;
- 第 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)
{
// wait simulation
Thread.Sleep(2000);
// action model instantiation
ViewModel01 modèle = new ViewModel01();
// calculation time
modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
// model update
TryUpdateModel(modèle, postedData);
if (!ModelState.IsValid)
{
// returns an error
modèle.Erreur = getErrorMessagesFor(ModelState);
return PartialView("Action01Error", modèle);
}
// every other time, an error is simulated
int val = session.Randomizer.Next(2);
if (val == 0)
{
modèle.Erreur = "[erreur aléatoire]";
return PartialView("Action01Error", modèle);
}
// calculations
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);
// view
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 行:如果发生错误,则填充模型的 [Error] 字段;
- 第 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 代码将作为对 POST 类型 Ajax HTTP 请求的响应发送,并放置在页面中 ID 为 [results] 的区域内。所有这些信息均来自主页面 [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 数据流将作为对 POST 类型 Ajax HTTP 请求的响应发送,并放置在页面中 id 为 [results] 的区域内:

7.3.6. G 会话管理
我们看到 [Action01Post] 使用了会话。会话模型采用以下 [SessionModel] 类型:
using System;
namespace Exemple_03.Models
{
public class SessionModel
{
public Random Randomizer { get; set; }
}
}
会话在 [Global.asax] 中初始化:
// Session
protected void Session_Start()
{
SessionModel sessionModel=new SessionModel();
sessionModel.Randomizer=new Random(DateTime.Now.Millisecond);
Session["data"] = sessionModel;
}
该会话在 [Application_Start] 中与一个模型相关联:
protected void Application_Start()
{
...
// model binders
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 请求开始时,第 7 行 ID 为 [loading] 的区域将在一秒后显示 [第 8 行]。该区域即第 21 行中的图片,该图片最初处于隐藏状态。这将生成如下界面:
![]() |
7.3.8. 处理 [Calculate] 链接
让我们来分析主页面 [Action01Get.cshtml] 上的 [Calculate] 链接:
<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 行:点击 [Calculate] 链接会触发 JS 函数 [postForm] 的执行。该函数在 [myScripts-01.js] 文件的第 5 行中定义。脚本内容如下:
![]() |
function postForm() {
// on fait un appel Ajax à la main avec JQuery
var loading = $("#loading");
var formulaire = $("#formulaire");
var résultats = $('#results');
$.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) {
//Use the Globalization plugin to parse the value
var val = Globalize.parseFloat(value);
return this.optional(element) || (
val >= param[0] && val <= param[1]);
}
});
第 19 至 37 行中的函数已在第 6.1 节中介绍过。它们负责处理页面的国际化。此处不再赘述。在第 1 至 17 行中,我们手动执行 Ajax 调用,而此前 [计算] 按钮的 Ajax 调用是由项目的 Ajax 库处理的。为此,我们使用了项目的 jQuery 库。
- 第 3 行:引用 ID 为 [loading] 的组件。[$("#loading")] 返回所有 ID 为 [loading] 的元素集合。这里只有一个;
- 第 4 行:引用 ID 为 [form] 的组件;
- 第 5 行:引用 ID 为 [results] 的组件;
- 第 6 行:包含选项的 Ajax 调用;
- 第 7 行:Ajax 调用的目标 URL;
- 第 8 行:使用的 HTTP 方法;
- 第 9 行:提交的数据。[form.serialize] 为 ID 为 [form] 的表单生成 POST 字符串 [A=val1&B=val2];
- 第 10 行:响应中预期的数据类型。我们知道服务器将返回一个 HTML 流;
- 第 11 行:请求开始时执行的方法。此处指定应显示 ID 为 [loading] 的组件。这是动画加载图标;
- 第 12 行:Ajax 请求成功时执行的方法。[data] 参数是来自服务器的完整响应。我们知道这是一个 HTML 流;
- 第 13 行:隐藏加载指示器;
- 第 14 行:使用 [data] 参数中的 HTML 内容更新 ID 为 [results] 的组件。
欢迎读者测试 [Calculate] 链接。其功能与 [Calculate] 按钮完全相同,唯一的区别在于错误消息显示为“ ”。使用该链接后,即可为 A 和 B 输入无效值:
![]() |
- 在 [1] 和 [2] 中,我们输入了无效值。这些值被客户端验证器标记为错误;
- 在 [3] 中,我们点击了 [计算] 链接;
- 在 [4] 中,由于我们收到了响应 [4],因此发生了 [POST] 请求。
当值无效且我们点击 [计算] 按钮时,不会向服务器发送 [POST] 请求。 在相同场景下,使用 [计算] 链接会触发向服务器的 [POST] 请求。因此,[计算] 按钮存在一种行为,而我们无法通过 [计算] 链接复现该行为。我们暂不尝试解决此问题,而是将其留待后续示例中处理,该示例还将说明另一个客户端验证问题。
7.4. 使用 JSON 数据流更新 HTML 页面
在前一个示例中,Web 服务器通过 HTML 数据流响应了 Ajax HTTP 请求。该数据流包含带有 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 行:请求失败时要执行的 JavaScript 函数;
- 第 12 行:请求成功时执行的 JavaScript 函数;
- 第 13 行:Ajax 请求返回结果(失败或成功)后要执行的 JavaScript 函数;
- 第 40–43 行:ID 为 [header] 的区域;
- 第 44–49 行:ID 为 [results] 的区域。该区域将显示四则运算的结果;
- 第 50–52 行:ID 为 [error] 的区域。该区域将显示任何错误信息。
7.4.2. [Action02Post] 操作
该 Ajax 请求由以下 [Action02Post] 操作处理:
[HttpPost]
public JsonResult Action02Post(FormCollection postedData, SessionModel session)
{
// wait simulation
Thread.Sleep(2000);
// model validation
ViewModel02 modèle = new ViewModel02();
// loading and calculation times
string HeureChargement = DateTime.Now.ToString("hh:mm:ss");
string HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
// model update
TryUpdateModel(modèle, postedData);
if (!ModelState.IsValid)
{
// returns an error
return Json(new { Erreur = getErrorMessagesFor(ModelState), HeureCalcul = HeureCalcul });
}
// every other time, an error is simulated
int val = session.Randomizer.Next(2);
if (val == 0)
{
// returns an error
return Json(new { Erreur = "[erreur aléatoire]", HeureCalcul = HeureCalcul });
}
// calculations
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);
// we return the results
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] 文件中定义:
// global data
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 loading
$(document).ready(function () {
formulaire = $("#formulaire");
entete = $("#entete");
loading = $("#loading");
erreur = $("#erreur");
résultats = $('#résultats');
heureCalcul = $("#heureCalcul");
msg = $("#msg");
AplusB = $("#AplusB");
AmoinsB = $("#AmoinsB");
AmultipliéparB = $("#AmultipliéparB");
AdiviséparB = $("#AdiviséparB");
// hide certain page elements
entete.hide();
résultats.hide();
erreur.hide();
});
// start
function OnBegin() {
....
}
// end of query
function OnComplete() {
...
}
// success
function OnSuccess(data) {
....
}
// error
function OnFailure(request, error) {
...
}
- 第 19 行:页面在浏览器中加载完成时执行的 JS 函数;
- 第 20–30 行:我们获取所有感兴趣的页面组件的引用。在页面上搜索组件会消耗资源,因此最好只执行一次;
- 第 33–35 行:将 [header]、[results] 和 [loading] 组件隐藏;
当 Ajax 请求开始时,将执行以下函数:
// start
function OnBegin() {
// wait signal on
loading.show();
// hide certain page elements
entete.hide();
résultats.hide();
erreur.hide();
}
- 第 4 行:显示 [loading] 组件。这是动画图像;
- 第 6–8 行:隐藏 [header]、[results] 和 [error] 组件;
如果 Ajax 请求成功,将执行以下 JS 代码:
// réussite
function OnSuccess(data) {
// affichage résultats
heureCalcul.text(data.HeureCalcul);
entete.show();
if (data.Erreur != '') {
msg.text(data.Erreur);
erreur.show();
return;
}
// pas d'erreur
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],则可以通过 [data.Error] 或 [data["Error"]] 这种写法来获取 [Error] 字段的值,具体取决于需求。JSON 字符串中的其他字段也遵循同样的规则。此外,若要将未格式化的文本赋值给 ID 为 X 的组件,我们写 [X.text(string)]。让我们回到 [OnSuccess] 函数的代码:
- 第 2 行:[data] 是接收到的 JSON 字符串;
- 第 4 行:[calculationTime] 组件获取其值;
- 第 5 行:显示 [entete] 组件;
- 第 6 行:检查 JSON 字符串的 [Error] 字段;
- 第 7 行:[msg] 组件获取其值;
- 第 8 行:显示 [error] 组件;
- 第 9 行:错误情况处理完毕;
- 第 12 行:[AplusB] 组件获取其值;
- 第 13 行:[AminusB] 组件获取其值;
- 第 14 行:[AmultipliedbyB] 组件获取其值;
- 第 15 行:为 [AdividedbyB] 组件赋值;
- 第 16 行:显示 [results] 组件。
如果 Ajax HTTP 请求失败,将执行 [ OnFailure] 函数。该失败由服务器返回的 HTTP 状态码决定。例如,500 [Internal Server Error] 代码表示服务器无法处理该请求。[OnFailure] 函数如下:
// error
function OnFailure(request, error) {
alert("L'erreur suivante s'est produite :" + error);
}
我们只是显示了一个包含发生错误的对话框。实际上,我们应该提供更具体的错误信息。我们很快会提出另一种解决方案。
最后,[OnComplete] 函数会在请求完成时执行,无论请求成功还是失败。
// end of query
function OnComplete() {
// wait signal off
loading.hide();
}
请注意,正是 [Action02Get.cshtml] 视图中 Ajax 调用的配置导致了这些函数的调用:
AjaxOptions ajaxOpts = new AjaxOptions
{
...
OnBegin = "OnBegin",
OnFailure = "OnFailure",
OnSuccess = "OnSuccess",
OnComplete = "OnComplete"
};
7.4.4. [Calculate] 链接
[Action02Get.cshtml] 视图中 [Calculate] 链接的 HTML 代码如下:
<a href="javascript:postForm()">Calculer</a>
JS 函数 [postForm] 位于导入的文件 [myScripts-02.js] 中:
<script type="text/javascript" src="~/Scripts/myScripts-02.js"></script>
其代码如下:
function postForm() {
// make a manual Ajax call with JQuery
$.ajax({
url: '/Premier/Action02Post',
type: 'POST',
data: formulaire.serialize(),
dataType: 'json',
beforeSend: OnBegin,
success: OnSuccess,
error: OnFailure,
complete: OnComplete
})
}
我们已经遇到过类似的代码。
- 第 4 行:Ajax 调用的目标 URL;
- 第 5 行:Ajax 调用使用的 HTTP 方法;
- 第 6 行:提交的值。这些是表单值序列化后的结果。通过 id [form] 标识的表单由变量 [form] 引用。[data] 将是一个字符串,格式为 [A=val1&B=val2];
- 第 7 行:预期的响应格式类型。这是一个 JSON 字符串;
- 第 8 行:Ajax 调用开始时要执行的 JS 函数;
- 第 9 行:Ajax 调用成功时执行的 JS 函数;
- 第 10 行:Ajax 调用失败时执行的 JS 函数;
- 第 11 行:在收到服务器响应后执行的 JavaScript 函数,无论响应成功还是失败。
让我们重新审视处理 Ajax 调用失败情况的 JavaScript 函数(第 10 行)。Ajax 调用会在多种情况下失败,例如当服务器返回 [403 禁止访问]、[404 未找到]、[500 服务器内部错误]、[301 永久重定向] 等错误代码时……
在上一个示例中,[OnFailure] 函数如下:
// error
function OnFailure(request, error) {
alert("L'erreur suivante s'est produite :" + error);
}
通常,显示 [error] 对象并不会提供任何有用的信息。如果您使用的是通过 jQuery 发起的 Ajax 请求,可以使用以下 [OnFailure] 方法;
// error
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 为 [error] 的区域。
为了测试此错误处理机制,我们将在 [Action02Post] 操作中人为地引发一个异常:
[HttpPost]
public JsonResult Action02Post(FormCollection postedData, SessionModel session)
{
// an artificial exception to test the Ajax call error function
throw new Exception();
// wait simulation
Thread.Sleep(2000);
// model validation
...
第 5 行抛出了异常。现在让我们测试该应用程序:
![]() |
我们得到以下响应 [1] 和 [2]:
![]() |
服务器响应让我们能够看到错误发生在服务器代码的哪一行。这通常是有用的信息。从现在起,我们将使用这种方法来处理 Ajax 调用中的错误。
7.5. 单页 Web 应用程序
Ajax 技术使我们能够构建单页应用程序:
- 首屏通过标准浏览器请求加载;
- 后续页面通过 Ajax 调用获取。因此,浏览器的 URL 始终保持不变,也不会加载新页面。此类应用程序被称为单页应用程序(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 链接:
- 链接标签 [第 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 行:[view] 将接收值 [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]中的[第1页]链接将显示[2]。
让我们回到 ASP.NET MVC 应用程序的总体示意图:
![]() |
得益于嵌入 HTML 页面并在浏览器中执行的 JavaScript,我们可以将代码卸载到浏览器,从而实现以下架构:
![]() |
- 在[1]中,ASP.NET MVC Web层已成为访问数据的Web接口,这些数据通常存储在数据库中。返回的视图仅包含数据,而不包含HTML标记,例如XML或JSON数据源;
- 在 [2] 中:浏览器显示由 Web 服务器提供的静态视图(即非动态生成的视图),该服务器可能位于与服务器 [1] 相同的机器上,也可能位于其他机器上。随后,这些静态视图会通过 JavaScript 从 Web 接口 [1] 获取数据进行增强;
- 嵌入 HTML 页面中的 JavaScript 代码可以分层组织:
- [展示]层处理用户交互,
- [DAO] 层通过 Web 服务器 [1] 处理数据访问,
- [业务逻辑]层对应于先前位于服务器[1]上、现已迁移至浏览器[2]的[业务逻辑]层;
这种架构的优势在于它整合了不同的技能体系:
- Web 服务器 [1] 的代码需要 .NET 技能,但不需要 JavaScript、HTML 或 CSS 技能;
- 嵌入在浏览器 [2] 中的代码需要掌握 JavaScript、HTML 和 CSS,但对 Web 服务器 [1] 所采用的技术不作限制。
因此,这种架构便于拥有不同技能的团队并行协作。它特别适合单页应用程序。
7.6. 单页 Web 应用程序与客户端验证
我们之前曾提到 Ajax-01 示例中存在一个异常情况。以下是相关背景的回顾:
![]() |
- 在 [1] 和 [2] 中,输入了无效值。这些值被客户端验证器标记;
- 在 [3] 中,我们点击了 [计算] 链接;
- 在 [4] 处,由于我们收到了响应 [4],因此发生了 [POST] 请求。
当值无效且点击 [计算] 按钮时,不会向服务器发送 [POST] 请求。而在相同场景下,点击 [计算] 链接会触发向服务器的 [POST] 请求。因此,[计算] 按钮存在一种行为,我们无法通过 [计算] 链接复现该行为。
我们将以新的视角重新审视这个示例:该应用程序将包含多个视图,并且属于我们刚刚描述的 [单页应用程序] 类型。
7.6.1. 示例中的视图
该示例包含多个视图:
![]() |
- 在 [1] 中,[Action05Get] 视图;
- 在 [2] 中,部分视图 [Form05];
- 在 [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 行:我们将在 [myScripts-05.js] 中添加自定义的 JavaScript 函数;
- 第 27 行:动画加载图片;
- 第 28–30 行:一个 `id` 为 [content] 的标签。部分视图 [Form05、Success05、Failure05] 将插入在此处;
- 第 29 行:插入片段视图 [Form05]。
视图 [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 为 [form];
- 第 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
{
// form
[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; }
// results
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
{
// the random number generator
public Random Randomizer { get; set; }
}
}
此会话模型已扩展以包含 A 和 B 的值:
using System;
namespace Exemple_03.Models
{
public class SessionModel
{
// the random number generator
public Random Randomizer { get; set; }
// the values of A and 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. 客户端操作 [Calculate]
让我们来分析用户与视图的交互:
![]() |
链接 [1] 是一个 JavaScript 链接:
<a href="javascript:calculer()">Calculer</a>
JavaScript 函数 [calculate] 位于文件 [myScripts-05.js] 中:
<script type="text/javascript" src="~/Scripts/myScripts-05.js"></script>
JavaScript 函数 [calculate] 的代码如下:
// global data
var content;
var loading;
function calculer() {
// first the references on DOM
var formulaire = $("#formulaire");
// then validate the form
if (!formulaire.validate().form()) {
// invalid form - terminated
return;
}
// make a manual Ajax call
$.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) {
// display server response
content.html(jqXHR.responseText);
}
})
}
function retourSaisies() {
...
}
function effacer() {
...
}
// document loading
$(document).ready(function () {
// retrieve the references of the page's various components
loading = $("#loading");
content = $("#content");
// we hide the moving image
loading.hide();
});
- 请注意,JavaScript 代码总是在客户端(即浏览器中)执行;
- 第 44 行:单页初始加载完成时执行的 JS 函数;
- 第 46 行:引用 ID 为 [loading] 的动画图片;
- 第 47 行:引用 ID 为 [content] 的区域。该区域接收部分视图 [Form05, Success05, Failure05];
- 第 2-3 行:将第 46-47 行的变量声明为全局变量,以便其他函数可以访问它们。在页面上搜索元素(第 46-47 行)会消耗资源。如果可以避免,则无需重复此搜索;
- 第 5 行:[calculate] 函数;
- 第 7 行:获取表单的引用。部分视图 [Form05] 已为其赋予 ID [form];
- 第 9 行:此语句运行客户端表单验证器。这正是第 176 页所指出的问题中缺失的部分。该方法由单页应用程序所使用的 [jquery.unobtrusive-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 调用成功,[calculate] 函数将使用服务器发送的 HTML 数据流更新 ID 为 [content] 的区域。
该 HTML 流正是 Ajax 调用所指向的 [Action05FaireCalcul] 操作发送的。此服务器端操作的代码如下:
[HttpPost]
public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
{
// model
ViewModel05 modèle = new ViewModel05();
// calculation time
modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
// model update
TryUpdateModel(modèle, postedData);
if (!ModelState.IsValid)
{
// returns an error
modèle.Erreurs = getListOfMessagesFor(ModelState);
return PartialView("Failure05", modèle);
}
...
}
- 第 1 行:该操作仅接受 [post] 参数;
- 第 2 行:它返回一个部分视图;
- 第 2 行:它接收提交的值(postedData)和会话模型(session)作为参数;
- 第 5 行:创建部分视图模型;
- 第 7 行:用计算时间对其进行更新;
- 第 9 行:我们尝试将提交的值应用到模型上。随后将执行其验证器。有人可能会疑惑,既然客户端验证器会在输入数据无效时阻止 POST 请求,我们为何还要大费周章?事实上,我们无法确定 POST 请求的来源。它可能是由非我们编写的代码发起的。因此,我们必须始终执行服务器端验证;
- 第 10 行:检查验证器是否成功;
- 第 13 行:如果模型无效,我们会将其更新为包含错误列表的状态。关于内部方法 [getListOfMessagesFor] 的细节我们不再赘述,该方法与第 60 页描述的 [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 函数 [calculate](该函数会触发向服务器发送 [Post] 请求以执行操作 [Action05FaireCalcul])会将此 HTML 输出放置在 ID 为 [content] 的区域中。最终效果如下:
![]() |
让我们继续分析 [Action05FaireCalcul] 操作的代码:
[HttpPost]
public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
{
// model
ViewModel05 modèle = new ViewModel05();
...
// we put the values of A and B in session
session.A = modèle.A;
session.B = modèle.B;
// no errors so far
List<string> erreurs = new List<string>();
// every other time, an error is simulated
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);
}
// calculations
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);
// view
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 函数 [calculate] 会触发向服务器发送 [Post] 请求的操作 [Action05FaireCalcul],并将此 HTML 流放置在 ID 为 [content] 的区域中。结果将类似于以下内容:
![]() |
7.6.6. [清除]按钮
JavaScript 链接 [Clear] 会将表单重置为初始状态:


在表单中,JS 链接 [清除] 的定义如下:
<a href="javascript:effacer()">Effacer</a>
JS 函数 [clear] 在 [myScripts-05.js] 文件中定义如下:
// global data
var content;
var loading;
function calculer() {
...
}
function retourSaisies() {
...
}
function effacer() {
// first the references on DOM
var formulaire = $("#formulaire");
var A = $("#A");
var B = $("#B");
// valid values are assigned to entries
A.val("0");
B.val("0");
// then validate the form to make
// any error msg
formulaire.validate().form();
// then assign empty strings to the input fields
A.val("");
B.val("");
}
// document loading
$(document).ready(function () {
// retrieve the references of the page's various components
loading = $("#loading");
content = $("#content");
// we hide the moving image
loading.hide();
});
- 第 15-17 行:获取 DOM(文档对象模型)中各个元素的引用;
- 第 19-20 行:我们在数字 A 和 B 的输入框中设置有效值;
- 第 23 行:运行客户端验证器。由于 A 和 B 的值有效,这将清除可能显示的任何错误信息;
- 第 25–26 行:在数字 A 和 B 的输入字段中输入空字符串;
7.6.7. 客户端操作 [返回输入字段]
JavaScript 链接 [返回输入字段] 允许您在获取结果后返回表单:


在表单中,JS 链接 [返回输入] 定义如下:
<a href="javascript:retourSaisies()">Retour aux saisies</a>
JS 函数 [retourSaisies] 在文件 [myScripts-05.js] 中定义如下:
// global data
var content;
var loading;
function calculer() {
...
}
function retourSaisies() {
// make a manual Ajax call
$.ajax({
url: '/Premier/Action05RetourSaisies',
type: 'POST',
dataType: 'html',
beforeSend: function () {
loading.show();
},
success: function (data) {
content.html(data);
},
complete: function () {
loading.hide();
// IMPORTANT!!! validation
$.validator.unobtrusive.parse($("#formulaire"));
},
error: function (jqXHR) {
content.html(jqXHR.responseText);
}
})
}
function effacer() {
...
}
// document loading
$(document).ready(function () {
// retrieve the references of the page's various components
loading = $("#loading");
content = $("#content");
// we hide the moving image
loading.hide();
});
- 第 11–29 行:一个 Ajax 调用;
- 第 12 行:目标 URL;
- 第 13 行:将通过 HTTP POST 请求进行请求。这是一个不带参数的 POST 请求。这就是为什么没有类似以下这行代码的原因:
这样的行;
- 第 14 行:服务器返回的预期响应是一个 HTML 流;
- 第 18–20 行:该 HTML 流将用于更新 ID 为 [content] 的区域;
服务器操作 [Action05RetourSaisies] 如下:
[HttpPost]
public PartialViewResult Action05RetourSaisies(SessionModel session)
{
// view
return PartialView("Formulaire05", new ViewModel05() { A = session.A, B = session.B });
}
- 第 2 行:该操作方法接收一个会话模型作为参数,我们之前已将输入的 A 和 B 值存储在该模型中;
- 第 5 行:我们返回部分视图 [Form05],其模型类型为 [ViewModel05],并在其中确保将 A 和 B 字段初始化为从会话中获取的 A 和 B 的值;
现在让我们回到 JavaScript 函数 [returnInput] 的代码:
function retourSaisies() {
// make a manual Ajax call
$.ajax({
url: '/Premier/Action05RetourSaisies',
type: 'POST',
dataType: 'html',
beforeSend: function () {
loading.show();
},
success: function (data) {
content.html(data);
},
complete: function () {
loading.hide();
// IMPORTANT!!! validation
$.validator.unobtrusive.parse($("#formulaire"));
},
error: function (jqXHR) {
content.html(jqXHR.responseText);
}
})
}
- 第 13 行:Ajax 调用完成时执行的方法;
- 第 14 行:隐藏动画加载图标;
- 第 16 行:我在网上找到的一条略显晦涩的指令,用于解决以下问题:在 [返回输入] 链接显示的表单中,客户端验证器不再起作用。在搜索 JS 库 [jquery.unobtrusive-ajax] 的相关信息时,我在第 16 行找到了解决方案。它会解析表单,可能是为了激活客户端验证器。
7.7. 使 ASP.NET 应用程序可在互联网上访问
参见第 9.26 节。
7.8. 从单页 APU 应用程序生成原生 Android 应用程序
参见第 9.27 节。








































