Ajax - ASP.NET MVC 4 系列
ASP.NET MVC 框架中包含一组 Ajax 辅助方法,可以用来创建表单和指向控制器操作的链接,它们是异步的,且不用编写任何脚本代码来实现程序的异步性,但需要引入脚本文件 jquery.unobtrusive-ajax.js,MVC 4 应用程序默认在 _Layout 视图中包含这个脚本:
当然,也可以去除它,而在需要的页面上手动引入:
Ajax 的 ActionLink 方法
在 Razor 视图中,可通过 Ajax 属性方法,该方法可创建一个具有异步行为的锚标签:
<div id="dailydeal">
<!--
参数1:链接文本
参数2:要异步调用的操作名称
参数3:AjaxOptions 对象
-->
@Ajax.ActionLink("Click here to see today's special!",
"DailyDeal",
new AjaxOptions
{
UpdateTargetId = "dailydeal",
InsertionMode = InsertionMode.Replace,
HttpMethod = "GET"
}
)
</div>
AjaxOptions 参数指定了发送请求和处理服务器返回的结果的方式,该参数还包括处理错误、显示和加载元素、显示确认对话框等的选项:
服务器端需要有一些响应的代码,此例中为 DailyDeal 操作,这里仅返回一个简单的系统时间字符串:
public string DailyDeal()
{
return DateTime.Now.ToString();
}
HTML 5 特性
Ajax.ActionLink 生成的内容能够获取服务器的响应,并可以直接把新内容移植到页面中,这是如何发生的呢?
如果查看该方法渲染的标记,就会看到如下代码:
<a data-ajax="true"
data-ajax-method="GET"
data-ajax-mode="replace"
data-ajax-update="#dailydeal"
href="/Home/DailyDeal">
Click here to see today's special!
</a>
仔细看,ActionLink 方法中指定的所有设置都被编码成了 HTML 特性,并且大多数特性都有 data-前缀,通常称为 data-特性。
HTML 5 规范为私有应用程序保留了 data-特性,Web 浏览器不会尝试解释它的内容,因此可以放心的把数据交给它,这些数据不会影响页面的显示或渲染。添加 jquery.unobtrusive-ajax.js 文件的目的是查找特定的 data-特性,然后操纵元素使其表现出不同行为。本例中,jQuery 查找了 a[data-ajax]=true 的所有锚标记,脚本识别了该异步元素,它自然就可以读取该元素的其他设置(像替换模式、更新目标、HTTP 方法),还能通过使用 jQuery 连接事件和发送请求来修改该元素的行为。
Ajax 表单
如果要在页面增加一个搜索功能,因为需要由用户的输入而做出相应的反馈,所以必须在页面上放置一个 form 表单,这里放一个异步表单:
@using (Ajax.BeginForm("ArtistSearch", "Home",
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "GET",
OnFailure = "searchFailed",
LoadingElementId = "ajax-loader",
UpdateTargetId = "searchResults"
}
))
{
<input type="text" name="q" />
<input type="submit" value="search" />
<img id="ajax-loader" src="@~Content/Images/ajax-loader.gif" style="display:none" />
}
当用户进行搜索时,浏览器会向 Home 控制器的 ArtistSearch 操作发送异步 GET 请求; 当执行异步请求时,客户端框架会显示 LoadingElementId 指定的元素,通常这个元素会出现一个具有动画效果的图片来告知用户后台正在进行一些处理;如果服务器代码返回一个错误,就意味着 Ajax 辅助方法执行失败,OnFailure 选项会触发预先设置的 js 函数,至少可以提示一个错误信息,让用户知道我们已经尽力了。
function searchFailed() {
$("#searchResults").html("Sorry, there was a problem with the search.");
}
客户端验证
对于数据注解特性来说,ASP.NET MVC 框架的客户端验证是默认开启的。下面介绍 Album 类的 Title 和 Price 属性:
[Required(ErrorMessage = "An Album Title is required")]
[StringLength(160)]
public string Title { get; set; }
[Required(ErrorMessage = "Price is required")]
[Range(0.01, 100.00, ErrorMessage = "Price must be between 0.01 and 100.00")]
public decimal Price { get; set; }
ASP.NET MVC 模型绑定器在设置这些属性时会执行服务器端验证,但同时,这些内置的特性也会触发客户端验证,客户端验证依赖于 jQuery 验证插件。
jQuery 验证
默认情况下,非侵入式 JavaScript 和客户端验证在 ASP.NET MVC 中是启用的,但通过 web.config 文件中的设置也可以改变这些行为:
如果需要实现客户端验证,那么需要一对脚本标签:
第一个 script 是验证插件,jQuery 验证实现了挂接到事件需要的所有逻辑(像提交和焦点事件),此外,还要执行客户端验证规则,该插件提供了丰富的默认验证规则集。
第二个 script 用于 jQuery 验证的 Microsoft 非侵入式适配器。这段脚本中的代码用来获取 ASP.NET MVC 框架发出的元数据,并将这些元数据转换成 jQuery 验证能够理解的数据!
那么,这些元数据从何而来?先看下面的 view 片段:
<p>
@Html.LabelFor(model => model.Title)
@Html.TextBoxFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</p>
<p>
@Html.LabelFor(model => model.Price)
@Html.TextBoxFor(model => model.Price)
@Html.ValidationMessageFor(model => model.Price)
</p>
这里,辅助方法 TextBoxFor 是关键,它为基于元数据的模型构建输入元素,当它看到验证元数据(属性上的数据注解)时,会将这些元数据放入到渲染的 HTML 中:
<p>
<label for="Title">Title</label>
<input data-val="true" data-val-length="字段 Title 必须是最大长度为 160 的字符串。" data-val-length-max="160" data-val-required="An Album Title is required" id="Title" name="Title" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
</p>
<p>
<label for="Price">Price</label>
<input data-val="true" data-val-number="字段 Price 必须是一个数字。" data-val-range="Price must be between 0.01 and 100.00" data-val-range-max="100" data-val-range-min="0.01" data-val-required="Price is required" id="Price" name="Price" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
</p>
再次看到了 data-特性,上述代码中,jquery.validate.unobtrusive 脚本负责使用这个元数据(以 data-val="true"开头)查找元素,并结合 jQuery 验证插件来执行元数据内的验证规则!jQuery 验证可运行每个击键和焦点事件上的规则,给用户提供关于错误值的及时反馈,当出现错误时,验证插件也能阻止表单提交,这也意味着不必在服务器上处理注定要失败的请求。
自定义验证
之前的篇章中,曾经写过自定义验证特性 MaxWordsAttribute 来验证一个字符串中的单词数量:
public class MaxWordsAttribute : ValidationAttribute
{
private readonly int _maxWords;
public MaxWordsAttribute(int maxWords) : base("Too many words in {0}")
{
this._maxWords = maxWords;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value != null)
{
var valueString = value.ToString();
if (valueString.Split(' ').Length > this._maxWords)
{
return new ValidationResult("Too many words!");
}
}
return ValidationResult.Success;
}
}
// 可以启用这个自定义特性
[Required(ErrorMessage = "An Album Title is required")]
[StringLength(160)]
[OAuthMVC.Filters.MaxWords(10)]
public string Title { get; set; }
1. 现在的问题是,这个自定义特性只支持服务器端的验证,而为了支持客户端验证,需要让特性实现接口 System.Web.Mvc.IClientValidatable,IClientValidatable 接口定义了单个方法 GetClientValidationRules,当 ASP.NET MVC 框架使用这个接口查找验证对象时,它会调用 GetClientValidationRules 方法来检索 ModelClientValidationRule 对象序列,这些对象携带有框架发送给客户端的元数据和规则,实现如下:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule();
rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
rule.ValidationParameters.Add("wordcount", _maxWords);
rule.ValidationType = "maxwords";
yield return rule;
}
要实现在客户端执行验证,需要提供:验证失败时的提示消息、允许的单词数的范围、一段用来计算单词数量的 JavaScript 代码标识。这些信息就是代码放进返回规则中的内容(如需在客户端触发多种类型的验证,代码可以返回多个规则)。ASP.NET MVC 框架在客户端上将这些返回的规则序列化为 data-特性:
<input data-val="true"
data-val-length="字段 Title 必须是最大长度为 160 的字符串。"
data-val-length-max="160"
data-val-maxwords="Too many words in Title"
data-val-maxwords-wordcount="10"
data-val-required="An Album Title is required" id="Title" name="Title" type="text" value="" />
验证类型(rule.ValidationType)和所有验证参数的名称(rule.ValidationParameters)必须都是小写,因为它们的值必须能够作为合法的 HTML 特性标识符使用。
2. 先前说过,客户端也需要一段计算单词数量的 JS 代码标识。幸运的是,我们没必要在客户端编写代码来从 data-特性 中挖掘元数据值。为了执行验证工作,需要以下两段脚本代码:
- 适配器:适配器和非侵入式 MVC 扩展一道识别需要的元数据,然后非侵入式扩展帮助从 data-特性 中检索值,并且把数据转换成 jQuery 能理解的格式。
- 验证规则:在 jQuery 用于中被称为验证器。
这两段代码都在同一个文件中,假设是 MusicScripts.js 文件,要确保 MusicScripts.js 出现在验证脚本之后:
首先要编写的代码是适配器。MVC 框架的非侵入式验证扩展存储了 jQuery.validator.unobtrusive.adapters 对象中的所有适配器,这些适配器对象公开了一个 API,可以用来添加新的适配器,如下表:
名 称 |
描 述 |
addBool | 为“启用”或“禁止”的验证规则创建适配器,不需要额外参数。 |
addSingleVal | 为需要从元数据中检索唯一参数值的验证规则创建适配器。 |
addMinMax | 创建一个映射到验证规则集的适配器,一个检查最小值,一个检查最大值,且这两个规则中至少有一个要依靠得到的数据运行。 |
Add | 创建一个不适合前面类别的适配器,因为它需要额外参数或额外的设置代码 |
对于最大单词数的情形,可使用 addSingleVal 或 addMinMax (或 Add,因为它适用于任何场合),由于不需要检查单词的最小数量,因此,选择第一个 addSingleVal:
// 参数一:适配器名称,必须与服务器端设置的 ValidationType 值匹配
// 参数二:要从元数据中检索的参数的名称,它匹配服务器的 ValidationParameters 集合的参数名称
$.validator.unobtrusive.adapters.addSingleVal("maxwords", "wordcount");
适配器相对而言比较简单,主要目标是识别非侵入式扩展要定位的元数据。有了适配器,现在就可以编写验证器。所有验证器都在 jQuery.validator 对象中,与 adapters 对象类似,validator 对象也有一个 API 函数,可用来添加新验证器,该函数的名称是:addMethod:
// 参数一:验证器名称,默认情况下,要匹配适配器的名称,而适配器的名称又要匹配服务器端的 ValidationType 值
// 参数二:当验证发生时被调用
// value:用户输入的值,如专辑的名称
// element:输入元素,其中也包含了要验证的值(在 value 本身没有提供足够信息的情况下使用)
// 第三个函数参数:一个所有验证参数的数组,这个示例中包含了单一验证参数(即最大的单词数量)
$.validator.addMethod("maxwords", function (value, element, maxwords) {
if (value) {
if (value.split(' ').length > maxwords) {
return false;
}
}
return true;
});
JSON 劫持
默认情况下,ASP.NET MVC 框架不允许使用 JSON 负载响应 HTTP GET 请求。如果为了响应 GET 请求,需要发送 JSON 格式的数据,就需要使用 JsonRequestBehavior.AllowGet 作为 Json 方法的第二个参数显式的来支持这一操作,例如:
public ActionResult DailyDeal()
{
var album = new Models.Album();
return Json(album, JsonRequestBehavior.AllowGet);
}
然而,这样就给了恶意用户可乘之机,他们可以通过知名的 JSON 劫持进程来获得对 JSON 负载的访问权,因此,不要在 GET 请求中使用 JSON 格式返回敏感信息。请参看这个例子:http://haacked.com/archive/2009/06/25/json-hijacking.aspx
修改搜索表单
尽管 Ajax 辅助方法提供了大量功能,但现在我们删除这些辅助方法,从头开始。jQuery 提供了从服务器检索数据的各种 API,首先修改表单使其直接使用 jQuery 而不使用 Ajax 辅助方法,修改后的 Index.cshtml 视图如下:
<form id="artistSearch" method="get" action="@Url.Action("ArtistSearch","Home")">
<input type="text" name="q" data-autocomplate-source="@Url.Action("QuickSearch","Home")" />
<input type="submit" value="search" />
<img id="ajax-loader" src="@~Content/Images/ajax-loader.gif" style="display:none" />
</form>
显而易见,如果不使用 Ajax 辅助方法,我们就需要自己编写 JS 代码来向服务器请求 HTML:
$("#artistSearch").submit(function (event) {
// 阻止触发默认事件,这里可以阻止表单直接提交到服务器
event.preventDefault();
var form = $(this);
$.getJSON(form.attr("action"), form.serialize(), function (data) {
// data 是服务器返回的 JSON 数据,这里可以做任何事,包括渲染客户端模板(如 Mustache 等)
});
});
form.attr("action") 可以使用 action 的特性值得到正确的 URL, form.serialize() 将表单内部所有输入值连接成一个字符串来构建数据,这里的例子是“q=xxx”,发出一个请求后,将得到的 JSON 响应反序列化为一个对象,然后作为参数来调用一个回调函数,回调函数内部可以做你想做的事。
使用 jQuery.ajax 获得最大灵活性
当要实现对 Ajax 请求的完全控制时,可以使用 jQuery.ajax 方法。该方法只使用一个参数,但可以指定 HTTP 动词(如GET、POST、DELETE、PUT等)、超时、错误处理程序等。所有其它的异步通信方法最终都调用了 ajax 方法!如:
$("#artistSearch").submit(function (event) {
// 阻止触发默认事件,这里可以阻止表单直接提交到服务器
event.preventDefault();
var form = $(this);
$.ajax({
url: form.attr("action"),
data: form.serialize(),
beforeSend: function () {
$("#ajax-loader").show();
},
complete: function () {
$("#ajax-loader").hide();
},
error: searchResults,
success: function (data) {
var html = Mustache.to_html($("#artistTemplate").html(), { artist: data });
$("#searchResults").empty().append(html);
}
});
});
调用 ajax 方法是繁琐的,因为需要自定义很多设置。url 和 data 属性就像是传递给 load 和 getJSON 方法的参数;回调期间又分别显示和隐藏了 gif 动画;即便调用服务器导致失败,jQuery 都将调用 complete 指定的回调函数;error 和 success 中只会有一个可以调用成功。