领先技术

使用 jQuery 和 ASP.NET Ajax 库实现预取模式

Dino Esposito

上个月,我讨论了如何使用 ASP.NET Ajax 库所附带的新功能实现母版-详细信息视图。这组新功能包括用于客户端实时数据绑定的语法以及丰富呈现组件(例如 DataView 客户端控件)。通过结合使用这些功能,您可轻松构建嵌套视图以表示一对多数据关系。

在 ASP.NET Ajax 库中,母版-详细信息视图的机制主要按 DataView 组件处理和公开其事件的方式在该组件的逻辑代码中定义。

本月我将更进一步,讨论如何以 ASP.NET Ajax 库为基础,实现“预取”这一常见和热门的 AJAX 设计模式。基本上,我将扩展上个月的示例(一个向下钻取客户详细信息的相对标准的视图),以自动和异步方式下载并显示相关订单(如果存在)。在此期间,我将谈及一些 jQuery 相关事项,并了解一下 ASP.NET Ajax 库中的新 jQuery 集成 API。闲话少说,让我们看看上下文并构建第一个版本的示例。

要扩展的演示

图 1 显示了我将以之为基础来添加预取功能的应用程序方案。

图像:示例应用程序的初始阶段

图 1 示例应用程序的初始阶段

用户可通过菜单栏按名称首字母筛选客户。进行选择后,会通过 HTML 项目符号列表显示较小的客户列表。这是母版视图。

呈现的每一项都已成为可选择的项。单击某项会使该客户的详细信息显示在相邻的详细信息视图中。上个月我就讨论到此处。如您在图 1 中所见,用户界面现在显示一个用于查看订单的按钮。我现在从此处开始继续讨论。

要做的第一个决策是关于体系结构的,并与要考虑的用例有关。要如何加载订单信息?这些信息是否已随客户信息一起下载?订单是否附加到客户?此处是否可选择延迟加载?

我们所考虑的代码应在客户端运行,因此不能依赖构建到某些对象/关系建模 (O/RM) 工具(如实体框架或 NHibernate)中的延迟加载功能。如果订单要延迟加载,则所有代码都要由您自己完成。另一方面,如果可以假设订单已在客户端可用(即订单已与客户信息一起下载),则您已完成了大部分工作。接下来只需将订单数据绑定到某个 HTML 模板并执行即可。

显然,如果您需要延迟加载,则事情会变得更加有趣。那么,我们便来处理此方案。

另外,您应知道,如果通过 AdoNetDataContext 对象获得数据,则可完全支持延迟加载。(我会在将来的文章中对此进行讨论。)有关详细信息,请务必查看 asp.net/ajaxlibrary/Reference.Sys-Data-AdoNetServiceProxy-fetchDeferredProperty-Method.ashx

加载脚本库的新方法

多年以来,由 Web 开发人员自己考虑页面需要哪些脚本文件。这在过去并不是令人望而却步的任务,因为使用的 JavaScript 代码比较简单且数量有限,从而可以轻松地检查是否缺少必需的文件。随着在网页中使用的 JavaScript 代码更加复杂且日益增多,问题也随之而来,需要在不同文件之间拆分脚本,进而正确引用这些脚本以避免出现讨厌的运行时“未定义对象”错误。

多年以后,许多流行的 JavaScript 库现在已针对此方面提供了各种功能。例如,jQuery UI 库采用模块化设计,使您可以仅下载和链接真正需要的部分。构成 ASP.NET Ajax 库的脚本也提供了相同的功能。但是,脚本加载程序还具有其他功能。

脚本加载程序可提供一些额外的服务,并通过将大脚本库划分为较小部分来进行构建。一旦告知加载程序您需要的库,便将与所需文件的正确顺序相关的所有任务都委托给加载程序。脚本加载程序会并行加载需要的所有脚本,然后以正确顺序执行这些脚本。这样,加载程序会使您免于引发任何“缺少对象”异常,并提供了最快的脚本处理方式。您只需要列出所需的脚本即可。

