迭代 7 – 添加 Ajax 功能 (VB)

Microsoft

下载代码

在第七次迭代中,我们通过添加对 Ajax 的支持来提高应用程序的响应能力和性能。

生成联系人管理 ASP.NET MVC 应用程序 (VB)

在本系列教程中,我们将从头到尾构建整个联系人管理应用程序。 通过 Contact Manager 应用程序,可以存储联系人列表的联系人信息(姓名、电话号码和电子邮件地址)。

我们通过多次迭代生成应用程序。 每次迭代后,我们都会逐步改进应用程序。 此多迭代方法的目标是使你能够了解每次更改的原因。

  • 迭代 #1 - 创建应用程序。 在第一次迭代中,我们将以最简单的方式创建联系人管理器。 添加了对基本数据库操作的支持:创建、读取、更新和删除 (CRUD) 。

  • 迭代 #2 - 使应用程序外观美观。 在此迭代中,我们通过修改默认 ASP.NET MVC 视图母版页和级联样式表来改进应用程序的外观。

  • 迭代 #3 - 添加表单验证。 在第三次迭代中,我们添加了基本表单验证。 我们会阻止用户在未填写必填表单字段的情况下提交表单。 我们还验证电子邮件地址和电话号码。

  • 迭代 #4 - 使应用程序松散耦合。 在第四次迭代中,我们将利用多种软件设计模式,以便更轻松地维护和修改 Contact Manager 应用程序。 例如,我们将应用程序重构为使用存储库模式和依赖关系注入模式。

  • 迭代 #5 - 创建单元测试。 在第五次迭代中,我们通过添加单元测试使应用程序更易于维护和修改。 我们将模拟数据模型类,并为控制器和验证逻辑生成单元测试。

  • 迭代 #6 - 使用测试驱动开发。 在第六次迭代中,我们通过先编写单元测试,然后针对单元测试编写代码,向应用程序添加新功能。 在此迭代中,我们将添加联系人组。

  • 迭代 #7 - 添加 Ajax 功能。 在第七次迭代中,我们通过添加对 Ajax 的支持来提高应用程序的响应能力和性能。

此迭代

在 Contact Manager 应用程序的此迭代中,我们将重构应用程序以使用 Ajax。 通过利用 Ajax,我们可以提高应用程序的响应能力。 当我们只需要更新页面中的某个区域时,我们可以避免呈现整个页面。

我们将重构索引视图,这样每当有人选择新的联系人组时,就不需要重新显示整个页面。 相反,当某人单击联系人组时,我们将只更新联系人列表,并保留页面的其余部分。

我们还将更改删除链接的工作方式。 我们将显示 JavaScript 确认对话框,而不是显示单独的确认页。 如果确认要删除联系人,则会对服务器执行 HTTP DELETE 操作,以从数据库中删除联系人记录。

此外,我们将利用 jQuery 向索引视图添加动画效果。 从服务器提取新的联系人列表时,将显示动画。

最后,我们将利用 ASP.NET AJAX 框架支持来管理浏览器历史记录。 每当执行 Ajax 调用以更新联系人列表时,我们都会创建历史记录点。 这样,浏览器的向后和向前按钮将正常工作。

为什么使用 Ajax?

使用 Ajax 有很多好处。 首先,向应用程序添加 Ajax 功能可提供更好的用户体验。 在普通 Web 应用程序中,每次用户执行操作时,都必须将整个页面发布回服务器。 每当执行某个操作时,浏览器会锁定,用户必须等待,直到提取并重新显示整个页面。

对于桌面应用程序,这是一种不可接受的体验。 但是,传统上,对于 Web 应用程序,我们生活在这种糟糕的用户体验中,因为我们不知道我们可以做得更好。 我们认为这是 Web 应用程序的一个限制,实际上,它只是我们想象的一个限制。

在 Ajax 应用程序中,无需仅仅为了更新页面而停止用户体验。 相反,你可以在后台执行异步请求来更新页面。 在页面的一部分更新时,不会强制用户等待。

