На переднем крае

Выборка с предсказанием на основе jQuery и ASP.NET Ajax Library

Дино Эспозито

В прошлой статье я рассматривал реализацию представлений «родительский/дочерний» (или «основной/подробности») (master-detail views), используя новые средства ASP.NET Ajax Library. К числу таких новых средств относятся синтаксис активного связывания с данными на клиентской стороне и компонент полнофункционального рендеринга, представленный клиентским элементом управления DataView. Если скомбинировать все эти средства, то вы сможете легко создавать вложенные представления, отражающие отношения данных «один ко многим».

В ASP.NET Ajax Library механика представлений «родительский/дочерний» в основном определена в логике компонента DataView и в том, как этот компонент обрабатывает и предоставляет свои события.

Сегодня мы сделаем еще один шаг и обсудим, как реализовать популярный проектировочный шаблон AJAX — Predictive Fetch (выборка с предсказанием) — на основе ASP.NET Ajax Library. По сути, я расширю пример из прошлой статьи — просмотр подробных сведений о клиентах — для автоматического и асинхронного скачивания и отображения информации о заказах, относящихся к конкретным клиентам (если таковые заказы есть). Попутно мы затронем некоторые вопросы, связанные с jQuery, и рассмотрим новый API интеграции jQuery в ASP.NET Ajax Library. А теперь без лишних предисловий давайте продумаем контекст и создадим первую версию примера.

Исходная демонстрация

На рис. 1 показан вариант приложения, к которому я добавлю средства выборки с предсказанием.

Рис. Приложение-пример в начальной стадии

Рис. 1. Приложение-пример в начальной стадии

В строке меню клиенты фильтруются по алфавиту. Как только вы выбираете определенную букву, выводится список клиентов меньшего размера в виде маркированного HTML-списка. Это родительское, или основное, представление (master view).

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

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

Обсуждаемый нами код будет работать на клиентской стороне, поэтому нельзя полагаться на механизмы отложенной загрузки, встроенные в некоторые инфраструктуры объектно-реляционного моделирования (object/relational modeling, O/RM), например Entity Framework или NHibernate. Если загрузка заказов должна быть отложенной, нужный код придется писать самостоятельно. С другой стороны, если информация о заказах уже доступна на клиентской стороне, т. е. она загружается вместе с информацией о клиентах, тогда большая часть работы проделана за вас. Вам нужно лишь связать данные из заказов с каким-либо HTML-шаблоном — вот и все.

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

Вы должны знать, что отложенная загрузка полностью поддерживается, если вы извлекаете свои данные через объект AdoNetDataContext. (О нем мы поговорим в будущих статьях.) Более подробные сведения см. по ссылке asp.net/ajaxlibrary/Reference.Sys-Data-AdoNetServiceProxy-fetchDeferredProperty-Method.ashx.

Новый способ загрузки библиотек сценариев

Годами веб-разработчикам приходилось самостоятельно решать, какие файлы сценариев могут понадобиться странице. Это не было устрашающей задачей, потому что довольно компактный и сравнительно несложный блок JavaScript-кода позволял проверять, не отсутствует ли нужный файл. Однако появление в веб-страницах все больших количеств более сложного JavaScript-кода в конечном счете вынудило распределять сценарии по наборам файлов, а значит, потребовалось следить за корректностью ссылок на них, чтобы избежать весьма неприятных ошибок периода выполнения вроде «undefined object» (неопределенный объект).

Соответствующие средства уже довольно давно предусматривались во многих популярных JavaScript-библиотеках. Например, библиотека jQuery UI имела модульную архитектуру и позволяла загружать и связывать только те части, которые действительно были вам нужны. Такая же функциональность предлагается сценариями, из которых состоит ASP.NET Ajax Library. Но загрузчик сценариев — это нечто большее.

Загрузчик сценариев предоставляет ряд дополнительных сервисов и разделяет большие библиотеки сценариев на меньшие части. Сообщая загрузчику сценариев об интересующей вас библиотеке, вы делегируете загрузчику любые задачи, связанные с корректным порядком требуемых файлов. Он загружает все необходимые сценарии параллельно, а затем выполняет их в правильном порядке. Благодаря этому загрузчик избавляет вас от любых исключений наподобие «missing object» (отсутствующий объект) и обеспечивает самую быструю обработку сценариев. Вам нужно лишь перечислить нужные сценарии.

Но постойте-ка. Если я перечислю все требуемые мне сценарии, то какой смысл использовать загрузчик? Дело в том, что этот загрузчик работает совсем не так, как загрузчик, используемый в общеизвестном процессе связывания сборок с проектом. Вы связываете сборку A и полагаетесь на загрузчик Visual Studio 2008 в идентификации любых статических зависимостей. Вот фрагмент кода, показывающий, как нужно обращаться с загрузчиком сценариев:

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

Метод Sys.require принимает массив ссылок на сценарии, которые вы хотите связать со своей страницей. В предыдущем примере вы указываете загрузчику позаботиться о двух сценариях: dataView и jQuery.

