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

Связывание с данными OData в веб-приложениях с помощью Knockout.js

Джули Лерман

Julie LermanБудучи «повернутой» на всем, что связано с данными, я посвятила весьма много времени написанию серверного кода и упустила из виду уйму интересных вещей, которые сейчас творятся на клиентской стороне. Джон Папа (John Papa), который раньше вел эту рубрику, теперь занимается рубрикой по клиентским технологиям и проделал массу работы по ознакомлению всех нас с новой популярной клиентской технологией — Knockout.js. Благодаря энтузиазму Папы и других в отношении Knockout.js я решила принять приглашение от Хогета (Hoguet) из MyWebGrocer.com на презентацию по Knockout в местной группе пользователей, VTdotNET. Это мероприятие собрало народа больше, чем обычно, и на нем присутствовали разработчики из .NET-сообщества. В ходе презентации Хогета мне стало понятно, почему столь многие веб-разработчики заинтересованы в Knockout. Она упрощает связывание с данными на клиентской стороне в веб-приложениях, используя шаблон Model-View-ViewModel (MVVM). Связывание с данными… Ну вот, теперь я могу что-то делать с данными! На следующий день я уже изучала, как соединить Knockout.js с инфраструктурой доступа к данным, и сейчас готова поделиться своими открытиями с вами.

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

Моя цель заключалась в том, чтобы посмотреть, как можно использовать Knockout.js в связывании и последующем обновлении данных, получаемых от WCF Data Service. В этой статье я покажу вам самые важные рабочие детали. После этого в прилагаемом к статье исходном коде вы увидите, как они соединены друг с другом.

Я начала с существующего WCF-сервиса данных. По сути, это тот же сервис, который я использовала в своей статье за декабрь 2011 г. «Handling Entity Framework Validations in WCF Data Services» (msdn.microsoft.com/magazine/hh580732). Однако я обновила его под использование недавно выпущенной WCF Data Services 5.

Напомню: в моей демонстрационной модели был единственный класс:

public class Person { public int PersonId { get; set; } [MaxLength(10)] public string IdentityCardNumber { get; set; } public string FirstName { get; set; } [Required] public string LastName { get; set; } }

Класс DbContext предоставлял класс Person в DbSet:

public class PersonModelContext : DbContext { public DbSet<Person> People { get; set; } }

Затем сервис данных предоставлял этот DbSet для чтения и записи:

public class DataService : DataService<PersonModelContext> { public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("People", EntitySetRights.All); } }

С помощью Knockout.js скрипт на клиентской стороне может реагировать на изменения значений свойств в объектах, связанных с данными. Например, если в скрипте вызывается Knockout-метод applyBindings с передачей некоего объекта, то Knockout будет уведомлять любой связанный с данными элемент об изменении свойств этого объекта. Вы можете добиться аналогичного поведения для наборов, которые отслеживаются Knockout в процессе добавления или удаления в них элементов. Все это происходит на клиентской стороне, и нет никакой нужды писать скрипт обработки событий или обращаться к серверу за помощью.

Для достижения своей цели я должна была выполнить ряд задач:

  • создать модели представлений с поддержкой Knockout;
  • получить OData-данные в формате JSON;
  • переместить OData-результаты в свой объект модели представления;
  • связать данные с Knockout.js;
  • для поддержки обновлений перемещать значения из модели представления обратно в OData-объект результата.

Создание моделей представлений с поддержкой Knockout

Чтобы все это работало, у Knockout должна быть возможность «наблюдать» за свойствами объекта. Вы можете сделать это при определении свойств объекта. Но постойте! Я не предлагаю «загрязнять» объекты предметной области логикой, специфичной для UI, чтобы Knockout мог отслеживать изменения значений. Именно здесь на сцене появляется шаблон MVVM. Он позволяет создать специфичную для UI версию (или представление) вашей модели — VM (ViewModel) из MVVM. Это означает, что вы можете передавать данные в приложение любым удобным способом (запрашивая WCF Data Services, используя другой сервис или даже новый Web API), а затем переформировать результаты под ваше представление. Например, мой сервис данных возвращает типы Person с FirstName, LastName и IdentityCard. Но в представлении меня интересуют только FirstName и LastName. В ViewModel-версии объекта можно даже применять логику, специфичную для представления. Благодаря этому вы получаете лучшее из двух миров: объект, адаптируемый под ваше представление независимо от того, что именно передает источник данных.

