《Pro ASP.NET MVC 3 Framework》学习笔记之三十二 【无入侵的Ajax】-attach

Ajax是Asynchronous JavaScript and XML的缩写,正如我们看到的,XML部分已经不再像过去那样重要,但是异步的部分却让Ajax非常有用。它是一种在后台从服务端请求数据的模型,而不用重新加载网页。

使用MVC无入侵的Ajax(Using MVC Unobtrusive Ajax)

MVC框架包含了对无入侵的Ajax的支持,而且是基于jQuery库的。下面创建示例项目UnobtrusiveAjax,如下:

View Code

//model
using System.ComponentModel.DataAnnotations;

namespace UnobtrusiveAjax.Models
{
    public class Appointment
    {
        public string ClientName { get; set; }
        [DataType(DataType.Date)]
        public DateTime Date { get; set; }
        public bool TermsAccepted { get; set; }
    }
}
//Controller
using System.Web.Mvc;
using UnobtrusiveAjax.Models;

namespace UnobtrusiveAjax.Controllers
{
    public class AppointmentController : Controller
    {
        //
        // GET: /Appointment/

        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(string id)
        {
            return View("Index", (object)id);
        }

        public ViewResult AppointmentData(string id)
        {
            IEnumerable data = new[] {
                 new Appointment { ClientName = "张三", Date = DateTime.Parse("1/1/2012")}, 
                 new Appointment { ClientName = "李四", Date = DateTime.Parse("2/1/2012")}, 
                 new Appointment { ClientName = "王五", Date = DateTime.Parse("3/1/2012")}, 
                 new Appointment { ClientName = "赵六", Date = DateTime.Parse("1/20/2012")}, 
                 new Appointment { ClientName = "田七", Date = DateTime.Parse("1/22/2012")}, 
                 new Appointment {ClientName = "黄九", Date = DateTime.Parse("2/25/2012")}, 
                 new Appointment {ClientName = "路人甲", Date = DateTime.Parse("2/25/2013")}
            };
            if (!string.IsNullOrEmpty(id) && id != "All")
            {
                data = data.Where(e => e.ClientName == id);
            }
            return View(data);
        }
    }
}

//Index.cshtml
@model string
@{
    ViewBag.Title = "Index";
}

Appointment List

@using (Html.BeginForm()) { "tabledata"> @Html.Action("AppointmentData", new { id = Model })
Client Name Appointment Date

@Html.DropDownList("id", new SelectList(new[] { "All", "张三", "李四", "王五" }, (Model ?? "All"))) "submit" value="Submit" />

} //AppointmentData.cshtml 部分视图 @using UnobtrusiveAjax.Models @model IEnumerable @foreach (Appointment appt in Model) { @Html.DisplayFor(m => appt.ClientName) @Html.DisplayFor(m => appt.Date) }

运行效果如下:


上面没有用到Ajax,仅仅是从dropdownlist里面选择一项然后提交,浏览器会将form表单传递给服务器,程序呈现一个经过筛选的视图并发送给浏览器。

启用/禁用无入侵的Ajax(Enabling and Disabling Unobtrusive Ajax)

处理启用/禁用Ajax的过程类似于前面对客户端验证的处理,配置如下:

 
     
     
 

我们还可以在controller,view或Global.asax里面通过程序控制。此外我们必须引入必要的js库,最简便的方式就是在_layout里面:

View Code




    @ViewBag.Title
    "@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    
    


    @RenderBody()

使用无入侵的Ajax表单(Using Unobtrusive Ajax Forms)

Ajax最常用的地方就是处理HTML表单,创建一个启用Ajax的表单需要两步:①创建一个AjaxOptions对象,通过它来指定具体的Ajax请求的行为。②替换Html.BeginForm为Ajax.BeginForm,如下:

View Code

@model string
@{
    ViewBag.Title = "Index";
    AjaxOptions ajaxOpts = new AjaxOptions { UpdateTargetId = "tabledata" };
}