利用 Ajax,还可以提高应用程序的性能。 考虑在没有 Ajax 功能的情况下,Contact Manager 应用程序目前的工作原理。 单击联系人组时,必须重新显示整个索引视图。 必须从数据库服务器检索联系人列表和联系人组列表。 所有这些数据都必须通过网络从 Web 服务器传递到 Web 浏览器。

但是,将 Ajax 功能添加到应用程序后,我们可以避免在用户单击联系人组时重新显示整个页面。 我们不再需要从数据库中获取联系人组。 我们不需要跨网络推送整个索引视图。 通过利用 Ajax,我们减少了数据库服务器必须执行的工作量,并减少了应用程序所需的网络流量量。

不要害怕阿贾克斯

一些开发人员会避免使用 Ajax,因为他们担心下层浏览器。 他们希望确保 Web 应用程序在由不支持 JavaScript 的浏览器访问时仍能正常工作。 由于 Ajax 依赖于 JavaScript,因此一些开发人员会避免使用 Ajax。

但是,如果你对如何实现 Ajax 持谨慎态度,则可以构建同时使用上层和下层浏览器的应用程序。 我们的 Contact Manager 应用程序将适用于支持 JavaScript 的浏览器和不支持 JavaScript 的浏览器。

如果将 Contact Manager 应用程序与支持 JavaScript 的浏览器一起使用,则你将获得更好的用户体验。 例如,单击联系人组时,只会更新显示联系人的页面区域。

另一方面,如果将 Contact Manager 应用程序与不支持 JavaScript (的浏览器一起使用,或者) 禁用了 JavaScript 的浏览器,则用户体验将略差一些。 例如,单击联系人组时,必须将整个“索引”视图发回到浏览器,以便显示匹配的联系人列表。

添加所需的 JavaScript 文件

我们需要使用三个 JavaScript 文件将 Ajax 功能添加到应用程序。 这三个文件都包含在新 ASP.NET MVC 应用程序的 Scripts 文件夹中。

如果计划在应用程序的多个页面中使用 Ajax,那么在应用程序的视图母版页中包含所需的 JavaScript 文件是有意义的。 这样,JavaScript 文件将自动包含在应用程序的所有页面中。

在视图母版页的 <head> 标记中添加以下 JavaScript include:

<script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>
    <script src="../../Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>
    <script src="../../Scripts/jquery-1.2.6.min.js" type="text/javascript"></script>

重构索引视图以使用 Ajax

让我们首先修改索引视图,以便单击联系人组仅更新显示联系人的视图区域。 图 1 中的红色框包含要更新的区域。

仅更新联系人

图 01:仅更新联系人 (单击以查看全尺寸图像)

第一步是将要异步更新的视图部分分隔为单独的部分 (视图用户控件) 。 显示联系人表的“索引”视图部分已移至清单 1 中的部分。

列表 1 - Views\Contact\ContactList.ascx

<%@ Control Language="VB" Inherits="System.Web.Mvc.ViewUserControl(Of ContactManager.Group)" %>
<table class="data-table" cellpadding="0" cellspacing="0">
    <thead>
        <tr>
            <th class="actions edit">
                Edit
            </th>
            <th class="actions delete">
                Delete
            </th>
            <th>
                Name
            </th>
            <th>
                Phone
            </th>
            <th>
                Email
            </th>
        </tr>
    </thead>
    <tbody>
        <% For Each item in Model.Contacts %>
        <tr>
            <td class="actions edit">
                <a href='<%= Url.Action("Edit", New With {.id=item.Id}) %>'><img src="../../Content/Edit.png" alt="Edit" /></a>
            </td>
            <td class="actions delete">
                <a href='<%= Url.Action("Delete", New With {.id=item.Id}) %>'><img src="../../Content/Delete.png" alt="Delete" /></a>
            </td>
            <th>
                <%= Html.Encode(item.FirstName) %>
                <%= Html.Encode(item.LastName) %>
            </th>
            <td>
                <%= Html.Encode(item.Phone) %>
            </td>
            <td>
                <%= Html.Encode(item.Email) %>
            </td>
        </tr>
        <% Next %>
    </tbody>