现在暂停一下。如果我必须列出所需的所有脚本,那么使用加载程序有何好处?加载程序需要的内容与在将程序集链接到项目这一众所周知的过程中需要的内容完全不同。您可链接程序集 A,让 Visual Studio 2008 加载程序来确定所有静态依赖关系。下面是演示如何处理脚本加载程序的代码段:

Sys.require([Sys.components.dataView, Sys.scripts.jQuery]);

Sys.require 方法采用一组对要链接到页面的脚本的引用。在前面的示例中,您指示加载程序处理两个脚本 dataView 和 jQuery。

但是如您所见,对方法 Sys.require 的调用不包括指向任何物理 .js 文件的任何 Web 服务器路径。那么路径位于何处呢?

将使用 ASP.NET Ajax 库加载程序的脚本需要向加载程序定义其自身,并在加载完成时通知加载程序。向加载程序注册脚本并不会导致任何往返操作,而只是一种使加载程序知道可能会调用它来管理新脚本的方法。图 2 中含有 MicrosoftAjax.js 的摘录,其中演示如何向加载程序注册 jQuery 和 jQuery.Validate。

图 2 向脚本加载程序注册 jQuery 和 jQuery.Validate

loader.defineScripts(null, [
     { name: "jQuery",
       releaseUrl: ajaxPath + "jquery/jquery-1.3.2.min.js",
       debugUrl: ajaxPath + "jquery/jquery-1.3.2.js",
       isLoaded: !!window.jQuery
     },
     { name: "jQueryValidate",
       releaseUrl: ajaxPath + 
                            "jquery.validate/1.5.5/jquery.validate.min.js",
       debugUrl: ajaxPath + "jquery.validate/1.5.5/jquery.validate.js",
       dependencies: ["jQuery"],
       isLoaded: !!(window.jQuery && jQuery.fn.validate)
     }
    ]);

当然,您也可对自定义脚本和客户端控件使用此方法。在这种情况下,除了实际脚本之外,还需要引用特定于加载程序的脚本定义。特定于加载程序的定义包括脚本的发布和调试服务器路径、用于引用该定义的公共名称、依赖关系以及为测试是否已正确加载库而计算的表达式。

若要使用脚本加载程序组件,需要引用名为 start.js 的新 JavaScript 文件。下面是混合使用新旧两种脚本加载技术的示例应用程序的摘录:

<asp:ScriptManagerProxy runat="server" ID="ScriptManagerProxy1">
    <Scripts>
        <asp:ScriptReference Path="~/Scripts/Ajax40/Preview6/start.js"/>
        <asp:ScriptReference Name="MicrosoftAjax.js" 
                       Path="~/Scripts/MicrosoftAjax.js"/>
        <asp:ScriptReference Path=
                                 "~/Scripts/MicrosoftAjaxTemplates.js"/>
     <asp:ScriptReference Path="~/MasterDetail4.aspx.js"/>
   </Scripts>
</asp:ScriptManagerProxy>

您使用传统的 <script> 元素引用 start.js 文件。其他脚本可使用 ScriptManager 控件、纯 <script> 元素或 Sys.require 方法进行引用。如您在上面的代码段中所见,不存在对 jQuery 库的引用。实际上,jQuery 库在通过 ScriptManager 链接且特定于页面的 JavaScript 文件中以编程方式进行引用。

ASP.NET Ajax 库的另一个有用功能是可通过 Sys 名称空间使用 jQuery 功能,相反,Microsoft 客户端组件作为 jQuery 插件进行公开。例如,这表示可以使用 Sys.onReady 函数为准备就绪的事件注册事件处理程序(典型的 jQuery 任务),如下所示:

Sys.onReady(
    function() {
        alert("Ready...");
    }
);

如果采用以上所述的所有新功能,要用作网页扩展的 JavaScript 文件的典型启动类似于以下这样:

