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

ASP.NET Ajax Library и WCF Data Services

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

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

Всего несколько лет назад веб-индустрия расценивала появление AJAX как начало новой эпохи расцвета веб-программирования, и вот теперь веб-разработчикам наконец доступен мощный набор средств программирования: ASP.NET Ajax Library и WCF Data Services. Разработчики могут отказаться от браузера как от исполняющей среды и выполнять через Интернет ряд вещей, которые раньше были возможны только при использовании смарт-клиентов.

Вызов удаленной конечной точки HTTP — возможность, которой сейчас пользуются многие приложения. Такие приложения скачивают потоки данных в формате JSON (JavaScript Object Notation) с помощью WCF-сервисов (Windows Communication Foundation) и разбирают полученный контент в JavaScript-объекты, после чего осуществляется рендеринг этих объектов в текущую HTML Document Object Model (DOM). Однако WCF-сервис на серверной стороне и JavaScript-код на клиентской стороне работают с разными типами данных, вынуждая вас создавать две разные модели объектов.

Обычно на серверной стороне требуется модель предметной области, с помощью которой ваш промежуточный уровень обрабатывает и представляет сущности. Entity Framework и LINQ to SQL — два превосходных инструмента для проектирования модели объектов на серверной стороне — с нуля или на основе существующей базы данных. Но в какой-то момент вам понадобится передать эти данные клиенту в ответ на вызов WCF-сервиса.

Одна из самых популярных мантр архитектуры, ориентированной на сервисы (SOA), гласит, что вы должны всегда передавать контракты данных (не классы), так как это способствует разделению презентационного и бизнес-уровней. Так что вам нужна совершенно другая модель объектов: модель представлений для презентационного уровня.

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

Таким образом, требуется комплексное решение для доступа к данным и манипуляций над ними. Комбинация WCF Data Services (ранее ADO.NET Data Services) и ASP.NET Ajax Library предоставляет полную инфраструктуру, позволяющую загружать данные, работать с ними и возвращать обновления на сервер. В этой статье будут рассмотрены компоненты ASP.NET Ajax JavaScript для эффективного доступа к данным на клиентской стороне.

WCF Data Services в двух словах

Основная идея решения для доступа к данным в WCF Data Services заключается в том, что вы создаете источник данных на серверной стороне и открываете доступ к нему через особую разновидность WCF-сервисов: WCF-сервис данных. (Отличное введение в WCF Data Services было опубликовано в номере «MSDN Magazine» за август 2008 г., см. по ссылке msdn.microsoft.com/magazine/cc748663.)

Обычно вы начинаете с создания модели предметной области, используя Entity Framework, затем пишете для нее WCF-сервис. Для примеров я задействую привычную базу данных Northwind и буду работать с небольшим подмножеством ее таблиц: Customers и Orders.

Во-первых, создайте новый проект библиотеки классов и добавьте новый элемент типа ADO.NET Entity Data Model. Далее скомпилируйте проект и ссылайтесь на его сборку из нового приложения ASP.NET. В коде примера я использую проект ASP.NET MVC. Все параметры строки подключения включите в содержимое web.config хост-приложения и добавьте новый элемент «ADO.NET Data Service» в веб-проект. (Старое название по-прежнему используется в Visual Studio 2008 и второй бета-версии Visual Studio 2010.) Теперь у вас есть библиотека классов с источником данных и WCF-сервис данных, который размещается в приложении ASP.NET и открывает клиентам доступ к контенту.

В простейшем случае весь код, который понадобится в WCF-сервисе данных, выглядит так:

public class NorthwindService : DataService<NorthwindEntities>
{
   public static void InitializeService(IDataServiceConfiguration config)
   {
      config.SetEntitySetAccessRule("Customers", EntitySetRights.All);
    }
}

По мере продвижения работы над проектом вам, вероятно, потребуется добавить больше правил доступа к наборам сущностей, а также поддержку (почему нет?) дополнительных операций сервиса. WCF-сервис данных — это обычный WCF-сервис, используемый в стиле REST. А значит, ничто не мешает вам добавить одну или несколько новых операций сервиса, каждая из которых представляет целый блок реальных операций над данными вроде сложного запроса или обновления. Помимо этого, как показано в предыдущем коде, сервис предоставит клиентам доступ к набору сущностей Customers без всяких ограничений операций. Это означает, что вы не сможете запросить информацию по клиентам одновременно со сведениями по заказам, хотя набор сущностей Orders присутствует во встроенной модели сущностей. Для открытия доступа клиентов к заказам требуется новое правило.

