数据点

利用 jQuery DataTables 插件查询 OData

Julie Lerman

下载代码示例

Julie Lerman
通过开放数据协议 (OData),数据生产者可以通过一种通用格式提供 Web 数据,只要使用支持 HTTP 的技术,任何人都可以使用这种格式。数据是通过 URI 提供的,您可以使用常见 HTTP 谓词(GET、PUT、POST、MERGE 和 DELETE)进行数据交互。您可以直接通过 JavaScript 等语言或使用客户端 API(如 Microsoft .NET Framework、Silverlight、PHP 或 Microsoft 提供的其他 API)直接处理数据交互。无论采用哪种方式,都可以按相同方式与所有 OData 源交互。

公开提供的 OData 服务(如 Netflix Inc. 和 eBay Inc. 提供的商用源、世界杯数据、甚至是提供 150 年棒球统计信息的服务)正在不断增加。

数据的访问越来越容易,但是数据的提供呢?面对 150 年的棒球统计信息或成千上万部电影,客户端仍需进行一些工作,才能检索和导航所有这些数据。

在最近一次有关 jQuery 的 Vermont .NET 用户组演示中,我从一个名为 DataTables 的 jQuery 插件获得灵感,该插件投资成本低,可供用户查询大量数据。尽管 DataTables 可以在需要时与服务器端代码进行更多交互,其长处还是在于可以极快地进行客户端处理。

JQuery 是一种客户端 Web 技术(可以在任何类型的 Web 应用程序中使用),可以简化 JavaScript 的处理。如果与 jQuery 领域的人士交流,总能感受到对方对这项技术的极大热情。DataTables 是众多 jQuery 插件中的一种。在任何类型的 Web 应用程序中,都可以使用 jQuery。