// Reference external JavaScript files

Sys.require([Sys.scripts.MicrosoftAjax, 

             Sys.scripts.Templates, 

             Sys.scripts.jQuery]);
Sys.onReady(
    function() {
        // Initialize scriptable elements 

        // of the page.
    }
);

但是,您还可以使用更简单的方法。您可以使用 Sys.require 加载如 DataView 这样的控件(而不是实现该控件的文件)。脚本加载程序会基于为 DataView 定义的依赖关系自动加载这些文件。让我们来重点讨论一下预取。

处理客户选择

若要获得图 1 中所示的用户界面,请使用 HTML 模板并使用 DataView 组件将数据附加到数据绑定占位符。在单击列出的某个客户时,会通过基于 DataView 的数据绑定自动显示客户详细信息。但是,未直接通过 DataView 绑定订单。这是由我们在本文开头所设置的要求引起的:按照设计,订单不随客户信息下载。

因此,若要提取订单,需要处理与 DataView 关联的模板中的选择更改。当前,DataView 不引发选择更改事件。DataView 确实为母版-详细信息方案提供巨大支持,但是这种支持很多时候是自动进行的,即使您可创建自定义命令和处理程序(请参阅 asp.net/ajaxlibrary/Reference.Sys-UI-DataView-onCommand-Method.ashx)也不例外。特别是对可以触发详细信息视图的所有可单击的元素,将 sys:command 特性设置为“select”,如下所示:

<li>
   <span sys:command="Select" 
         id="itemCustomer" 

         class="normalitem">
   <span>{binding CompanyName}</span>
   <span>{binding Country}</span>
   </span>        
</li>

单击元素时,会在 DataView 中触发 onCommand 事件,从而更新 selectedData 属性的内容以反映选择。因此会刷新模板中绑定到 selectedData 的所有部分。然而,数据绑定会导致更新显示的数据,但不执行任何代码。

如前所述,在 DataView 中触发某个命令时,会在内部引发 onCommand 事件。作为开发人员,您可以为该事件注册自己的处理程序。遗憾的是,至少对于 DataView 组件的当前预发布版本,会在更新所选的索引属性前调用命令处理程序。实际效果是,您可以在将要显示详细信息视图时进行截取,但是对于要显示的新内容却一无所知。该事件的唯一目标似乎是向开发人员提供一种方法,以防止在未验证某些重要条件的情况下进行选择更改。

下面这种方法现在有效,且在以后会继续有效,无论 DataView 组件任何改进。将 onclick 处理程序附加到母版视图的所有可单击元素,并绑定额外的特性以包含所有有用的关键信息。下面是母版视图中可重复部分的新标记:

<li>
   <span sys:command="Select" 

         sys:commandargument="{binding ID}"

         onclick="fetchOrders(this)"
         id="itemCustomer" 

         class="normalitem">
   <span>{binding CompanyName}</span>
   <span>{binding Country}</span>
   </span>        
</li>

该标记具有两个更改。首先,它现在包括一个新的 sys:commandargument 特性;其次,它具有用于单击事件的处理程序。sys:commandargument 特性包含已选择的客户的 ID。该 ID 通过数据绑定发出。在其中放置该 ID 的特性不必是 sys:commandargument;也可使用任何自定义特性。

单击处理程序负责根据所设置的加载策略提取订单。图 3 显示订单加载程序的源代码。

图 3 用于提取订单的代码

function fetchOrders(elem)
{
    // Set the customer ID
    var id = elem["commandargument"];
    currentCustomer = id;
    
    // Check the jQuery cache first
    var cachedInfo = $('#viewOfCustomers').data(id);
    if (typeof (cachedInfo) !== 'undefined') 
        return;

    // Download orders asynchronously
    $.ajax({
         type: "POST",
         url: "/mydataservice.asmx/FindOrders",
         data: "id=" + id,
         success: function(response) {

              var output = response.text;
              $('#viewOfCustomers').data(id, output);
              if (id == currentCustomer)
                  $("#listOfOrders0").html(output);
         }
    });
}