</table>

请注意,清单 1 中的 部分的模型与索引视图不同。 %@ Page %> 指令中的 <Inherits 属性指定部分继承自 ViewUserControl<Group> 类。

更新后的索引视图包含在清单 2 中。

列表 2 - Views\Contact\Index.aspx

<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage(Of ContactManager.IndexModel)" %>
<%@ Import Namespace="ContactManager" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
<title>Index</title>
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<ul id="leftColumn">
<% For Each item in Model.Groups %>
    <li <%= Html.Selected(item.Id, Model.SelectedGroup.Id) %>>
    <%= Ajax.ActionLink(item.Name, "Index", New With { .id = item.Id }, New AjaxOptions With { .UpdateTargetId = "divContactList"})%>
    </li>
<% Next %>
</ul>
<div id="divContactList">
    <% Html.RenderPartial("ContactList", Model.SelectedGroup) %>
</div>

<div class="divContactList-bottom"> </div>
</asp:Content>

关于清单 2 中更新的视图,应注意两点。 首先,请注意,所有移动到部分的内容都会替换为对 Html.RenderPartial () 的调用。 首次请求索引视图时,将调用 Html.RenderPartial () 方法以显示初始联系人集。

其次,请注意,用于显示联系人组的 Html.ActionLink () 已替换为 Ajax.ActionLink () 。 使用以下参数调用 Ajax.ActionLink () :

<%= Ajax.ActionLink(item.Name, "Index", New With { .id = item.Id }, New AjaxOptions With { .UpdateTargetId = "divContactList"})%>

第一个参数表示要为链接显示的文本,第二个参数表示路由值,第三个参数表示 Ajax 选项。 在这种情况下,我们使用 UpdateTargetId Ajax 选项指向要在 Ajax 请求完成后更新的 HTML <div> 标记。 我们希望使用 <新的联系人列表更新 div> 标记。

联系人控制器的更新的 Index () 方法包含在清单 3 中。

列表 3 - Controllers\ContactController.vb (Index 方法)

Public Function Index(ByVal id As Integer?) As ActionResult
    ' Get selected group
    Dim selectedGroup = _service.GetGroup(id)
    if IsNothing(selectedGroup) Then
        Return RedirectToAction("Index", "Group")
    End If

    ' Normal Request
    if Not Request.IsAjaxRequest() Then
        Dim model As new IndexModel With { _
            .Groups = _service.ListGroups(), _
            .SelectedGroup = selectedGroup _
        }
        Return View("Index", model)
    End If

    ' Ajax Request
    return PartialView("ContactList", selectedGroup)
End Function

更新的 Index () 操作有条件地返回以下两个项之一。 如果 Index () 操作由 Ajax 请求调用,则控制器将返回部分。 否则,Index () 操作将返回整个视图。

请注意,Index () 操作在由 Ajax 请求调用时不需要返回尽可能多的数据。 在普通请求的上下文中,“索引”操作返回所有联系人组和所选联系人组的列表。 在 Ajax 请求的上下文中,Index () 操作仅返回所选组。 Ajax 意味着数据库服务器上的工作量更少。

修改后的索引视图适用于上级和下层浏览器。 如果单击联系人组,并且浏览器支持 JavaScript,则仅更新包含联系人列表的视图区域。 另一方面,如果浏览器不支持 JavaScript,则会更新整个视图。

更新后的索引视图有一个问题。 单击联系人组时,不会突出显示所选组。 由于组列表显示在 Ajax 请求期间更新的区域之外,因此不会突出显示正确的组。 我们将在下一部分解决此问题。

添加 jQuery 动画效果

通常,单击网页中的链接时,可以使用浏览器进度栏检测浏览器是否正在主动提取更新的内容。 另一方面,执行 Ajax 请求时,浏览器进度栏不显示任何进度。 这会使用户感到紧张。 如何知道浏览器是否已冻结?

可通过多种方式向用户指示在执行 Ajax 请求时正在执行工作。 一种方法是显示简单的动画。 例如,可以在 Ajax 请求开始时淡出区域,并在请求完成时淡出该区域。