Однако, как видите, вызов метода Sys.require не включает никакого пути веб-сервера к любому из физических файлов .js. Где же тогда скрыт этот путь?

Сценарии, способные работать с загрузчиком ASP.NET Ajax Library, должны сами идентифицировать себя для загрузчика и уведомлять его об окончании своей загрузки. Регистрация сценария в загрузчике не приводит к дополнительному обмену данными между сервером и клиентом; это просто способ, который позволяет указать загрузчику, что ему может поступить вызов, требующий управления данным сценарием. На рис. 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)
     }
    ]);

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

Чтобы использовать компонент — загрузчик сценариев, вы должны сослаться на новый JavaScript-файл start.js. Ниже приведен фрагмент из кода приложения-примера, в котором применяются старый и новый методы загрузки сценариев:

<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>

Ссылка на файл start.js осуществляется с помощью классического элемента <script>. На другие сценарии можно ссылаться с применением элемента управления ScriptManager, элементов <script> или метода Sys.require. Как видно из приведенного выше фрагмента, ссылки на библиотеку jQuery нет. По сути, ссылка на библиотеку jQuery осуществляется программным способом из JavaScript-файла, специфичного для страницы и связанного через ScriptManager.

Другая интересная особенность ASP.NET Ajax Library — доступ к функциональности jQuery через пространство имен Sys и предоставление клиентских компонентов Microsoft как подключаемых модулей (плагинов) jQuery. Это означает, что вы можете, например, зарегистрировать обработчик для события ready — это типичная задача в jQuery, — с помощью функции Sys.onReady:

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. Давайте сосредоточимся на выборке с предсказанием.

Обработка выбора клиента

Чтобы получить UI, как на рис. 1, вы используете HTML-шаблоны и подключаете данные к средствам подстановки, связываемым с данными, с помощью компонента DataView. Детальные сведения о клиенте автоматически показываются благодаря связыванию с данными на основе DataView в момент выбора клиента из списка. Однако заказы не связываются напрямую через DataView. Это вызвано нашими же требованиями, сформулированными в начале статьи: согласно архитектуре заказы на загружаются вместе с информацией о клиентах.

Поэтому для получения заказов вам нужно обрабатывать смену выбранного элемента внутри шаблона, связанного с DataView. В данный момент DataView не генерирует событие смены выбранного элемента. DataView предоставляет полную поддержку сценариев «основной/детали», но по большей части на внутреннем уровне, хотя вы можете создавать собственные команды и обработчики (asp.net/ajaxlibrary/Reference.Sys-UI-DataView-onCommand-Method.ashx). В частности, можно задать атрибут sys:command, чтобы пометить элемент, при щелчке которого будет появляться представление «детали», как показано здесь:

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

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

После щелчка этого элемента генерируется событие onCommand в DataView, и в результате содержимое свойства selectedData обновляется, чтобы отразить текущий выбор. Соответственно обновляются и любые части шаблона, связанные с selectedData. Однако связывание с данными обеспечивает обновление отображаемых данных, а не выполнение какого-либо кода.

Как упоминалось, когда в DataView срабатывает команда, на внутреннем уровне генерируется событие onCommand. Программист может зарегистрировать собственный обработчик этого события. Увы, по крайней мере в текущей предварительной версии компонента DataView обработчик команды вызывается до обновления выбранного свойства index. В итоге получается, что вы можете выполнять свой код перед самым выводом представления «детали», но не имея ни малейшего понятия о том, что именно отображается в этом представлении. Похоже, единственное предназначение этого события — дать разработчикам способ предотвращать смену выбора, если не соблюдены какие-то важные условия.

Подход, который работает сегодня и будет работать впоследствии независимо от усовершенствований в компоненте DataView, заключается в следующем. Вы подключаете обработчик onclick к любому элементу, который можно щелкнуть, в основном представлении (master view) и связываете дополнительный атрибут, который будет содержать любую важную для вас информацию. Вот новая разметка для повторяемой части основного представления:

<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 содержит идентификатор выбранного клиента. Этот идентификатор генерируется через механизм связывания с данными. Атрибут, в котором вы храните идентификатор, не обязательно должен быть именно 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-элемент, который щелкнул пользователь. Сначала она получает значение атрибута, содержащего идентификатор клиента. Затем она проверяет, не загружены ли заказы в клиентский кеш jQuery. Если их там нет, она выполняет асинхронную загрузку. При этом используется метод jQuery AJAX для формирования запроса POST к веб-сервису. В этом примере я предполагаю, что веб-сервис использует AJAX-шаблон «HTML Message» и возвращает чистый HTML, готовый к объединению с разметкой страницы. (Заметьте, что это не всегда лучший подход и в основном работает в устаревших сценариях. С точки зрения чистой архитектуры, если бы вы запрашивали у конечной точки JSON-данные, то получали бы намного меньший объем данных.)

Если запрос заканчивается успешно, разметка заказов сначала кешируется, а затем выводится в заданном месте (рис. 4).

Рис. Выборка и отображение заказов