我正好使用 .NET Framework 进行大部分工作,在本专栏中,我将同时使用 ASP.NET MVC 和 WebForms,演示如何在应用程序中使用一些基本的 DataTables 插件功能。但是,WebForms 应用程序中的逻辑将由客户端代码驱动。我将使用 Netflix OData 服务 (http://odata.netflix.com/v1/Catalog),这样,我可以演示如何处理在使用各种 OData 服务时可能遇到的一些常见缺陷。

可以从 datatables.net 下载 DataTables 插件。如果您刚刚接触 Odata 的用法,可以访问 msdn.microsoft.com/data/odata 上 MSDN 开发中心的“WCF 数据服务”部分,从而加快熟悉速度。

使用 LINQ 和客户端 API 查询 OData

我将从一个简单的 MVC 应用程序开始,在该应用程序中,我使用 Visual Studio 添加服务引用向导添加了对 http://odata.netflix.com/v1/Catalog 的服务引用。这进而创建了要在应用程序中使用的代理类,并基于该服务生成一个实体数据模型,如图 1 所示。该向导还添加对 .NET Framework OData 客户端库 API 的引用。.NET Framework 和 Silverlight OData 客户端库都支持 LINQ 查询,因此,OData 的使用相当简单。

图 1 解决方案资源管理器中的 MVC 项目

启动控制器 HomeController.cs 使用 OData 客户端库和服务代理查询以下特定流派的所有电影标题:Independent。查询结果返回到与此特定控制器操作关联的视图:

public ActionResult Index() {
  var svcUri = new Uri("http://odata.
netflix.com//v1//Catalog");

  var context = new NetflixOData.NetflixCatalog(svcUri);
  var query = from genre in context.Genres
              where genre.Name == "Independent"
              from title in genre.Titles
              where title.ReleaseYear>=2007
              select title ;
  var titles = query.ToList();             
  return View(titles);
}

HomeController 索引视图 (\Views\HomeController\index.aspx) 中的标记是执行所有相关表示逻辑的位置。若要利用 jQuery 和 DataTables 插件,需要向项目添加一组脚本文件。或者,可以指向联机脚本集(请参见 asp.net/ajaxLibrary/CDN.ashx 处的 Microsoft AJAX 内容传送网络),不过,我选择在本地承载这些脚本。DataTables 插件的下载包含可以放入项目中的 \media 文件夹(包含这些脚本)。您可以在图 1 中看到我已完成此操作。

图 2 包含 Index.aspx 文件的代码列表。

图 2 HomeController Index.aspx

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
 Inherits="System.Web.Mvc.ViewPage<IEnumerable<Title>>" %>
<%@ Import Namespace="JQueryMVC.Controllers" %>
<%@ Import Namespace="JQueryMVC.NetflixOData" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" 
  runat="server">
    Home Page
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" 
  runat="server">
  <head>
    <link href="../../media/css/demo-table.css" 
      rel="stylesheet" type="text/css" />
    <script src="../../media/js/jquery.js" 
      type="text/javascript"></script>
    <script src="../../media/js/jquery.dataTables.js" 
      type="text/javascript"></script>

    <script type="text/javascript" charset="utf-8">
        $(document).ready(function () {
           $('#Netflix').dataTable();
        });
    </script>
  </head>
  <div>
    <table id="Netflix">
      <thead><tr><th>Title</th>
                 <th>Rating</th>
                 <th>Runtime</th></tr></thead>
      <tbody>
        <% foreach (Title title in Model)
           { %>
             <tr><td><%= title.Name %> </td>
                 <td><%= title.AverageRating %></td>
                 <td><%= title.Runtime %></td></tr>
           <% } %>
      </tbody>
    </table>
  </div>
</asp:Content>

<head> 节开头处的 CSS 链接和两个脚本源指向 CSS 格式设置以及关键的 jQuery 和 jQuery.datatables JavaScript 文件。

接下来,看一下页面中呈现的表。DataTables 插件依赖于 <thead> 节中存储的表 ID 和标题信息。在此之后,一小段代码循环访问从 HomeController.cs 文件传入视图的 IEnumerable<Title>,在适当的列中显示 Name、AverageRating 和 Runtime 值。

当页面最初启动时,标题标记中的 JavaScript 方法使用 jQuery 在窗体中查找 Netflix 表,并将 dataTable 函数应用于该表。DataTables 是高度可配置的,但是对于这一调用 dataTable 函数的简单窗体,引用的表 Netflix 将获取 DataTables 默认配置。图 3 显示生成的页面。

图 3 使用 DataTables 插件显示数据

DataTables 执行的操作不仅仅是使用 CSS 美化表。请注意,从底部的信息可知,它检索了 155 行。默认情况下,它开始按每页 10 项进行客户端分页,不过,用户可以在下拉列表中选择每页 25、50 或 100 项。搜索框基于表中所有可用列中的搜索结果进行筛选。用户还可以单击标题行列对数据进行排序。DataTables 插件功能集十分丰富,甚至还有插件的插件。您可以在 datatables.net 网站上了解有关增强默认设置的更多信息。

在客户端查询 OData

并不是所有情况都适合使用客户端 API,因此,我将介绍更有挑战性的任务,即在客户端查询 Netflix OData,而不借助其他可用库(AJAX 客户端库)。我将使用 DataTables 插件,同时避开 Netflix 服务施加的一些限制。在使用其他公共 OData 服务时,可能会遇到这些限制。

这次,我使用 ASP.NET WebForms 应用程序(尽管可以使用普通的旧 HTML,因为此页面中未使用任何 .NET Framework 代码)。此应用程序中还需要 \media 文件夹,但是不会创建针对服务的代理,因此无需使用“添加服务引用”。

dataTable 函数有一个名为 sAjaxSource 的方法,该方法自动从目标源检索数据。不过,这要求以特定方式设置结果格式。OData 结果不符合这一要求。加利福尼亚的开发人员 Jeff Morris 撰写了一篇很好的博客文章,演示如何在 WCF 数据服务查询侦听器中重塑 OData 结果。这篇文章位于 bit.ly/bMPzTH

我将使用 AJAX 以本机形式返回 OData,然后手动填充表。

页面主体首先是定义表及其 <theader>(DataTables 需要它),以及一个空 <tbody>:

<body>
  <form id="form1" runat="server">
    <table id="Netflix" width="100%">
      <thead>
        <tr><th width="50%">Title</th>
            <th>Rating</th>
            <th>Runtime</th></tr>
      </thead>
      <tbody id="netflixBody"/>
    </table>
  </form>
</body>

页面有一些函数:GetData、displayResults 和一个用于处理 Netflix 服务当前缺点之一的帮助程序函数。 与 OData 的 .NET 客户端库类似,有一个 AJAX 客户端库,它是 Microsoft ASP.NET AJAX API 的一部分。 下面是 AJAX 文档中的一个示例,展示了一个使用此库的 JavaScript OData 查询的外观:

function doQuery() {
var northwindService = new
Sys.Data.OpenDataServiceProxy("/Northwind.svc");
northwindService.query("/Customers", cbSuccess, cbFailure, userContext);

或者,您可以只使用 AJAX 和 jQuery,就像我在下面的示例中那样。我们来看一看标头脚本的开头,其中包括 getData 函数:

<script type="text/javascript" charset="utf-8">
  var oTable;
  var query = "http://odata.
netflix.com/v1/Catalog/Titles?$orderby=Name&$top=500"

  $(document).ready(function () { getData() });

  function getData() {
    var url = query + "&$callback= displayResults" 
      + "&$format=json";
    $.ajax({ dataType: "jsonp", url: url });
  }

当页面启动时,document.ready 函数自动调用 getData。getData 从预定义的 OData 查询构造一个 URL,并追加参数以返回 OData 作为 JSON(默认 AtomPub 格式的一种替代形式),以及定义要在 AJAX 调用完成时执行的方法。

当 AJAX 调用完成时,使用 OData 查询的结果调用 displayResults 函数(请参见图 4)。

图 4 准备 OData 结果以便显示

function displayResults(results) {
  var entities;
  var redraw;

// Find data in results 
  if (results.d[0] == undefined) {
    queryNext = results.d.__next;
    entities = results.d.results;
  }
  else {
    queryNext = "";
    entities = results.d;
  }

  // Instantiate dataTable if necessary
  if (oTable ==null)
    oTable = $('#Netflix').dataTable();

  // Build table rows from data using dataTables.Add
  for (var post in entities) {
    if (post == queryResults.length-1)
      redraw = true; //only redraw table on last item
    else
      redraw = false;

    oTable.fnAddData([
      entities[post].Name, entities[post].Rating, 
      entities[post].Runtime],redraw);
  }

  // Continue retrieving results
  if (queryNext > "") {
    query = FixNetFlixUrl(queryNext);
    getData();
  }
}

使用“find data in results”注释的代码节处理我提到的 Netflix 局限之一。 Netflix 强制进行服务器端分页以保护其服务器,每个请求仅返回 500 行。 您能想象有人会慢悠悠地查询所有电影吗? 我相信这种情况经常发生。 服务器端分页不会妨碍获取更多行;您只需显式执行此操作。

在客户端处理大量数据正是 DataTables 所擅长的,您很可能需要利用该功能。 当检索大量数据(例如 5,000 行)时,加载所有数据可能需要较长时间,但是一旦这些数据位于内存中之后,最终用户就可以利用 DataTables 对这些数据执行所有类型的筛选和排序。

我第一次看到演示的 DataTables 时,演示人员说要将它用于企业报告工具,在该工具中会下载 80,000 行。 我反对这样滥用 Internet 和服务器。 不过,看到 DataTables 的运行性能后,我不再反对在受控方案中采用这种做法。

OData 提供了一种轻松请求另一批数据的方式,而 Netflix 提供此挂钩供您使用。 下面是一个请求 501 个结果的查询:

http://odata.
netflix.com/v1/Catalog/Titles?$orderby=Name&$top=501

当查询超过该服务的限制时,Netflix 使用 OData 继续标记功能。除了这些项之外,最后一项之后还包含一项查询结果。下面是 AtomPub 格式的查询:

<link rel="next"
  href="http://odata.
netflix.com:20000/v1/Catalog/Titles/?$orderby=
Name&$top=1&$skiptoken='1975%20Oklahoma%20National%20Championship%20
Game','BVZUb'" /> 
</feed>

skiptoken 参数告知查询从何处开始下一组结果。在 JSON 中,结果开头处名为 __next 的属性中可以看到该项,如图 5 所示。

图 5 请求的 JSON 结果超过服务配置的返回数据量

当查询未超过限制时,这些项直接位于 d 属性内,如图 6 所示。这便是为何 GetData 需要测试可在何处找到这些结果。如果存在继续标记,则它将其存储在 NextQuery 中,然后执行其余查询以便在内存中生成完整结果集。

图 6 请求的 JSON 结果未超过服务配置的返回数据量

如果查看 __next 属性,可以注意到,Netflix 向查询添加了一个端口号 20,000。但是,如果直接执行该查询,则会失败。因此,在请求数据之前,应从 URI 中删除该端口号。这就是我在请求数据之前调用的 FixNetFlixUrl 函数的用途。

在使用公共 OData 服务时,必须注意这类异常情况。现在,您已了解如何处理对返回结果数有限制的服务,以及如何处理在其继续标记中形成重大更改的服务。

对于检索的每个结果集,该方法都使用 DataTables fnAddData 方法向表添加每个项。重绘表的开销很大,因此,我将 fnAddData 的 redraw 参数设置为 false,直至完成最后一项结果为止。重绘整个数据检索会使 UI 更加流畅,而不是等到所有 5,000 行都已检索并添加到表。

将初始查询修改为返回 5,000 行,并将重绘推迟到最后执行之后,在 Internet 访问速度缓慢的佛蒙特州乡村环境中,几乎只需一分钟便可捕获所有行并显示表。重绘每一行十分快捷,即使添加更多行,也可以与表交互。这真是一个惊喜。

所有 5,000 行都在表中之后,DataTables 可十分出色地进行排序和搜索。排序所需的时间不到一秒。搜索瞬间即可完成,它可响应搜索框中的每次击键(请参见图 7)。

图 7 DataTables 中的实时搜索结果

针对 Internet Explorer 8 的小调整

DataTables 最近的一个更新会触发一项 Internet Explorer 8 功能,在 DataTables 中处理大型结果集时,这非常不方便。当执行太多脚本行时,Internet Explorer 会显示警告消息。

Microsoft 支持站点建议调整客户端计算机的注册表以更改此行为。要修复此应用程序,这不是合理的解决方案;如果可以避免,我不希望修改客户端的注册表设置。不过,还有另外一种方法。

DataTables 用户论坛中的一篇文章建议修改 DataTables 脚本文件。我实现了这种修改,效果很好。有关详细信息,请参见 bit.ly/co4AMD 上标题为“排序导致 IE 引发‘此页面上的脚本造成 Internet Explorer 运行速度减慢’”的论坛主题。

众多功能等待探索

希望您已了解足够的信息,可以理解我对这种强大插件的兴奋之情。在我演示的只读方案中,您可以执行更多操作来配置表的外观及其行为。使用 DataTables 还便于编辑,如果要在服务器端保留部分逻辑,也可以发挥 DataTables 的长处。

对我而言,通过使用 DataTables,最终用户可以从不断增多的公开 OData 服务中查询大量可用数据,就像是极客天堂中进行的比赛。

Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。她在世界各地的用户组和会议中演示过数据访问和其他 Microsoft .NET Framework 主题。Lerman 是《Programming Entity Framework》(O'Reilly Media,2010)一书的作者,该书受到广泛称赞,她的博客地址是 thedatafarm.com/blog。请通过 Twitter.com 关注她:@julielerman.

衷心感谢以下技术专家对本文的审阅:Rey BangoAlex James