我们将使用 Microsoft ASP.NET MVC 框架附带的 jQuery 库来创建动画效果。 更新后的索引视图包含在清单 4 中。

列表 4 - Views\Contact\Index.aspx

<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage(Of ContactManager.IndexModel)" %>
<%@ Import Namespace="ContactManager" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
<title>Index</title>
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<script type="text/javascript">

    function beginContactList(args) 
    {
        // Highlight selected group
        $('#leftColumn li').removeClass('selected');
        $(this).parent().addClass('selected');

        // Animate
        $('#divContactList').fadeOut('normal');
    }

    function successContactList() 
    {
        // Animate
        $('#divContactList').fadeIn('normal');
    }

    function failureContactList()
    {
        alert("Could not retrieve contacts.");
    }

</script>

<ul id="leftColumn">
<% For Each item in Model.Groups %>
    <li <%= Html.Selected(item.Id, Model.SelectedGroup.Id) %>>
    <%= Ajax.ActionLink(item.Name, "Index", New With { .id = item.Id }, New AjaxOptions With { .UpdateTargetId = "divContactList", .OnBegin = "beginContactList", .OnSuccess = "successContactList", .OnFailure = "failureContactList" })%>
    </li>
<% Next %>
</ul>
<div id="divContactList">
    <% Html.RenderPartial("ContactList", Model.SelectedGroup) %>
</div>

<div class="divContactList-bottom"> </div>
</asp:Content>

请注意,更新后的索引视图包含三个新的 JavaScript 函数。 前两个函数使用 jQuery 在单击新联系人组时淡出和淡入联系人列表中。 当 Ajax 请求导致错误时,第三个函数会显示错误消息, (例如网络超时) 。

第一个函数还负责突出显示所选组。 将 class= selected 属性添加到父元素 (单击的元素的 LI 元素) 。 同样,jQuery 可以轻松选择正确的元素并添加 CSS 类。

这些脚本在 Ajax.ActionLink () AjaxOptions 参数的帮助下绑定到组链接。 更新后的 Ajax.ActionLink () 方法调用如下所示:

<%= Ajax.ActionLink(item.Name, "Index", New With { .id = item.Id }, New AjaxOptions With { .UpdateTargetId = "divContactList", .OnBegin = "beginContactList", .OnSuccess = "successContactList", .OnFailure = "failureContactList" })%>

添加浏览器历史记录支持

通常,单击链接以更新页面时,浏览器历史记录会更新。 这样,就可以单击浏览器的“后退”按钮,将时间移回到页面的上一状态。 例如,如果单击“好友”联系人组,然后单击“商务联系人”组,则可以单击浏览器的“返回”按钮,以导航回选择“好友”联系人组时的页面状态。

遗憾的是,执行 Ajax 请求不会自动更新浏览器历史记录。 如果单击某个联系人组,并且通过 Ajax 请求检索匹配的联系人列表,则不会更新浏览器历史记录。 选择新的联系人组后,不能使用浏览器的“后退”按钮导航回联系人组。

如果希望用户在执行 Ajax 请求后能够使用浏览器的“后退”按钮,则需要执行更多的工作。 你需要利用 ASP.NET AJAX Framework 中内置的浏览器历史记录管理功能。

ASP.NET AJAX 浏览器历史记录中,需要执行三项操作:

  1. 通过将 enableBrowserHistory 属性设置为 true 来启用浏览器历史记录。
  2. 通过调用 addHistoryPoint () 方法在视图状态更改时保存历史记录点。
  3. 在引发导航事件时重新构造视图的状态。

更新后的索引视图包含在清单 5 中。

列表 5 - Views\Contact\Index.aspx