Рис. 4. Выборка и отображение заказов

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

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

Давайте сосредоточимся на наиболее распространенных задачах, которые выполнял бы пользователь в UI, показанном на рис. 4. В типичном случае пользователь выберет конкретного клиента. Затем он скорее всего потратит несколько секунд на просмотр отображенной информации. Пока пользователь занят этим делом, автоматически загружаются заказы, относящиеся к данному клиенту.

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

Что происходит с загруженными заказами? В чем заключается рекомендованный подход к обращению с ними после загрузки?

Манипуляции с загруженными заказами

Если честно, я не вижу четко предпочтительного способа обращения с заблаговременно загруженными данными в такой ситуации. Многое зависит от требований конечных пользователей.

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

Метод $.ajax работает асинхронно и подключается к собственной функции обратного вызова при успешном выполнении. Эта функция принимает заказы, загруженные для данного клиента, но во время ее выполнения отображаемый клиент может смениться. Применяемая мной политика гарантирует, что заказы немедленно отображаются, если они относятся к текущему клиенту. В ином случае заказы кешируются и становятся доступными при возврате пользователя к этому клиенту.

Внимательнее рассмотрим функцию обратного вызова при успешном выполнении процедуры выборки:

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, и ей присваивается идентификатор клиента, для которого осуществляется выборка заказов. Однако currentCustomer — глобальная переменная, которой присваивается значение при каждом выполнении процедуры выборки (рис. 3). Фокус в том, что глобальная переменная может быть изменена из разных мест, поэтому проверка в конце обратного вызова, выполняемого после загрузки, имеет смысл.

Какова роль кнопки View orders, показанной на рис. 1 и 4? Она позволяет увидеть список заказов для данного клиента. Дело в том, что в этом примере отображение заказов является не более чем дополнительной возможностью. Поэтому кнопка, инициирующая просмотр, — вполне логичный элемент в UI.

Когда пользователь щелкает эту кнопку, информация о заказах может быть доступна, а может быть и нет. Если информации нет, значит, согласно архитектуре этого примера, загрузка еще не закончилась или же эта операция по какой-то причине завершилась неудачей. Поэтому пользователю выводится в UI сообщение, показанное на рис. 5.

Рис. Информация о заказах еще недоступна

Рис. 5. Информация о заказах еще недоступна

Если пользователь остается на той же странице, заказы автоматически отображаются после успешной загрузки, как вы видели на рис. 4.

Первый отображаемый клиент

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

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

В этом случае автоматически отображается информация о первом клиенте для выбранной буквы. Поскольку ничего щелкать не нужно, автоматической выборки заказов не происходит. Вы можете получить доступ к заказам для первого клиента, вновь щелкнув его. Тогда первый клиент будет обрабатываться, как и любой другой. Есть ли какой-нибудь способ избежать такого поведения?

Для пользователя одного щелчка кнопки View orders будет недостаточно в случае первого клиента. По сути, обработчик кнопки ограничивает отображаемую информацию теми данными, которые находятся в кеше. Так сделано для того, чтобы избежать дублирования операций в коде и попытаться делать все только раз:

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. Исходный идентификатор дополняется порядковым номером. Если исходный идентификатор — itemCustomer, то идентификатор первого элемента будет itemCustomer0. (Этот аспект поведения DataView может измениться в финальной версии ASP.NET Ajax Library.) Также заметьте, что fetchOrders требует передачи DOM-элемента. Запрос jQuery возвращает набор DOM-элементов. Вот почему в приведенном выше коде нужен селектор элемента.

Наконец, возможно и другое решение, если для вас приемлемо, что после связывания с данными изначально не показывается информация ни о каких клиентах. Если вы присвоите атрибуту initialselectedindex компонента DataView значение –1, изначально никакой клиент выбираться не будет. В итоге, чтобы увидеть информацию о заказах, вам понадобится щелкнуть имя любого клиента, что запустит выборку соответствующих заказов.

Заключение

DataView — впечатляющий инструмент для связывания с данными в контексте клиентского веб-приложения. Он разработан специально для распространенных сценариев вроде получения представлений «родительский/дочерний» («основной/детали»). Но он не поддерживает все возможные сценарии. В этой статье я показал кое-какой код, расширяющий DataView за счет реализации шаблона выборки с предсказанием.

[Примечание: бета-версия ASP.NET Ajax Library доступна для скачивания на сайтеajax.codeplex.com*. Предполагается, что она будет выпущена одновременно с Visual Studio 2010.*]

Дино Эспозито (Dino Esposito) — автор книги «Programming ASP.NET MVC», которая скоро будет опубликована издательством Microsoft Press, и соавтор книги «Microsoft .NET: Architecting Applications for the Enterprise» (Microsoft Press, 2008). Проживает в Италии и часто выступает на отраслевых мероприятиях по всему миру. С ним можно связаться через его блог weblogs.asp.net/despos.

Выражаю благодарность за рецензирование этой статьи эксперту Стефену Уолтеру (Stephen Walther).