Доступ к данным

Продольные и поперечные срезы данных OData с помощью jQuery-плагина DataTables

Джули Лерман

Загрузка примера кода

Julie LermanOpen Data Protocol (OData) позволяет предоставлять данные через Интернет в формате, распознаваемом всеми потребителями, использующими технологии с поддержкой HTTP. Данные доступны по URI, и для взаимодействия с ними можно применять распространенные HTTP-команды GET, PUT, POST, MERGE и DELETE. Такое взаимодействие осуществляется напрямую с помощью языка вроде JavaScript или клиентского API, такого как Microsoft .NET Framework, Silverlight, PHP и др. В любом случае со всеми OData-каналами вы взаимодействуете одинаково.

В настоящее время появляется все больше общедоступных OData-сервисов, в том числе коммерческих каналов от Netflix Inc. и eBay Inc., предлагающих информацию по чемпионатам мира, и даже сервис, с помощью которого можно получить статистику по бейсболу за последние 150 лет.

Доступ к данным становится все проще, но как насчет их представления? Когда у вас имеется статистика по бейсболу за 150 лет или тысячи наименований фильмов, на клиентской стороне все равно придется приложить некоторые усилия, чтобы извлекать эти данные и каким-то образом перемещаться по ним.

На недавней презентации по jQuery, подготовленной Vermont .NET User Group, меня весьма впечатлил плагин (подключаемый модуль) на jQuery под названием DataTables — недорогой инструмент, позволяющий делать продольные и поперечные срезы в огромных массивах данных. Мощь DataTables выражается в потрясающе быстрой обработке на клиентской стороне, но при желании вы можете интенсивнее взаимодействовать с кодом на серверной стороне.

JQuery — клиентская веб-технология, которая упрощает работу с JavaScript. Если вы пообщаетесь с кем-нибудь, кто уже запрыгнул в фургон jQuery, то наверняка услышите массу положительных отзывов об этой технологии. DataTables — один из огромного сонма плагинов на jQuery. А jQuery можно использовать в веб-приложении любого типа.