<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage(Of ContactManager.IndexModel)" %>
<%@ Import Namespace="ContactManager" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
<title>Index</title>
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<script type="text/javascript">

    var _currentGroupId = -1;

    Sys.Application.add_init(pageInit);

    function pageInit() {
        // Enable history
        Sys.Application.set_enableHistory(true);

        // Add Handler for history
        Sys.Application.add_navigate(navigate);
    }

    function navigate(sender, e) {
        // Get groupId from address bar
        var groupId = e.get_state().groupId;

        // If groupId != currentGroupId then navigate
        if (groupId != _currentGroupId) {
            _currentGroupId = groupId;
            $("#divContactList").load("/Contact/Index/" + groupId);
            selectGroup(groupId);
        }
    }

    function selectGroup(groupId) {
        $('#leftColumn li').removeClass('selected');
        if (groupId)
            $('a[groupid=' + groupId + ']').parent().addClass('selected');
        else
            $('#leftColumn li:first').addClass('selected');
    }

    function beginContactList(args) {
        // Highlight selected group
        _currentGroupId = this.getAttribute("groupid");
        selectGroup(_currentGroupId);

        // Add history point
        Sys.Application.addHistoryPoint({ "groupId": _currentGroupId });

        // Animate
        $('#divContactList').fadeOut('normal');
    }

    function successContactList() {
        // Animate
        $('#divContactList').fadeIn('normal');
    }

    function failureContactList() {
        alert("Could not retrieve contacts.");
    }

</script>

<ul id="leftColumn">
<% For Each item in Model.Groups %>
    <li <%= Html.Selected(item.Id, Model.SelectedGroup.Id) %>>
    <%= Ajax.ActionLink(item.Name, "Index", New With { .id = item.Id }, New AjaxOptions With { .UpdateTargetId = "divContactList", .OnBegin = "beginContactList", .OnSuccess = "successContactList", .OnFailure = "failureContactList" }, New With { .groupid = item.Id })%>
    </li>
<% Next %>
</ul>
<div id="divContactList">
    <% Html.RenderPartial("ContactList", Model.SelectedGroup) %>
</div>

<div class="divContactList-bottom"> </div>
</asp:Content>

在列表 5 中,在 pageInit () 函数中启用了浏览器历史记录。 pageInit () 函数还用于设置 navigate 事件的事件处理程序。 每当浏览器的“前进”或“后退”按钮导致页面状态更改时,都会引发导航事件。

单击联系人组时,将调用 beginContactList () 方法。 此方法通过调用 addHistoryPoint () 方法创建新的历史记录点。 单击的联系人组的 ID 将添加到历史记录中。

组 ID 是从联系人组链接上的 expando 属性检索的。 通过对 Ajax.ActionLink () 的以下调用呈现链接。

<%= Ajax.ActionLink(item.Name, "Index", New With { .id = item.Id }, New AjaxOptions With { .UpdateTargetId = "divContactList", .OnBegin = "beginContactList", .OnSuccess = "successContactList", .OnFailure = "failureContactList" }, New With { .groupid = item.Id })%>

传递给 Ajax.ActionLink () 的最后一个参数将名为 groupid 的 expando 属性添加到链接 (小写,以便实现 XHTML 兼容性) 。

当用户点击浏览器的“后退”或“前进”按钮时,将引发 navigate 事件并调用 navigate () 方法。 此方法更新页面中显示的联系人,以匹配与传递给 navigate 方法的浏览器历史记录点对应的页面状态。

执行 Ajax 删除

目前,若要删除联系人,需要单击“删除”链接,然后单击删除确认页中显示的“删除”按钮 (请参阅图 2) 。 这似乎有很多页面请求来执行一些简单的操作,如删除数据库记录。

删除确认页

图 02:删除确认页 (单击查看全尺寸图像)

跳过删除确认页并直接从“索引”视图中删除联系人是很容易的。 应避免这种诱惑,因为采用此方法会让应用程序出现安全漏洞。 通常,在调用修改 Web 应用程序状态的操作时,不希望执行 HTTP GET 操作。 执行删除时,需要执行 HTTP POST 或更好的 HTTP DELETE 操作。

“删除”链接包含在 ContactList 部分。 清单 6 中包含 ContactList 部分的更新版本。

列表 6 - Views\Contact\ContactList.ascx