fetchOrders 函数接收单击的 DOM 元素。首先,该函数检索包含客户 ID 的约定特性的值。接下来检查在 jQuery 客户端缓存中是否已存在订单。如果不存在,则最终会继续进行异步下载。该函数使用 jQuery AJAX 方法来安排对 Web 服务的 POST 请求。我在此示例中假定 Web 服务采用“HTML 消息”AJAX 模式,并返回准备好与页面合并的纯 HTML。(请注意,这并不一定是最好的方法,主要用于旧方案。从纯粹的设计角度来看,向端点查询 JSON 数据所产生的负载会小得多。)

如果请求成功,则订单标记会先进行缓存,然后显示在预期位置上(请参阅图 4)。

图像:提取和显示订单

图 4 提取和显示订单

图 4 仅显示了一个屏幕快照,并不能真正说明发生的情况。在单击选择某个客户以向下钻取时,会以异步方式引发对订单的请求。同时会显示该客户的详细信息。您可能记得,无需按需下载客户信息,因为该信息会在用户单击首字母的高级菜单时成块下载。

下载订单可能需要一段时间,此操作不会向用户提供任何反馈,也不要求用户提供任何反馈。该操作独立进行,对于用户完全透明。整个预取模式的关键在于可预先提取用户可能会请求的信息。若要表现出真正的优势,必须异步实现此功能,并且从可用性角度考虑,最好对用户不可见。

我们来重点讨论用户在图 4 中的用户界面中执行的最常见任务。用户通常会单击以选择客户。接下来,用户可能会花费片刻时间阅读显示的信息。当用户查看显示内容时,会以静默方式下载所选客户的订单。

用户可能会(也可能不会)请求立即查看订单。例如,用户可能决定切换到其他客户,阅读相关信息,然后切换回第一个用户,也可能又导航到另一个客户。无论是哪种情况,只需通过单击以向下钻取客户的相关信息,用户便可触发相关订单的提取操作。

会对下载的订单执行何种操作?在下载订单时建议以何种方式处理订单?

处理提取的订单

坦白说,在这样的方案中,我找不到具有明显优势的预加载数据处理方法。这主要取决于您从利益相关者和最终用户处获得的输入。

但是,如果用户仍查看刚刚为其下载完订单的客户,则建议自动显示订单。

$.ajax 方法以异步方式工作,并会附加到其自己的成功回调。回调接收为给定客户下载的订单,但是在回调运行时,显示的客户可能不同。我使用的策略可确保在订单与当前客户相对应时,直接显示这些订单。否则,订单会进行缓存,在用户返回并单击“View orders”按钮时提供这些订单。

让我们再看看提取过程的成功回调:

function(response) 
{
  // Store orders to the cache

  $('#viewOfCustomers').data(id, response.text);

  // If the current customer is the customer for which orders

  // have been fetched, update the user interface 

  if (id == currentCustomer)
     $("#listOfOrders0").html(response.text);
}

ID 变量是 $.ajax 方法的本地变量,设置为要为其提取订单的客户的 ID。currentCustomer 变量则是全局变量,在执行提取过程时随时设置(请参阅图 3)。诀窍在于全局变量可从多个位置更新,因此在下载回调结束时进行检查才有意义。

图 1图 4 中看到的“View orders”按钮有何作用?该按钮供需要查看给定客户订单的用户使用。按照设计,在此示例中可以选择显示订单。因此,在用户界面中应该有一个触发视图的按钮。

用户单击以查看订单时,订单信息可能会(也可能不会)可用。如果订单不可用,则表示(按照设计)下载已挂起或由于某种原因已失败。因此,会向用户显示图 5 中所示的用户界面。

图像:订单尚不可用

图 5 订单尚不可用

如果用户一直处于相同页面中,则在下载成功完成时将自动显示订单,如图 4 中所示。