Так сложилось, что большую часть своей работы я делаю с помощью .NET Framework, поэтому в этой статье я продемонстрирую применение некоторых базовых средств плагина DataTables в приложениях, использующих как ASP.NET MVC, так и Web Forms. Однако логика в приложениях Web Forms будет управляться кодом на клиентской стороне. Я буду работать с OData-сервисом Netflix (http://odata.netflix.com/v1/Catalog), который дает мне возможность показать вам, как обходить некоторые распространенные подводные камни, с которыми вы можете столкнуться при использовании различных OData-сервисов.

Плагин DataTables можно скачать с datatables.net. Если вы новичок в OData, советую посетить раздел WCF Data Services на сайте MSDN Developer Center по ссылке msdn.microsoft.com/data/odata.

Запрос OData через LINQ и клиентские API

Начну с простого MVC-приложения, в которое я добавила ссылку на сервис http://odata.netflix.com/v1/Catalog с помощью мастера Add Service Reference в Visual Studio. В результате автоматически создаются прокси-классы и формируется Entity Data Model на основе этого сервиса, как показано на рис. 1. Мастер также добавляет ссылки на клиентские библиотечные OData API в .NET Framework. Клиентские библиотеки OData в .NET Framework и Silverlight существенно облегчают работу с OData благодаря их поддержке запросов через LINQ.

image: The MVC Project in Solution Explorer

Рис. 1. Проект MVC в Solution Explorer

Мой стартовый контроллер, HomeController.cs, использует клиентскую библиотеку OData и прокси сервиса для запроса всех названий фильмов определенного жанра: Independent (независимое кино). Результаты запроса возвращаются в View, сопоставленное с конкретной операцией этого контроллера:

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 Index (\Views\HomeController\index.aspx) выполняется вся интересная презентационная логика. Чтобы задействовать jQuery и плагин DataTables, нужно добавить в проект набор файлов сценариев. В качестве альтернативы можно указать онлайновый набор сценариев (см.Microsoft AJAX Content Delivery Network по ссылке asp.net/ajaxLibrary/CDN.ashx), но я предпочитаю размещать их локально. В пакете для скачивания плагина DataTables содержится папка \media (в ней находятся сценарии), которую можно перетащить в проект. Как видите, на рис. 1 я уже сделала это.

Исходный код в файле Index.aspx показан на рис. 2.

Рис. 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>

Ссылка на CSS и два источника сценариев в начале раздела <head> указывают на форматирование CSS и критически важные JavaScript-файлы jQuery и jQuery.datatables.

Далее сосредоточимся на таблице, так как она размещается на странице. Плагин DataTables извлекает идентификатор таблицы и информацию заголовка, хранящиеся в разделе <thead>. После этого код перебирает в цикле IEnumerable<Title>, переданный в View из файла HomeController.cs, и отображает значения Name, AverageRating и Runtime в соответствующих столбцах.

При первом запуске страницы JavaScript-метод в теге header использует jQuery для нахождения таблицы Netflix в форме и применяет к ней функцию dataTable. Плагин DataTables очень гибок в конфигурировании, но в данном случае при простом вызове функции dataTable для таблицы Netflix, на которую мы ссылаемся, будет достаточно конфигурации DataTables по умолчанию. Полученная страница показана на рис. 3.

image: Displaying Data with the DataTables Plug-In

Рис. 3. Отображение данных с помощью плагина DataTables

Чтобы придать этой таблице такой симпатичный вид, DataTables не просто применил CSS. Заметьте, что внизу сообщается о том, что плагин извлек 155 записей. По умолчанию разбиение на страницы на клиентской стороне начинается с десять записей, хотя в раскрывающемся списке пользователь может выбрать 25, 50 или 100 записей на страницу. Поле Search позволяет фильтровать данные по результатам поиска во всех доступных столбцах таблицы. Пользователь также может щелкать заголовочные поля для сортировки данных по соответствующему столбцу. Функционал плагина DataTables столь обширен, что поддерживает даже подключение к себе других плагинов.Подробности см. на сайте datatables.net.

Запрос OData на клиентской стороне

Преимущества работы с клиентским API доступны не всегда, поэтому я переключусь на более трудную задачу:запрос Netflix OData на клиентской стороне в отсутствие одной из библиотек. Я задействую плагин DataTables, но столкнусь с некоторыми ограничениями, накладываемыми сервисом Netflix. Скорее всего вы тоже столкнетесь с теми же ограничениями при работе и с другими общедоступными OData-сервисами.

На этот раз я использую приложение ASP.NET Web Forms, хотя могла бы применить чистый HTML, так как на этой странице не будет никакого кода .NET Framework. В этом приложении потребуется папка \media, но создавать прокси для сервиса мы не будем, поэтому необходимости в Add Service Reference тоже нет.

В функции dataTable есть метод sAjaxSource, который автоматически извлекает данные из заданного источника. Но это требует специфического форматирования ваших результатов. А результаты OData имеют другой формат. Джефф Моррис (Jeff Morris), разработчик из Калифорнии, написал в блоге отличную статью (bit.ly/bMPzTH), в которой продемонстрировал переформатирование результатов OData в перехватчике запросов WCF Data Services. С этой записью можно ознакомиться по ссылке bit.ly/bMPzTH.

Вместо этого я воспользуюсь AJAX для возврата OData в его исходной форме, а затем вручную заполню таблицу.

Тело страницы начинается с таблицы и определения ее тега <thead> (требуется для 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. По аналогии с клиентской .NET-библиотекой для OData, существует клиентская библиотека для 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, а та формирует URL из предопределенного запроса OData и добавляет параметры, в которых будут возвращаться OData-данные в формате JSON (альтернативном по отношению к формату AtomPub по умолчанию), а также определяет метод, выполняемый по завершении AJAX-вызова.

Когда AJAX-вызов завершен, вызывается функция displayResults, которая использует результаты запроса OData (рис. 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();
  }
}

Блок кода, начиная от комментария «находим данные в результатах», обходит одно из упомянутых ограничений Netflix. Дело в том, что Netflix заставляет использовать разбиение на страницы на серверной стороне для защиты своих серверов и возвращает только по 500 записей на запрос. Можете вообразить, как некто очень ленивый запрашивает сразу все названия фильмов? Уверена, что это случается часто. Но разбиение на страницы на серверной стороне не запрещает получать дополнительные записи — вы просто должны делать это явным образом.

Обработка больших объемов данных на клиенте — как раз то, в чем проявляется сильнейшая сторона DataTables, и существует высокая вероятность того, что вы захотите воспользоваться ее преимуществами. Загрузка всех данных может потребовать несколько большего времени, когда они считываются большими порциями (например, по 5000 строк), но, как только они оказываются в памяти, DataTables способен предоставить пользователю все виды фильтрации и сортировки этих данных.

Когда я впервые увидела демонстрацию DataTables, я выяснила у того, кто проводил презентацию, что они использовали этот плагин в качестве средства корпоративной отчетности и загружали при этом 80 000 записей. Поначалу это вызвало во мне протест:как можно так нагружать Интернет и сервер? Однако, посмотрев DataTables в действии, я больше не имею никаких возражений против такого варианта использования, если, конечно, все делается под контролем.

OData позволяет легко запрашивать порцию данных другого размера, и Netflix предоставляет соответствующую точку подключения для выполнения этой задачи. Вот пример запроса, где запрашивается 501 результат:

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

Когда запрос превышает заданный лимит сервиса, Netflix использует специфическую функциональность OData — маркер продолжения (continuation token). В дополнение к стандартному набору записей в результатах содержится еще один элемент. Здесь он имеет формат 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.

image: JSON Results of a Request for More Data than the Service Is Configured to Return

Рис. 5. JSON-результаты запроса от сервиса данных большей порцией

Когда запрос не превышает этот лимит, записи находятся прямо внутри свойства d, как видно на рис. 6. Именно по этой причине в GetData нужна проверка, чтобы узнать, где будет выполняться поиск результатов. Если имеется маркер продолжения, он сохраняется в NextQuery, а затем выдается запрос продолжения, чтобы сформировать в памяти полный набор результатов.

image: JSON Results for a Request Within the Configured Return Amount

Рис. 6. JSON-результаты запроса от сервиса данных в пределах стандартной порции

Если вы посмотрите на свойство __next, то заметите, что Netflix добавил в запрос номер порта (20000). Однако, если вы напрямую выполняете этот запрос, он будет заканчиваться неудачей. Поэтому в таком случае перед запросом нужно удалять номер порта из URI. В этом и состоит смысл функции FixNetFlixUrl, которую я вызываю до выдачи запроса.

Будьте внимательны к подобным аномалиям, используя общедоступные сервисы OData. Теперь вы знаете, как обращаться с сервисом, который ограничивает число возвращаемых им результатов да еще и вносит разрушающее изменение в свой маркер продолжения.

Для каждого извлекаемого набора результатов этот метод, добавляя каждый элемент в таблицу, вызывает метод DataTables fnAddData. Перерисовка таблицы — операция дорогостоящая, поэтому я присваиваю параметру redraw метода fnAddData значение false до тех пор, пока не будет достигнут последний элемент в результатах. Перерисовка получения данных делает пользовательский интерфейс более плавным в отличие необходимости ждать, пока 5 000 строк будут получены и добавлены в таблицу.

После модификации исходного запроса для возврата 5000 строк оказалось, что на получение всех записей и отображение таблицы в моей сельской местности неподалеку от Вермонта с весьма медленным соединением с Интернетом и оттягиванием перерисовки до последнего момента уходила почти минута. Перерисовка каждой строки была гораздо шустрее, и я смогла взаимодействовать с таблицей даже в процессе добавления очередных строк. Приятный сюрприз!

Как только в таблице оказались все 5000 строк, DataTables просто изумительно справлялся с сортировкой и поиском. Сортировка занимала чуть меньше секунды. Поиск вообще был мгновенным, так как поле поиска реагировало на ввод каждого символа (рис. 7).

image: Real-Time Search Results in DataTables

Рис. 7. Результаты поиска в реальном времени в DataTables

Небольшое изменение для Internet Explorer 8

Последнее обновление DataTables вызывает в Internet Explorer 8 срабатывание одной из его функций, совершенно нежелательных при работе с большими наборами результатов в DataTables. Internet Explorer отображает предупреждение, когда выполняется слишком много строк сценария (скрипта).

На сайте поддержки Microsoft рекомендует слегка модифицировать реестр на клиентском компьютере, чтобы изменить это поведение. Не думаю, что это разумное решение для исправления проблемы с данным приложением:я предпочитаю не возиться с параметрами реестра на клиентском компьютере, если можно сделать по-другому. И такой вариант есть.

В одном из постов на пользовательских форумах DataTables была предложена модификация в файле сценария DataTables. Я воспользовалась этим вариантом, и все чудесно сработало. Подробности читайте на форуме в ветке «Sorting causes IE to throw 'A script on this page is causing Internet Explorer to run slowly'» по ссылке bit.ly/co4AMD.

Многие возможности остались неисследованными

Надеюсь, вы увидели достаточно, чтобы понять мой неподдельный интерес к этому впечатляющему плагину.Возможности этого плагина гораздо обширнее, чем я смогла рассказать в этой статье. DataTables также поддерживает редактирование, причем весьма гибкое, и, если вы хотите разместить часть логики на серверной стороне, вы можете сделать это, не потеряв никаких преимуществ DataTables.

DataTables позволяет делать поперечные и продольные срезы в больших массивах данных от общедоступных OData-сервисов, количество которых постоянно растет.И на мой взгляд, этот плагин появился весьма своевременно.

Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором очень популярной книги «Programming Entity Framework» (O’Reilly Media, 2009). Вы также можете встретить ее на Twitter.com @julielerman.

Выражаю благодарность за рецензирование статьи экспертам Рею Бэнгоу (Rey Bango) и Алексу Джеймсу (Alex James)