<%@ Control Language="VB" Inherits="System.Web.Mvc.ViewUserControl(Of ContactManager.Group)" %>
<%@ Import Namespace="ContactManager" %>
<table class="data-table" cellpadding="0" cellspacing="0">
    <thead>
        <tr>
            <th class="actions edit">
                Edit
            </th>
            <th class="actions delete">
                Delete
            </th>
            <th>
                Name
            </th>
            <th>
                Phone
            </th>
            <th>
                Email
            </th>
        </tr>
    </thead>
    <tbody>
        <% For Each item in Model.Contacts %>
        <tr>
            <td class="actions edit">
                <a href='<%= Url.Action("Edit", New With {.id=item.Id}) %>'><img src="../../Content/Edit.png" alt="Edit" /></a>
            </td>
            <td class="actions delete">
                <%= Ajax.ImageActionLink("../../Content/Delete.png", "Delete", "Delete", New with { .id = item.Id }, New AjaxOptions With { .Confirm = "Delete contact?", .HttpMethod = "Delete", .UpdateTargetId = "divContactList" })%> 
            </td>
            <th>
                <%= Html.Encode(item.FirstName) %>
                <%= Html.Encode(item.LastName) %>
            </th>
            <td>
                <%= Html.Encode(item.Phone) %>
            </td>
            <td>
                <%= Html.Encode(item.Email) %>
            </td>
        </tr>
        <% Next %>
    </tbody>
</table>

通过对 Ajax.ImageActionLink () 方法的以下调用呈现 Delete 链接:

<%= Ajax.ImageActionLink("../../Content/Delete.png", "Delete", "Delete", New with { .id = item.Id }, New AjaxOptions With { .Confirm = "Delete contact?", .HttpMethod = "Delete", .UpdateTargetId = "divContactList" })%<

注意

Ajax.ImageActionLink () 不是 ASP.NET MVC 框架的标准部分。 Ajax.ImageActionLink () 是包含在 Contact Manager 项目中的自定义帮助程序方法。

AjaxOptions 参数有两个属性。 首先,Confirm 属性用于显示弹出的 JavaScript 确认对话框。 其次,HttpMethod 属性用于执行 HTTP DELETE 操作。

清单 7 包含已添加到 Contact 控制器的新 AjaxDelete () 操作。

列表 7 - Controllers\ContactController.vb (AjaxDelete)

<AcceptVerbs(HttpVerbs.Delete), ActionName("Delete")> _
Public Function AjaxDelete(ByVal id As Integer) As ActionResult
    ' Get contact and group
    Dim contactToDelete = _service.GetContact(id)
    Dim selectedGroup = _service.GetGroup(contactToDelete.Group.Id)

    ' Delete from database
    _service.DeleteContact(contactToDelete)

    ' Return Contact List
    Return PartialView("ContactList", selectedGroup)
End Function

AjaxDelete () 操作使用 AcceptVerbs 属性进行修饰。 此属性可防止调用操作,除非由 HTTP DELETE 操作以外的任何 HTTP 操作调用。 具体而言,不能使用 HTTP GET 调用此操作。

删除数据库记录后,需要显示不包含已删除记录的已更新联系人列表。 AjaxDelete () 方法返回 ContactList 部分和更新的联系人列表。

总结

在此迭代中,我们向 Contact Manager 应用程序添加了 Ajax 功能。 我们使用 Ajax 来提高应用程序的响应能力和性能。

首先,我们重构了索引视图,以便单击联系人组不会更新整个视图。 相反,单击联系人组只会更新联系人列表。

接下来,我们使用 jQuery 动画效果在联系人列表中淡出和淡出。 向 Ajax 应用程序添加动画可用于为应用程序的用户提供等效的浏览器进度栏。

我们还向 Ajax 应用程序添加了浏览器历史记录支持。 我们允许用户单击浏览器的“后退”和“转发”按钮来更改索引视图的状态。

最后,我们创建了一个支持 HTTP DELETE 操作的删除链接。 通过执行 Ajax 删除,用户可以删除数据库记录,而无需用户请求额外的删除确认页。