Appointment List

@using (Ajax.BeginForm("AppointmentData",ajaxOpts)) { "tabledata"> @Html.Action("AppointmentData", new { id = Model })
Client Name Appointment Date

@Html.DropDownList("id", new SelectList(new[] { "All", "张三", "李四", "王五" }, (Model ?? "All"))) "submit" value="Submit" />

}

AjaxOptions类有一套属性让我们能够配置对服务器如何进行异步请求以及如何处理返回的数据。下面是列举的属性:
Confirm:在请求之前发送一个确认对话框给用户
HttpMethod:设置请求的HTTP方法(Get/Post)
InsertionMode:指定从服务器检索的内容插入HTML的方式,有三种:①InsertAfter  ②InsertBefore  ③Replace(默认)
LoadingElementId:指定在Ajax请求执行展示的HTML元素的ID
LoadingElementDuration:指定通过LoadingElementId设置的元素展示的时间(毫秒)
UpdateTargetId:设置从服务器检索的内容插入到HTML元素的ID
Url:设置请求的URL

在上面的例子中,我们设置的UpdateTargetId属性是”tabledata”,这个Id指向了tbody元素。当从服务器获取到内容时,我们从AppointmentData action方法获取的数据会被替换掉(当我们从下拉框里选择一个人筛选,提交以后原来table里面的行会被替换成筛选以后的数据)。

理解无入侵的Ajax的工作原理(Understanding How Unobtrusive Ajax Works)

可以先看下页面的部分源码:

data-ajax=”true” data-ajax-mode=”replace” data-ajax-update=”#tabledata” id=”form0″ method=”post”> 
jquery.unobtrusive-ajax.js会扫描HTML Dom并且通过寻找值为true的data-ajax属性来识别Ajax表单。其他的指定的以data-ajax开始的属性的值使用AjaxOptions类,这些配置项被用于配置jQuery。
跟前面使用客户端验证一样,Ajax的使用是可选的。我可以直接使用jQuery或其他的js库,或者直接写js调用。但是,推荐不要在同一个View里面混合使用,因为这样可能会有一些糟糕的交互,如复制或丢弃Ajax请求。

禁用无入侵的Ajax的影响(THE EFFECT OF DISABLING UNOBTRUSIVE AJAX)

在MVC之前的版本对Ajax的支持不是无入侵的。而是添加关于如何执行Ajax请求的详情(包括使用的JSON数据和js代码)到HTML,如果我们禁用了无入侵的Ajax功能,调用Ajax方法将会使用旧的方式。例如,下面是禁用了以后生成的form元素:

"/Appointment/AppointmentData" id="form0" method="post" onclick="Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));" 
onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event),
{insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: 'tabledata' });
">

 另外,还有下面的脚本也会添加到HTML:

 

这些添加的内容必须是在视图里面引入了MicrosoftAjax.js和MicrosoftMvcAjax.js库才会起作用,否则form表单会同步提交到服务器而不是异步的。

设置Ajax选项(Setting Ajax Options)

我可以通过设置AjaxOptions对象的属性值来调整ajax请求的行为。

确保优雅的降级(Ensuring Graceful Degradation)(这里不知道怎么翻译合适,请大家指导)

但我们设置了启用了Ajax的表单时,传递了一个action方法名参数。在上面的例子里面是AppointmentData方法。使用这种方式的一个问题就是如果用户禁用了js或者浏览器不支持js,那么它就不能很好的运行了。浏览器会丢弃当前的HTML页并用指定的action方法(这里就是AppointmentData)的返回结果替换。想简单的展现这样的类似的效果,可以直接请求~/Appointment/AppointmentData。
下面是使用AjaxOptions.Url来保证比较优雅的降级(当禁用了js的时候)

//加粗的部分是修改后的
@model string @{ ViewBag.Title = "Index"; AjaxOptions ajaxOpts = new AjaxOptions { UpdateTargetId = "tabledata",Url=Url.Action("AppointmentData") }; }