Вот клиентский PersonViewModel, определенный мной в JavaScript-объекте:

function PersonViewModel(model) { model = model || {}; var self = this; self.FirstName = ko.observable(model.FirstName || ' '); self.LastName = ko.observable(model.LastName || ' '); }

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

Получение OData в формате JSON

Мой OData-запрос будет просто возвращать первый Person от сервиса данных. Сейчас он поступает с моего сервера разработки:

https://localhost:43447/DataService.svc/People?$top=1

По умолчанию OData-результаты возвращаются в формате ATOM (выражаемом с использованием XML). Однако Knockout.js работает с данными JSON, которые OData тоже может предоставлять. Так как я работаю непосредственно в JavaScript, гораздо проще иметь дело с JSON-результатами, чем с XML. В JavaScript-запросе можно добавить параметр в OData-запрос, чтобы указать, что результаты возвращаются в виде JSON: «$format=json». Но это требует, чтобы ваш сервис данных знал, как обрабатывать этот параметр запроса формата. Мой не знает. Если я захочу пойти таким путем, например при использовании AJAX для OData-вызовов, мне придется задействовать расширение в своем сервисе для поддержки JSON-вывода (детали см. по ссылке bit.ly/mtzpN4).

Однако, поскольку я использую инструментальный набор datajs для OData (datajs.codeplex.com), мне не нужно заботиться об этом. Этот набор по умолчанию автоматически добавляет заголовочную информацию в запросы так, чтобы они возвращали JSON-результаты. Поэтому мне не понадобится добавлять расширение JSONP в свой сервис данных. OData-объект от инструментального набора datajs содержит метод read, позволяющий выполнять запрос, результаты которого должны быть в формате JSON:

OData.read({   requestUri: https://localhost:43447/DataService.svc/People?$top=1"   })

Передача OData в PersonViewModel

Как только результаты получены (в моем случае единственный тип Person, определенный в модели предметной области), я создаю на их основе экземпляр PersonViewModel. Мой JavaScript-метод personToViewModel принимает Person, создает из его значений новый PersonViewModel и возвращает этот объект:

function personToViewModel(person) {   var vm=new PersonViewModel;   vm.FirstName(person.FirstName);   vm.LastName(person.LastName);   return vm; }

Заметьте, что я задаю значения, передавая новые значения так, будто свойства являются методами. Изначально я задавала значения, используя vm.FirstName=person.FirstName. Но это превращало FirstName в строку, а не наблюдаемый объект. Некоторое время я отчаянно барахталась, пытаясь понять, почему Knockout не замечает последующие изменения значения, но потом отказалась от этих попыток и обратилась за помощью. Свойства являются функциями, а не строками, поэтому нужно задавать их, используя синтаксис методов.

Я хочу, чтобы в ответ на запрос выполнялся personToViewModel. Это возможно, так как OData.read позволяет определить, какой метод обратного вызова нужно использовать в случае успешного выполнения запроса. В данном примере я передам результаты методу mapResultsToViewModel, который в свою очередь вызовет personToViewModel (рис. 1). В другой части решения я заранее определила переменную peopleFeed как https://localhost:43447/DataService.svc/People.

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

OData.read({ requestUri: peopleFeed + "?$top=1" }, function (data, response) { mapResultsToViewModel(data.results); }, function (err) { alert("Error occurred " + err.message); }); function mapResultsToViewModel(results) { person = results[0]; vm = personToViewModel(person) ko.applyBindings(vm); }

Связывание с HTML-элементами управления

Обратите внимание на код в методе mapResultsToViewModel: ko.applyBindings(vm). Это еще один ключевой момент в том, как работает Knockout. Но к чему я применяю привязки? Это как раз то, что я определю в своей разметке. В коде разметки я использую Knockout-атрибут data-bind для связывания значений из моего PersonViewModel с некоторыми элементами input:

<body> <input data-bind="value: FirstName"></input> <input data-bind="value: LastName"></input> <input id="save" type="button" value="Save" onclick="save();"></input> </body>

Если бы мне нужно было только показывать данные, я могла бы использовать элементы label, и вместо связывания со значением можно было бы связывать с текстом, например:

<label data-bind="text: FirstName"></label>

Но мне требуется возможность редактирования, поэтому я использую не только элемент input. Мой Knockout-атрибут data-bind также указывает, что я осуществляю связывание со значениями этих свойств.

Основные ингредиенты, обеспечиваемые Knockout для моего решения, — наблюдаемые свойства в модели представления, атрибут data-bind для элементов разметки и метод applyBindings, добавляющий логику периода выполнения, необходимую Knockout для уведомления этих элементов об изменениях в значениях свойств.

Если я запущу то, что есть на данный момент в приложении, я увижу Person, возвращаемый запросом в режиме отладки, как показано на рис. 2.

Person Data from the OData Service
Рис. 2. Данные Person от OData-сервиса

На рис. 3 приведены значения свойств PersonViewModel, отображаемых на странице.

The PersonViewModel Object Bound to Input Controls
Рис. 3. Объект PersonViewModel, связанный с элементами input

Сохранение обратно в базе данных

Благодаря Knockout мне не нужно извлекать значения из элементов input, когда настает пора сохранять эти значения. Knockout уже обновил объект PersonViewModel, связанный с формой. В своем методе save я буду передавать значения PersonViewModel обратно в объект person (поступающий от сервиса), а затем сохранять эти значения обратно в базе данных посредством своего сервиса. В пакете исходного кода вы увидите, что я храню экземпляр исходного person, изначально полученный OData-запросом, и тот же объект я использую здесь. Как только я обновляю person методом viewModeltoPerson, я могу передать его обратно в OData.request как часть объекта request (рис. 4). Объект request является первым параметром и состоит из URI, метода и данных. Взгляните на документацию datajs по ссылке bit.ly/FPTkZ5, если вас интересуют подробности метода request. Заметьте: я использую тот факт, что экземпляр person хранит URI, к которому он привязан, в свойстве __metadata.uri. Это свойство избавляет меня от необходимости «зашивать» в код URI, который представляет собой https://localhost:43447/DataService.svc/People(1).

Рис. 4. Сохранение изменений в базе данных

function save() { viewModeltoPerson(vm, person); OData.request( { requestUri: person.__metadata.uri, method: "PUT", data: person }, success, saveError ); } function success(data, response) { alert("Saved"); } function saveError(error) { alert("Error occurred " + error.message); } } function viewModeltoPerson(vm,person) { person.FirstName = vm.FirstName(); person.LastName = vm.LastName(); }

Теперь, когда я модифицирую данные (изменяя Julia на Julie, например) и нажимаю кнопку Save, я не только получаю уведомление «Saved», указывающее на отсутствие ошибок, но и вижу процедуру обновления базы данных в своем средстве профилирования:

exec sp_executesql N'update [dbo].[People] set [FirstName] = @0 where ([PersonId] = @1) ',N'@0 nvarchar(max) ,@1 int',@0=N'Julie',@1=1

Knockout.js и WCF Data Services для широких масс

Исследование Knockout.js подтолкнуло меня к изучению некоторых новых инструментов, которые могут использовать любые разработчики — не только для платформы .NET. И хотя это заставило меня поупражняться с плохо мне знакомым JavaScript, написанный мной код сфокусирован на привычных манипуляциях над объектом, а не на скучнейшем взаимодействии с элементами управления. Попутно я встала на путь истинный в архитектуре подобных приложений, используя подход MVVM, чтобы различать объекты модели от тех, которые должны быть представлены в UI. Разумеется, с помощью Knockout.js можно делать гораздо больше, особенно интересна эта библиотека в построении адаптивных веб-UI. Кроме того, при создании источника данных можно использовать такие великолепные средства, как WCF Web API (bit.ly/kthOgY). Я с удовольствием буду учиться у специалистов в этой области и искать предлоги для работы на клиентской стороне.

Исходный код можно скачать по ссылке code.msdn.microsoft.com/mag201206DataPoints.


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

Выражаю благодарность за рецензирование статьи экспертам Джону Папе (John Papa) и Алехандро Триго (Alejandro Trigo).