先显示的客户

若要完成通过预取功能丰富的此母版-详细信息方案演示,还需进行一项工作。DataView 组件允许指定要在显示中以所选模式呈现的特定数据项。可通过 DataView 组件的 initialselectedindex 特性控制最初要选择的项。如下所示:

<ul class="sys-template" sys:attach="dataview" id="masterView"
    dataview:dataprovider="/aspnetajax4/mydataservice.asmx"
    dataview:fetchoperation="LookupCustomers"
    dataview:selecteditemclass="selecteditem" 
    dataview:initialselectedindex="0">

在此例中,会自动显示为所选首字母检索的第一个客户。因为用户无需进行单击,所以不会自动提取订单。您仍可通过再次单击第一个客户来访问其订单。这样,第一个客户将如显示的其他任何客户那样进行处理。是否可以避免发生此行为?

对于用户,单击第一个客户查看订单是不够的。实际上,按钮处理程序会将可以显示的信息限制为缓存中的内容。这样做是为了避免代码中存在重复的行为,并尝试一次性完成所有处理。如下所示:

function display() 
{
    // Attempt to retrieve orders from cache
    var cachedInfo = $('#viewOfCustomers').data(currentCustomer);
    if (typeof (cachedInfo) !== 'undefined')  
        data = cachedInfo;
    else
        data = "No orders found yet. Please wait ...";

    // Display any data that has been retrieved
    $("#listOfOrders0").html(data);
}

前面的 display 函数必须略作改进,以在当前没有选择客户的情况下触发订单提取。这是使用全局变量 currentCustomer 的另一个原因。下面是已编辑过的 display 函数代码:

function display() 
{
    if (currentCustomer == "") 
    {
        // Get the ID of the first item rendered by the DataView
        currentCustomer = $("#itemCustomer0").attr("commandargument");

        // The fetchOrders method requires a DOM element.
        // Extract the DOM element from the jQuery result.
        fetchOrders($("#itemCustomer0")[0]);    
    }

    // Attempt to retrieve orders from cache
    ...

    // Display any data that has been retrieved
    ...
}

如果未手动选择任何客户,则会读取呈现的第一个项的 sys:commandargument。执行此操作的最快捷方法是使用通过 DataView 呈现的项的 ID 的命名约定。原始 ID 后会追加连续的编号。如果原始 ID 是 itemCustomer,则第一个元素的 ID 将是 itemCustomer0。(DataView 的这一方面在 ASP.NET Ajax 库的最终发行版本中可能会更改。)另请注意,fetchOrders 要求您传入一个 DOM 元素。jQuery 查询会返回 DOM 元素的集合。这就是您需要在上面的代码中添加项选择器的原因。

最后请注意,如果您可以接受数据绑定后最初不显示任何客户,则还可以使用另一种解决方案。如果将 DataView 的 initialselectedindex 特性设置为 -1,则最初不会选择任何客户。这样,若要查看订单详细信息,需要单击任意客户,这会触发对关联订单的提取。

总结

DataView 是用于在 Web 客户端应用程序上下文中进行数据绑定的强大工具。它特别针对诸如母版-详细信息视图这样的常见方案而设计。但并不支持所有可能的方案。在本文中,我演示了一些代码,这些代码通过实现“预取”模式来扩展 DataView 解决方案。

[ ASP.NET Ajax 库测试版可以从 ajax.codeplex.com 下载获得。预计它将与 Visual Studio 2010 同时发布。—Ed。]

Dino Esposito 是 Microsoft Press 即将出版的《Programming ASP.NET MVC》的作者,并且是《Microsoft .NET:Architecting Applications for the Enterprise》(由 Microsoft Press 在 2008 年出版)一书的合著者。Esposito 定居于意大利,经常在世界各地的业内活动中发表演讲。您可访问他的博客,网址为 weblogs.asp.net/despos

衷心感谢以下技术专家审阅本文:Stephen Walther