Appointment List

@using (Ajax.BeginForm(ajaxOpts)) {...

运行程序,禁用js,点击提交页面会正常显示,虽然不是异步的。如果不做上面的修改,直接禁用掉js。效果跟上面的直接请求~/Appointment/AppointmentData是一样的。
我们使用Url.Action方法创建一个调用了AppointmentData的Url,并使用接收一个AjaxOptions参数的重载。这个版本的重载创建一个回发的表单给呈现视图的action方法,也就是这里的Index。当我们启用了js,则会异步提交,产生好的用户体验。

使用Ajax请求时给用户提供一个反馈(Providing the User with Feedback While Making an Ajax Request)

使用Ajax的一个弊端就是用户不能很明确的知道发生了什么,我们可以使用AjaxOptions.LoadingElementId属性来补救。修改Index.cshtml如下:

View Code

@model string
@{
    ViewBag.Title = "Index";
    AjaxOptions ajaxOpts = new AjaxOptions
    {
        UpdateTargetId = "tabledata",
        Url = Url.Action("AppointmentData"),
        LoadingElementId = "loading",
        LoadingElementDuration = 2000
    };
}

Appointment List

"loading" style="display: none; color: Red; font-weight: bold">

Loading Data...

@using (Ajax.BeginForm(ajaxOpts)) { "tabledata"> @Html.Action("AppointmentData", new { id = Model })
Client Name Appointment Date

@Html.DropDownList("id", new SelectList(new[] { "All", "张三", "李四", "王五" }, (Model ?? "All"))) "submit" value="Submit" />

}

请求之前提示用户(Prompting the User Before Making a Request)

AjaxOptions.Confirm属性可以用在用户异步请求之前给用户一个提示信息。示例如下:

@model string
@{
    ViewBag.Title = "Index";
    AjaxOptions ajaxOpts = new AjaxOptions
    {
        UpdateTargetId = "tabledata",
        Url = Url.Action("AppointmentData"),
        LoadingElementId = "loading",
        LoadingElementDuration = 2000,
        Confirm="你希望请求新的数据吗?"
    };
}

创建Ajax链接(Creating Ajax Links)

不仅可以创建启用了Ajax的表单,还能异步创建锚元素。修改Index.cshtml如下:

View Code

@model string
@{
    ViewBag.Title = "Index";
    AjaxOptions ajaxOpts = new AjaxOptions
    {
        UpdateTargetId = "tabledata",
        Url = Url.Action("AppointmentData"),
        LoadingElementId = "loading"
    };
}

Appointment List

"loading" style="display: none; color: Red; font-weight: bold">

Loading Data...

@using (Ajax.BeginForm(ajaxOpts)) { "tabledata"> @Html.Action("AppointmentData", new { id = Model })
Client Name Appointment Date

@Html.DropDownList("id", new SelectList(new[] { "All", "张三", "李四", "王五" }, (Model ?? "All"))) "submit" value="Submit" />

} @foreach (string str in new[] { "All", "张三", "李四", "王五" }) {
"margin-right: 5px; float: left"> @Ajax.ActionLink(str, "AppointmentData", new { id = str }, new AjaxOptions { UpdateTargetId = "tabledata", LoadingElementId = "loading", })
}

我们使用了一个foreach循环来创建一个div,里面包含的项跟下拉框里面是一样的。生成的HTML为:

当我点击下面生成的链接时,效果跟上面选择一个下拉框的选项提交的效果一样。

对链接确保优雅降级(Ensuring Graceful Degradation for Links)

类似与前面针对表单的优雅降级,如果js被禁用了,点击上面生成的链接,会跟请求~/Appointment/AppointmentData一样。可以同样使用
Url = Url.Action(“AppointmentData”, new { id = str}) 来处理。修改Index.cshtml如下:

@foreach (string str in new[] { "All", "张三", "李四", "王五" })
{
    
"margin-right: 5px; float: left"> @Ajax.ActionLink(str, "Index", new { id = str }, new AjaxOptions { UpdateTargetId = "tabledata", LoadingElementId = "loading", Url = Url.Action("AppointmentData", new { id = str }) })
}

通过为每一个链接创建一个单独的AjaxOptions对象,我们可以指定路由的值以至于每一个链接都是一个单一的请求。这非常有用,因为我们的链接不是Form的一部分(查看源代码可以看到div在form表单外面)。我们还需要修改下控制器。当javascript禁用的时候,点击链接产生一个Get请求,会直接导向没有参数的Index控制器,而这个控制器是没有任何数据筛选逻辑的,所以不会对提交产生任何效果。修改如下:

//public ActionResult Index()
//{
//    return View();
//}

//[HttpPost]
public ActionResult Index(string id)
{
     return View("Index", (object)id);
}

使用Ajax回调(Working with Ajax Callbacks)

AjaxOptions类定义了一套属性,让我们指定Ajax请求生命周期的多个点上调用的javascript函数。如下:

  属性名 jQuery事件 描述
OnBegin beforeSend 请求发出之前调用
OnComplete complete 请求成功以后调用
OnFailure error 请求失败以后调用
OnSuccess success 请求完成后调用,不考请求是否成功或失败

 

 

 

每一个Ajax回调属性关联了一个jQuery支持的Ajax事件,想了解更多的内容可以点击这里

使用Ajax回调的第一步就是创建我们想调用的javascript函数。我们在_Layout.cshtml里面添加了script元素,如下:

View Code




    @ViewBag.Title
    "@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    
    
    


    @RenderBody()

我们定义了4个函数,每一个针对一个回调。为了将这些函数跟回调挂钩,我们指定函数的名称跟AjaxOptions属性的值一致,如下:

@foreach (string str in new[] { "All", "张三", "李四", "王五" })
{
    
"margin-right: 5px; float: left"> @Ajax.ActionLink(str, "Index", new { id = str }, new AjaxOptions { UpdateTargetId = "tabledata", LoadingElementId = "loading", Url = Url.Action("AppointmentData", new { id = str }), OnBegin = "OnBegin", OnFailure = "OnFailure", OnSuccess = "OnSuccess", OnComplete = "OnComplete" })
}

运行程序,点击一个链接会看到一序列的窗口如下:

使用JSON(Working with JSON)

到目前为止,服务端都是呈现一个HTML片段并发送给浏览器,这是最常使用和被接受的技术,但是如果我们想使用服务端返回的数据做更多的事情,需要使用JSON数据格式来方便的我们的操作。了解JSON

添加JSON支持到控制器(Adding JSON Support to the Controller)

MVC里面生成JSON数据的action方法非常简便,如下例:

View Code

using System.Web.Mvc;
using UnobtrusiveAjax.Models;

namespace UnobtrusiveAjax.Controllers
{
    public class AppointmentController : Controller
    {
        //
        // GET: /Appointment/

        //public ActionResult Index()
        //{
        //    return View();
        //}

        //[HttpPost]
        public ActionResult Index(string id)
        {
            return View("Index", (object)id);
        }

        public ViewResult AppointmentData(string id)
        {
            IEnumerable data = new[] {
                 new Appointment { ClientName = "张三", Date = DateTime.Parse("1/1/2012")}, 
                 new Appointment { ClientName = "李四", Date = DateTime.Parse("2/1/2012")}, 
                 new Appointment { ClientName = "王五", Date = DateTime.Parse("3/1/2012")}, 
                 new Appointment { ClientName = "赵六", Date = DateTime.Parse("1/20/2012")}, 
                 new Appointment { ClientName = "田七", Date = DateTime.Parse("1/22/2012")}, 
                 new Appointment {ClientName = "黄九", Date = DateTime.Parse("2/25/2012")}, 
                 new Appointment {ClientName = "路人甲", Date = DateTime.Parse("2/25/2013")}
            };
            if (!string.IsNullOrEmpty(id) && id != "All")
            {
                data = data.Where(e => e.ClientName == id);
            }
            return View(data);
        }

        public JsonResult JsonData(string id)
        {
            IEnumerable data = new[] {
                 new Appointment { ClientName = "张三", Date = DateTime.Parse("1/1/2012")}, 
                 new Appointment { ClientName = "李四", Date = DateTime.Parse("2/1/2012")}, 
                 new Appointment { ClientName = "王五", Date = DateTime.Parse("3/1/2012")}, 
                 new Appointment { ClientName = "赵六", Date = DateTime.Parse("1/20/2012")}, 
                 new Appointment { ClientName = "田七", Date = DateTime.Parse("1/22/2012")}, 
                 new Appointment {ClientName = "黄九", Date = DateTime.Parse("2/25/2012")}, 
                 new Appointment {ClientName = "路人甲", Date = DateTime.Parse("2/25/2013")}
            };
            if (!string.IsNullOrEmpty(id) && id != "All")
            {
                data = data.Where(e => e.ClientName == id);
            }
            var formattedData = data.Select(m => new
            {
                Client = m.ClientName,
                Date = m.Date.ToShortDateString(),
                TermsAccepted = m.TermsAccepted
            });

            return Json(formattedData, JsonRequestBehavior.AllowGet);
        }
    }
}

我们添加了一个新的action方法JsonData,返回一个JsonResult的对象。通过调用Json()来创建一个JsonResult,把传入的数据转换为Json格式。像这样:
return Json(formattedData, JsonRequestBehavior.AllowGet);

在这个例子中,我们传递了JsonRequestBehavior.AllowGet的枚举值。默认情况下,JSON数据仅仅在响应POST请求时发送,传递这个枚举值就告诉MVC框架响应Get请求并传递Json数据。

注意:我们应该在返回的数据不是私人的,可以公开的数据时使用JsonRequestBehavior.AllowGet。因为这存在一个安全问题,第三方的站点是可以截取响应Get请求时返回的JSON数据,这也是为什么默认情况下只响应Post请求的JSON数据。大多数情况下,我们能够使用POST请求检索JSON数据,并且避免了这个问题。

返回JsonResult的action方法和其他生成HTML的方法有个不同的地方——使用了LINQ创建一个匿名的类型。像这样:
var formattedData = data.Select(m => new
{
    ClientName = m.ClientName,
    Date = m.Date.ToShortDateString(),
    TermsAccepted = m.TermsAccepted
});

有两个理由让我们在这里使用匿名类型的对象:①客户端不需要用TermsAccepted属性,所以也就不需要把它发送到客户端。②经过这样的处理后可以使得客户端处理JSON数据更加简单。当MVC框架使用JSON编码一个Datetime值时,处理的有点奇怪,会呈现下面的内容:
{“ClientName”:”Joe”,”Date”:”/Date(1325376000000)/”,”TermsAccepted”:false}
这个不寻常的日期格式使得在客户端转换为javascript Date对象更加容易,然后进行操作。其实,我们不需要在客户端对Date进行操作,我们只用把它作为字符串展示就OK,并且将Date转为String操作在服务端更加容易。我们可以通过URL来查看生成的JSON数据,如:

在浏览器端处理JSON(Processing JSON in the Browser)

为了处理检索的JSON数据,我们使用OnSuccess回调属性在AjaxOptions类里面指定一个javascript函数,如下:

@foreach (string str in new[] { "All", "张三", "李四", "王五" })
{
    
"margin-right: 5px; float: left"> @Ajax.ActionLink(str, "Index", new { id = str }, new AjaxOptions { LoadingElementId = "loading", Url = Url.Action("JsonData", new { id = str }), OnSuccess = "OnSuccess" })
}

注意:这里没有给UpdateTargetId属性赋值,我们不能依靠无入侵的ajax脚本来处理JSON数据,因为这些数据不是HTML。相反我们需要写一个js函数来处理JSON并生成我们需要的HTML,如下:

View Code

    

在Action方法里探测Ajax请求(Detecting Ajax Requests in the Action Method)

我们不必创建两个action方法来分别生成JSON和HTML,可以探测请求是否是Ajax请求并发送JSON,并同时针对其他的请求发送HTML。下面用AppointmentData来说明:

View Code

        public ActionResult AppointmentData(string id)
        {
            IEnumerable data = new[] {
                 new Appointment { ClientName = "张三", Date = DateTime.Parse("1/1/2012")}, 
                 new Appointment { ClientName = "李四", Date = DateTime.Parse("2/1/2012")}, 
                 new Appointment { ClientName = "王五", Date = DateTime.Parse("3/1/2012")}, 
                 new Appointment { ClientName = "赵六", Date = DateTime.Parse("1/20/2012")}, 
                 new Appointment { ClientName = "田七", Date = DateTime.Parse("1/22/2012")}, 
                 new Appointment {ClientName = "黄九", Date = DateTime.Parse("2/25/2012")}, 
                 new Appointment {ClientName = "路人甲", Date = DateTime.Parse("2/25/2013")}
            };
            if (!string.IsNullOrEmpty(id) && id != "All")
            {
                data = data.Where(e => e.ClientName == id);
            }

            if (Request.IsAjaxRequest())
            {
                return Json(data.Select(m => new
                {
                    ClientName = m.ClientName,
                    Date = m.Date.ToShortDateString()
                }), JsonRequestBehavior.AllowGet);
            }
            else
            {
                return View(data);
            }

        }

此时修改Index部分如下(加粗部分):

@foreach (string str in new[] { "All", "张三", "李四", "王五" })
{
    
"margin-right: 5px; float: left"> @Ajax.ActionLink(str, "Index", new { id = str }, new AjaxOptions { LoadingElementId = "loading", Url = Url.Action("AppointmentData", new { id = str }), OnSuccess = "OnSuccess" })
}

接收JSON数据(Receiving JSON Data)

大多数情况下,我们想发送JSON数据到客户端。我们也能够很容易处理反过来的情况,MVC框架的模型绑定系统能够使用JSON数据绑定到一个model类。下面的例子就是包含一个提交一个JSON对象到action方法的jQuery脚本:

View Code

//HomeController
using System.Web.Mvc;
using UnobtrusiveAjax.Models;

namespace UnobtrusiveAjax.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            return View(new Appointment());
        }

        [HttpPost]
        public ActionResult Index(Appointment app)
        {
            if (Request.IsAjaxRequest())
            {
                return Json(new
                {
                    ClientName = app.ClientName,
                    Date = app.Date.ToShortDateString(),
                    TermsAccepted = app.TermsAccepted
                });
            }
            else
            {
                return View();
            }
        }

    }
}

//Index.cshtml
@using UnobtrusiveAjax.Models
@model Appointment

创建Appointment

@using (Html.BeginForm()) { @Html.EditorForModel() "submit" value="提交" /> }
"results" style="display: none"> 这是你创建的预约:

ClientName: "clienttarget" />

Date: "datetarget" />

Terms Accepted: "termstarget" />

Tip:上面的脚本依赖JSON.stringify函数,这个函数在大多数浏览器可用,但是在IE7及之前版本的浏览器不支持。这时可以引入json2.js库来支持,点这里下载

模型绑定系统能够识别JSON数据并以绑定form数据和querystring数据同样的方式绑定JSON数据,我们不需要做任何特别的操作来接收JSON数据,binder会创建Appointment对象,并使用JSON对象的值来填充属性。然后又把Appointment对象转为JSON返回给客户端。运行程序可以看到效果。

本章的笔记就到这里了,晚安!

发表评论

电子邮件地址不会被公开。 必填项已用*标注