Пока вы не добавите новые REST-методы в WCF-сервис данных, разрешены только базовые операции создания, чтения, обновления и удаления (CRUD), выражаемые с использованием специального формата URI. (Информацию об этом синтаксисе см. по ссылке msdn.microsoft.com/data/cc668792.) Формат URI позволяет приложениям запрашивать сущности, проходить по связям между сущностями и применять любые изменения. Каждая CRUD-операция сопоставлена соответствующей команде HTTP: GET для запросов, POST для вставок, PUT для обновлений и DELETE для удалений.

Клиент, который получает ссылку на WCF-сервис данных, принимает прокси-класс, созданный утилитой datasvcutil.exe или прозрачно сгенерированный мастером Add Service Reference в Visual Studio.

Вызов WCF-сервиса данных с платформы смарт-клиента очень прост, будь то Silverlight, Windows или Windows Presentation Foundation (WPF). То же самое можно сказать в отношении связывания с данными на серверной стороне в ASP.NET. А как обстоит дело с веб-клиентами на основе JavaScript и AJAX?

Использование сервисов данных через ASP.NET Ajax Library

В ASP.NET Ajax Library имеются два компонента JavaScript, относящихся к WCF Data Services: OpenDataServiceProxy и OpenDataContext.

OpenDataContext в основном спроектирован для управления CRUD-операциями из веб-клиента. Его можно рассматривать как JavaScript-эквивалент класса DataServiceContext. Определенный в пространстве имен System.Data.Services.Client, класс DataServiceContext представляет контекст исполняющей среды для указанного WCF-сервиса данных. OpenDataContext отслеживает изменения в используемых сущностях и может интеллектуально генерировать команды, посылаемые сервису на серверной стороне.

Класс OpenDataServiceProxy выступает в роли облегченной версии дополнительного прокси для WCF-сервиса данных. Фактически он управляет ситуациями, в которых требуется только чтение, но может применяться и для вызова дополнительных операций, предоставляемых сервисом. Класс OpenDataServiceProxy инициализируется следующим образом:

var proxy = new Sys.Data.OpenDataServiceProxy(url);

После этого класс готов к работе. Больше времени, как правило, уходит на конфигурирование объекта OpenDataContext. Но соединение устанавливается аналогично:

var dataContext = new Sys.Data.OpenDataContext();   
dataContext.set_serviceUri(url);

Оба класса можно использовать в качестве провайдеров данных для DataView. Какой из них вы будете использовать, зависит от того, что именно вам нужно. Если любые CRUD-операции должны выполняться через сервис, лучше задействовать прокси. Если же на клиенте присутствует какая-то логика и вы намерены выполнять наборы CRUD-операций до применения изменений, тогда предпочтительнее контекст данных. Давайте рассмотрим OpenDataContext.

Использование класса OpenDataContext

Вот как создается и инициализируется экземпляр класса OpenDataContext:

<script type="text/javascript">
  var dataContext;
    
  Sys.require([Sys.components.dataView, Sys.components.openDataContext]);

  Sys.onReady(function() {
    dataContext = Sys.create.openDataContext(
      {
         serviceUri: "/NorthwindService.svc",
         mergeOption: Sys.Data.MergeOption.appendOnly
      });

    });
</script>

Заметьте, что здесь применяется функция Sys.require для динамического связывания только с теми файлами сценариев, которые необходимы используемым компонентам. Если вы выберете подход с Sys.require, то единственный файл сценария, с которым вам понадобится связывание при традиционной работе, — start.js:

(see code <script src="../../Scripts/MicrosoftAjax/Start.js" 
  type="text/javascript">
</script>

Однако все используемые вами файлы должны быть доступны на сервере, или же вы должны ссылаться на них через Microsoft CDN (Content Delivery Network).

Если забежать вперед и посмотреть на рис. 2, в обработчике события ready документа создается новый экземпляр класса OpenDataContext. И вновь обратите внимание на применение новейшего сокращенного синтаксиса кода, определяющего стандартные события и создающего экземпляры стандартных объектов. Фабрика класса OpenDataContext принимает URL сервиса и некоторые дополнительные параметры. К этому моменту вы готовы использовать контекст данных как провайдер данных для некоторых UI-компонентов DataView в странице, как показано на рис. 1.

Рис. Использование контекста данных в качестве провайдера данных

<table>
    <tr class="tableHeader">
        <td>ID</td>
        <td>Name</td>
        <td>Contact</td>
    </tr>
    <tbody sys:attach="dataview" 
           class="sys-template"
           dataview:dataprovider="{{ dataContext }}"
           dataview:fetchoperation="Customers"
           dataview:autofetch="true">
         <tr>
             <td>{{ CustomerID }}</td>
             <td>{{ CompanyName }}</td>
             <td>{{ ContactName }}</td> 
         </tr>
     </tbody>
</table>

Создается экземпляр компонента DataView и используется для заполнения шаблона, к которому он прикреплен. DataView предоставляет инфраструктурный код, необходимый для загрузки данных через WCF-сервис данных и связывания с HTML-шаблоном. Где принимается решение о том, какие данные следует загружать? Иными словами, как указать строку запроса данных, которые вы хотите получить?

Свойство fetchoperation компонента DataView указывает имя вызываемой операции сервиса. Если провайдер является простым прокси сервиса, тогда свойство fetchoperation принимает имя открытого метода сервиса. А если вы используете класс OpenDataContext, ожидается, что значением свойства fetchoperation будет строка, которую распознает исполняющая среда WCF Data Services. Это может быть любое выражение наподобие:

Customers
Customers('ALFKI')
Customers('ALFKI')?$expand=Orders
Customers('ALFKI')?$expand=Orders&$orderBy=City

Если вы просто укажете имя набора сущности, то получите весь список сущностей. Ряд ключевых слов вроде $expand, $orderBy и $filter позволяют включать связанные наборы сущностей (разновидность inner join), упорядочивать по свойству и фильтровать возвращаемые сущности по булеву условию.

Вы можете вручную сформировать запрос как строку с учетом надлежащего формата URI. Или использовать встроенный JavaScript-объект OpenDataQueryBuilder (рис. 2).

Рис. Использование объекта AdoNetQueryBuilder

<script type="text/javascript">
    var dataContext;
    var queryObject;   

    Sys.require([Sys.components.dataView, 
                Sys.components.openDataContext]);

    Sys.onReady(function() {
        dataContext = Sys.create.openDataContext(
           {
               serviceUri: "/NorthwindService.svc",
               mergeOption: Sys.Data.MergeOption.appendOnly
           });
        queryObject = new Sys.Data.OpenDataQueryBuilder("Customers");
        queryObject.set_orderby("ContactName");    
        queryObject.set_filter("City eq " + "’London’");  
        queryObject.set_expand("Orders");      
    });
</script>

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

<tbody sys:attach="dataview" 
       class="sys-template"
       dataview:dataprovider="{{ dataContext }}"
       dataview:fetchoperation="{{ queryObject.toString() }}"
       dataview:autofetch="true">

С помощью метода toString вы извлекаете строку запроса из формирователя. В нашем примере конечной строкой запроса будет:

Customers?$expand=Orders&$filter="City eq 'London'"&$orderby=ContactName

Сервис возвращает набор составных объектов, в которых заключена демографическая информация о клиентах и некоторые сведения о заказах. Вывод представлен на рис. 3.


Рис. Запрос данных с использованием WCF-сервиса данных

Значения в последнем столбце указывают количество заказов, размещенных клиентом. Из-за атрибута $expand в запросе поток данных JSON содержит массив объектов-заказов. HTML-шаблон ссылается на длину этого массива и заполняет столбец так:

<td>{{ Orders.length }}</td>

Заметьте, что для успешного получения информации о заказах, вы должны сначала вернуться к исходному коду WCF-сервиса данных и разрешить доступ к набору сущностей Orders:

public static void InitializeService(
  IDataServiceConfiguration config)
{
  config.SetEntitySetAccessRule(
    "Customers", EntitySetRights.All);
  config.SetEntitySetAccessRule(
    "Orders", EntitySetRights.All);
}

Рассмотрим, как все это работает, если у вас более амбициозные планы и вы хотите обновлять данные на клиенте перед их возвратом сервису.

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

На рис. 4 показан HTML-шаблон фрагмента страницы, используемого для запроса клиента. Пользователь вводит идентификатор, щелкает кнопку и получает актуальные данные, которые можно редактировать.

Рис. Запрос сервиса данных

<div id="Demo2">
<table>
   <tr class="tableHeader"><td>Customer ID</td></tr>
   <tr><td>
     <%= Html.TextBox("CustomerID", "ALFKI") %>
     <input type="button" value="Load" onclick="doLoad()" />
   </td></tr>
 </table>
    
 <br /><br />
    
 <table sys:attach="dataview" id="Editor"
       class="sys-template"
       dataview:dataprovider="{{ dataContext }}"
       dataview:autofetch="false">
   <tr>
     <td class="caption">ID</td>
     <td>{{ CustomerID }}</td>
   </tr>
   <tr>
     <td class="caption">Company</td>
     <td>{{ CompanyName }}</td>
   </tr>    
   <tr>            
     <td class="caption">Address</td>
     <td>
        <%=Html.SysTextBox("Address", "{binding Address}")%></td>
   </tr>    
   <tr>       
     <td class="caption">City</td>         
     <td>{{ City }}</td>
    </tr>  
</table>
</div>

Загрузка данных происходит по требованию. Вот код, который берет на себя вызов сервиса данных:

function doLoad() {
  var id = Sys.get("#CustomerID").value;

  // Prepare the query
  var queryObject = new Sys.Data.OpenDataQueryBuilder("Customers");
  queryObject.set_filter("CustomerID eq '" + id + "’");
  var command = queryObject.toString();

  // Set up the DataView
  var dataView = Sys.get("$Editor").component();
  dataView.set_fetchOperation(command);
  dataView.fetchData();

Сначала вы получаете нужные входные данные, а именно: текст, введенный пользователем в поле. Далее вы формируете строку запроса, используя новый объект OpenDataQueryBuilder. Наконец, вы указываете DataView (который в свою очередь настраивается на использование WCF-сервиса данных) загрузить данные для запроса.

Любые полученные данные отображаются через активную привязку, поддерживаемую ASP.NET Ajax Library; она гарантирует своевременное обновление любого участвующего JavaScript-объекта (рис. 5).


Рис. Локальное редактирование объекта

Текстовое поле, в котором вы редактируете адрес клиента, определяется так:

<td class="caption">Address</td>
<td><%= Html.SysTextBox("Address", "{binding Address}") %></td>

Помимо выражения {binding}, обратите внимание на собственную вспомогательную HTML-функцию (HTML helper), используемую в ASP.NET MVC. У вас может возникнуть аналогичная ситуация, если вы попытаетесь применить активную привязку к данным и AJAX-шаблоны в контексте приложения Web Forms. Так в чем же проблема?

Чтобы связывание с данными работало, применяемые атрибуты надо предварять префиксом пространства имен sys:. Следовательно, чтобы связать какой-либо текст с текстовым полем, вам нужно добиться генерации следующей HTML-разметки:

<input type="text" ... sys:value="{binding Address}" />

Как в ASP.NET MVC, так и в Web Forms вы можете изящно решить эту проблему, введя HTML-литералы. Иначе говоря, вам нужна адаптированная версия средств, предоставляемых выбранной инфраструктурой ASP.NET для абстрагирования части разметки: вспомогательные HTML-функции или серверные элементы управления. В частности, в ASP.NET MVC вы можете прибегнуть к собственной вспомогательной HTML-функции, которая генерирует атрибут sys:value (рис. 6).

Рис. 6 Собственный вспомогательный HTML-компонент

public static string SysTextBox(this HtmlHelper htmlHelper, 
    string name, 
    string value, 
   IDictionary<string, object> htmlAttributes)
{
    var builder = new TagBuilder("input");
    builder.MergeAttributes(htmlAttributes);
    builder.MergeAttribute("type", "text");
    builder.MergeAttribute("name", name, true);
    builder.MergeAttribute("id", name, true);
    builder.MergeAttribute("sys:value", value, true);
    return builder.ToString(TagRenderMode.SelfClosing);
}

Изменения в адресе показываемого клиента регистрируются по мере их появления и обнаружения объектом контекста данных. Заметьте, что это возможно, только если вы используете объект контекста данных в качестве провайдера данных DataView, применяемого для рендеринга. Эту дополнительную работу может выполнять за вас объект OpenDataContext в отношении ранее упомянутого объекта OpenDataServiceProxy.

Как можно сохранять изменения? Чтобы гарантировать возврат сервису данных изменений, вам нужно лишь вызвать метод saveChanges экземпляра контекста данных. Однако в зависимости от типа создаваемого приложения вам могут понадобиться дополнительные уровни управления. Например, вам может потребоваться добавить кнопку Commit, после нажатия которой сначала суммируются все изменения, а потом пользователю выводится окно, где нужно подтвердить, действительно ли он хочет сохранить отложенные изменения. На рис. 7 показан JavaScript-код для такой кнопки Commit (фиксации изменений).

Рис. Кнопка Commit для подтверждения изменений

function doCommit() {
    var pendingChanges = dataContext.get_hasChanges();
    if (pendingChanges !== true) {
        alert("No pending changes to save.");
        return;
    }

    var changes = dataContext.get_changes();
    var buffer = "";
    for (var i = 0; i < changes.length; i++) {
        ch = changes[i];

      // Function makeReadable just converts the action to readable text
        buffer += makeReadable(ch.action) +
                  " --> " + 
                  ch.item["Address"];
        buffer += "\n";
    }
    if (confirm(buffer))
        dataContext.saveChanges();
}

Функция проверяет по текущему контексту данных наличие любых отложенных (ждущих) изменений. Если они есть, она формирует из них суммарное изменение. Метод get_changes контекста данных возвращает массив объектов с информацией о типе операции (вставка, удаление или обновление) и локальном объекте, который участвовал в изменениях. На рис. 8 приведено диалоговое окно, запускаемое предыдущим кодом при попытке фиксации отложенных изменений.


Рис. Обнаружены отложенные изменения

Стоит отметить, что всякий раз, когда вы выбираете нового клиента, вы теряете изменения по предыдущему клиенту. Это связано с тем, что контекст данных очищается и заполняется другими данными. Сохранение изменений в каком-либо другом объекте просто не имеет смысла — вы перезапишете клон самого контекста данных.

Вся мощь клиентского прокси WCF-сервиса данных на самом деле не проявляется при использовании UI с одним объектом. В ASP.NET Ajax Library Beta Kit вы найдете отличный способ тестирования этих возможностей: пример ImageOrganizer. Однако я могу дать вам представление о том, что я хотел сказать, просто расширив немного свой пример. Допустим, у вас есть представление «основной/детали» (master-detail view) и вы можете переключаться из одного представления клиента в другое без выхода со страницы и без обязательного сохранения изменений. Загрузка данных происходит только раз (или периодически), и на то время, пока эти данные остаются в памяти, все изменения, допускаемые через ваш UI, отслеживаются корректно (рис. 9).


Рис. Отслеживание изменений на клиентской стороне

Вставки и удаления

До сих пор я говорил в основном об обновлениях. Ну а как насчет вставок и удалений? Эти операции требуют чуть больше работы. Прежде всего вы не можете полагаться на связывание с данными для внесения изменений в нижележащий объект. Вы обязаны обновить набор (или объект) в памяти, полученный от контекста данных, применяемого в рамках UI. Для операций вставки вы просто создаете новый локальный экземпляр объекта, подходящий для отображения, и добавляете его в связанный набор. В этот момент, если ваш UI является полностью связанным с данными, он сможете отразить изменение. Затем вам нужно уведомить контекст данных о том, что в набор сущностей добавлен новый объект, который нужно отслеживать. Вот типичный код, необходимый для подключения к JavaScript-обработчику кнопки, вставляющей объект:

// Create a new local object
var newCustomer = { ID: "DINOE", CompanyName: "...", ... };

// Add it to the collection used for data binding
dataView.get_data().add(newCustomer);

// Inform the data context object
dataContext.insertEntity(newCustomer, "Customers");

Удалить объект проще. Вы удаляете его из набора в памяти и вызываете метод removeEntity контекста данных:

var index = customerList.get_selectedIndex();
var customer = dataView.get_data()[index];
dataContext.removeEntity(customer);
imageData.remove(customer);

Избегайте путаницы

Объекты OpenDataContext и DataView хорошо работают совместно, но не путайте их друг с другом. Объект OpenDataContext представляет клиентский прокси удаленного WCF-сервиса данных. Однако это очень специфический тип прокси. Он реализует шаблон «Unit of Work» на клиентской стороне, так как отслеживает изменения в сущностях, которые помогал извлечь. Контекст данных является отличным провайдером данных для компонента DataView. Компонент DataView отвечает исключительно за рендеринг. Он предоставляет подключаемые модули для шаблонов, чтобы упростить запуск удаленных операций, но эта возможность предназначена только для разработчиков. Никакой логики CRUD и управления данными в DataView нет.

В этой статье я не закапывался в глубины WCF Data Services и не коснулся таких аспектов, как параллельная обработка, отложенная загрузка и безопасность. Также я не сказал ни слова о передачах данных. Надеюсь, эта статья послужит неплохим введением в то, как выполнять некоторые наиболее важные операции с помощью ASP.NET Ajax Library и WCF Data Services. Остальное мы рассмотрим в будущих статьях. Оставайтесь с нами!

 

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

Выражаю благодарность за рецензирование статьи экспертам Борису Риверз-Муру (Boris Rivers-Moore) и Стефену Уолтеру (Stephen Walther).