Приложения Windows Store на JavaScript может создавать любой, кто умеет разрабатывать неуправляемые Windows-приложения с применением HTML и JavaScript, но JavaScript не всегда является лучшим выбором. Какое-то поведение в ваших приложениях может быть эффективнее реализовано на более объектно-ориентированных языках вроде C#, Visual Basic или C++. Кроме того, определенные части кода могут оказаться подходящими кандидатами для повторного использования в нескольких компонентах Windows Runtime (WinRT), которым нужны данные от UI-уровня. Передача данных из JavaScript в WinRT-компоненты и обратно в UI важна для понимания в любой из этих ситуаций.
В Web данные часто передаются от клиента серверу и обратно в виде JSON-объектов. В различных контекстах инфраструктуры, подобные ASP.NET Web Forms и ASP.NET MVC, предоставляют такие средства, как механизмы связывания моделей (model binders) или, как минимум, какую-то форму «волшебной» обработки на серверной стороне для разбора JSON-объектов. WinRT-компоненты имеют объекты с поддержкой разбора JSON, но эта поддержка низкоуровневая, и во многих случаях требует явной обработки с вашей стороны.
В этой статье демонстрируется, как надежно разбирать JSON-строки, передаваемые в WinRT-компоненты, чтобы получать строго типизированные объекты и возвращать результат обратно в UI.
Ограничения во взаимодействии с WinRT-компонентами
Прежде чем обсуждать конкретику разбора JSON-объектов, нужно сначала ознакомиться с требованиями и ограничениями во взаимодействии с WinRT-компонентами. В справочном разделе MSDN «Creating Windows Runtime Components in C# and Visual Basic» (bit.ly/WgBBai) детально описано, что необходимо для WinRT-компонентов при объявлении параметров методов и возвращаемых типов. Допустимые типы по большей части состоят из элементарных типов и ряда типов наборов, поэтому попытка передать исходный JSON-объект в компонент будет отклонена. Лучший способ передачи JSON-объекта в управляемый компонент — сначала сериализовать его (используя метод JSON.stringify), так как строки полностью поддерживаются этими классами.
Разбор JSON-объектов в управляемом коде
Пространство имен Windows.Data.Json включает ряд классов, предназначенных для работы с JSON-объектами в строго типизированном стиле, в том числе классы JsonValue, JsonArray и JsonObject. Класс JsonValue представляет значение JSON, выраженное в виде строки, числа, булева значения, массива или объекта (детали см. по ссылке bit.ly/14AcTmF). Разбор JSON-строки требует передачи исходной строки в JsonValue, который затем вернет экземпляр JsonObject.
Класс JsonObject представляет полный JSON-объект и включает методы для манипуляций над исходным объектом. Через класс JsonObject можно добавлять и удалять члены, извлекать данные из членов, перебирать все члены и даже вновь сериализовать этот объект. Подробнее о JsonObject см. по ссылке bit.ly/WDWZkG.
Класс JsonArray представляет JSON-массив, который опять же включает целый набор методов для управления массивом, в частности для итерации, добавления и удаления элементов массива. Подробнее об интерфейсе класса JsonArray см. по ссылке bit.ly/XVUZo1.
В качестве примера рассмотрим, как приступить к использованию этих классов, и для этого возьмем следующий JSON-объект в JavaScript:
{
firstName: "Craig"
}
Прежде чем передавать этот объект WinRT-компоненту, вы должны сериализовать его в строку, используя функцию JSON.stringify. Заметьте, что происходит после сериализации объекта: тот же объект представляется следующим образом:
"{
'_backingData': {
'firstName': 'Craig'
},
'firstName': 'Craig',
'backingData': {
'firstName':'Craig'}
}"
Это может оказаться сюрпризом для вас, так как вызов той же функции в веб-браузере просто сериализует объект в строку без добавления каких-либо членов в объект. Это изменение в структуре JSON-строки влияет на то, как вы извлекаете данные из объекта.
Первый шаг в считывании этих данных в WinRT-компоненте — попытка разобрать входную строку как экземпляр JsonValue. Если это удается, тогда вы можете запросить JsonObject от корневого экземпляра JsonValue. В этом случае JsonValue является корневым объектом, созданным вызовом функции stringify, и JsonObject предоставляет вам доступ к исходному объекту, с которого вы начинали в JavaScript-коде.
Следующий код показывает, как использовать метод GetNamedString (после того, как JsonObject становится доступен) для извлечения значения члена «firstName» в переменную:
JsonValue root;
JsonObject jsonObject;
string firstName;
if (JsonValue.TryParse(jsonString, out root))
{
jsonObject = root.GetObject();
if (jsonObject.ContainsKey("firstName"))
{
firstName = jsonObject.GetNamedString("firstName");
}
}
Аналогичный подход применяется для доступа к булевым и числовым членам, где доступны методы GetNamedBoolean и GetNamedNumber. Следующий шаг состоит в реализации методов расширения JsonObject, чтобы упростить доступ к JSON-данным.
Методы расширения для JsonObject
Исходная реализация класса JsonObject предоставляет низкоуровневое поведение, которое можно значительно расширить некоторыми простыми методами, позволяющими обрабатывать неидеальное форматирование и избегать исключений, если какие-то члены отсутствуют в источнике. Иначе говоря, объекты, созданные в JavaScript, неизбежно вызывают проблемы с форматированием или структурой, которые могли бы привести к исключениям. Добавление следующих методов расширения в класс JsonObject поможет снять остроту этих проблем.
Первый добавляемый метод расширения — GetStringValue. Его реализация показана на рис. 1, и он сначала проверяет, что член существует в объекте. В данном случае параметр key — это имя свойства JSON-объекта. Если данный член существует, вызывается метод TryGetValue, чтобы попытаться получить данные из экземпляра JsonObject. Если значение найдено, оно возвращается в виде объекта, реализующего интерфейс IJsonValue.
Рис. 1. Реализация метода расширения GetStringValue
public static string GetStringValue(this JsonObject jsonObject,
string key)
{
IJsonValue value;
string returnValue = string.Empty;
if (jsonObject.ContainsKey(key))
{
if (jsonObject.TryGetValue(key, out value))
{
if (value.ValueType == JsonValueType.String)
{
returnValue = jsonObject.GetNamedString(key);
}
else if (value.ValueType == JsonValueType.Number)
{
returnValue = jsonObject.GetNamedNumber(key).ToString();
}
else if (value.ValueType == JsonValueType.Boolean)
{
returnValue = jsonObject.GetNamedBoolean(key).ToString();
}
}
}
return returnValue;
}
Интерфейс IJsonValue включает свойство ValueType только для чтения, которое предоставляет значение из перечисления JsonValueType, обозначающее тип данных объекта. После согласования ValueType для получения данных из объекта используется соответствующим образом типизированный метод.
Метод GetStringValue распознает булевы и числовые значения, обеспечивая защиту от неправильно структурированных JSON-объектов. Вы могли бы сделать свою реализацию более строгой и отказываться от передачи или генерировать ошибку, если JSON-объект имеет не совсем правильный формат для ожидаемого типа, но код в моем примере более гибок при разборе и защищает от ошибок.
Следующий метод расширения (рис. 2) — реализация для извлечения булевых значений. В данном случае метод GetBooleanValue поддерживает булевы значения, выражаемые строками (например, значение true выражается как «1» или «true» и т. д.) и числами (скажем, true = 1, а false = 0).
Рис. 2. Реализация метода расширения GetBooleanValue
public static bool? GetBooleanValue(this JsonObject jsonObject,
string key)
{
IJsonValue value;
bool? returnValue = null;
if (jsonObject.ContainsKey(key))
{
if (jsonObject.TryGetValue(key, out value))
{
if (value.ValueType == JsonValueType.String)
{
string v = jsonObject.GetNamedString(key).ToLower();
if (v == "1" || v == "true")
{
returnValue = true;
}
else if (v == "0" || v == "false")
{
returnValue = false;
}
}
else if (value.ValueType == JsonValueType.Number)
{
int v = Convert.ToInt32(jsonObject.GetNamedNumber(key));
if (v == 1)
{
returnValue = true;
}
else if (v == 0)
{
returnValue = false;
}
}
else if (value.ValueType == JsonValueType.Boolean)
{
returnValue = value.GetBoolean();
}
}
}
return returnValue;
}
Методы расширения для числовых типов настроены на возврат типов, которые могут содержать null-значения (nullable types), поэтому в данном случае GetDoubleValue возвращает double, допускающий null-значение. Корректирующая логика в этом случае пытается преобразовать строки в возможные числовые значения (рис. 3).
Рис. 3. Реализация метода расширения GetDoubleValue
public static double? GetDoubleValue(this JsonObject jsonObject,
string key)
{
IJsonValue value;
double? returnValue = null;
double parsedValue;
if (jsonObject.ContainsKey(key))
{
if (jsonObject.TryGetValue(key, out value))
{
if (value.ValueType == JsonValueType.String)
{
if (double.TryParse(jsonObject.GetNamedString(key),
out parsedValue))
{
returnValue = parsedValue;
}
}
else if (value.ValueType == JsonValueType.Number)
{
returnValue = jsonObject.GetNamedNumber(key);
}
}
}
return returnValue;
}
Поскольку встроенный метод для извлечения чисел в классе JsonObject возвращает double, а значения данных зачастую выражаются как целые, следующий код показывает, как метод GetIntegerValue обертывает метод GetDoubleValue и преобразует результат в целочисленное значение:
public static int? GetIntegerValue(this JsonObject jsonObject,
string key)
{
double? value = jsonObject.GetDoubleValue(key);
int? returnValue = null;
if (value.HasValue)
{
returnValue = Convert.ToInt32(value.Value);
}
return returnValue;
}
Добавление поддержки фабрики
Теперь, когда класс JsonObject расширен и включает некоторую высокоуровневую поддержку извлечения данных в элементарные типы, нашим следующим шагом будет использование этой поддержки в классах фабрики, отвечающих за принятие входных JSON-строк и возврат экземпляра объекта предметной области (domain object).
Следующий код иллюстрирует, как в этой системе моделируется некая персона:
internal class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool? IsOnWestCoast { get; set; }
}
Ниже показан метод Create в классе PersonFactory, который принимает строку:
public static Person Create(string jsonString)
{
JsonValue json;
Person person = new Person();
if (JsonValue.TryParse(jsonString, out json))
{
person = PersonFactory.Create(json);
}
return person;
}
На рис. 4 представлен метод Create, принимающий JsonValue. Эти методы Create, используемые совместно, отвечают за прием исходной строки и возврат экземпляра класса Person с ожидаемыми данными в каждом его члене. Методы разделены и перегружены, чтобы обеспечить поддержку JSON-массивов (поясняется в следующем разделе).
Рис. 4. Метод Create класса PersonFactory, принимающий JsonValue
public static Person Create(JsonValue personValue)
{
Person person = new Person();
JsonObject jsonObject = personValue.GetObject();
int? id = jsonObject.GetIntegerValue("id");
if (id.HasValue)
{
person.Id = id.Value;
}
person.FirstName = jsonObject.GetStringValue("firstName");
person.LastName = jsonObject.GetStringValue("lastName");
bool? isOnWestCoast = jsonObject.GetBooleanValue("isOnWestCoast");
if (isOnWestCoast.HasValue)
{
person.IsOnWestCoast = isOnWestCoast.Value;
}
return person;
}
Добавление поддержки массивов
Иногда ваши данные поступают в виде массивов объектов, а не отдельных объектов. В этом случае вы должны попытаться разобрать строку как массив, используя класс JsonArray. На рис. 5 показано, как входная строка разбирается в массив и каждый элемент передается методу Create для окончательного разбора в модель. Заметьте, что сначала создается новый экземпляр списка Person на тот случай, если строку не удастся разобрать в массив объектов; тогда результатом будет пустой массив, что поможет избежать неожиданных исключений.
Рис. 5. Метод CreateList класса PersonFactory
public static IList<Person> CreateList(string peopleJson)
{
List<Person> people = new List<Person>();
JsonArray array = new JsonArray();
if (JsonArray.TryParse(peopleJson, out array))
{
if (array.Count > 0)
{
foreach (JsonValue value in array)
{
people.Add(PersonFactory.Create(value));
}
}
}
return people;
}
Добавление поддержки классов
Теперь нужно создать объект, отвечающий за использование класса фабрики и делающий что-то интересное с получаемыми экземплярами модели. Рис. 6 демонстрирует, как используются индивидуальные строки и строки JSON-массива, которые потом обрабатываются как строго типизированные объекты.
Рис. 6. Реализация ContactsManager (без поддержки асинхронности)
using System.Collections.Generic;
public sealed class ContactsManager
{
private string AddContact(string personJson)
{
Person person = PersonFactory.Create(personJson);
return string.Format("{0} {1} is added to the system.",
person.FirstName,
person.LastName);
}
private string AddContacts(string personJson)
{
IList<Person> people = PersonFactory.CreateList(personJson);
return string.Format("{0} {1} and {2} {3} are added to the system.",
people[0].FirstName,
people[0].LastName,
people[1].FirstName,
people[1].LastName);
}
}
Поддержка асинхронного взаимодействия
Вызовы методов WinRT-компонентов должны осуществляться асинхронно, так как JSON-сообщения потенциально способны достигать произвольного размера, что может вызвать задержки в вашем приложении.
Следующий код включает метод, добавленный в ContactsManager для поддержки асинхронного доступа к методу AddContact:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
public IAsyncOperation<string> AddContactAsync(string personJson)
{
return Task.Run<string>(() =>
{
return this.AddContact(personJson);
}).AsAsyncOperation();
}
Метод AddContactAsync принимает JSON-строку, а затем запускает Task, который делает все, что нужно для выполнения метода AddContact. По завершении Task ответ посылается в JavaScript-обещание (promise), что облегчается благодаря поддержке интерфейса IAsyncOperation. Полный исходный код класса ContactsManager с асинхронной поддержкой как AddContact, так и AddContacts вы найдете в пакете кода, который можно скачать для этой статьи.
Выполнение обещаний в JavaScript
Последняя часть головоломки — использование класса ContactsManager в JavaScript и вызовы этого класса по шаблону обещаний (promise pattern). Подход, применяемый в этом примере, заключается в реализации модели представления, которая передает смоделированные данные WinRT-компоненту, а затем ждет ответ. Данные, передаваемые компоненту, определены на рис. 7; они включают один JSON-объект, а также массив.
Рис. 7. Источник данных JSON
var _model = {
contact: {
id: 1000,
firstName: "Craig",
lastName: "Shoemaker"
},
contacts: [
{
id: 1001,
firstName: "Craig",
lastName: "Shoemaker",
isOnWestCoast: "true"
},
{
id: 1002,
firstName: "Jason",
lastName: "Beres",
isOnWestCoast: "0"
}
]
}
Модель представления, показанная на рис. 8, содержит член для модели и члены для сообщений, возвращаемых WinRT-компонентом. Инфраструктура связывания Windows Library for JavaScript (WinJS) обеспечивает привязку сообщений, возвращаемых из ответа, к HTML-элементам. Полный листинг модуля страницы доступен в сопутствующем этой статье пакете кода, и вы можете посмотреть в нем, как сведены воедино все части.
Рис. 8. Модель представления, использующая ContactsManager
var _vm = {
ViewModel: WinJS.Binding.as({
model: _model,
contactMsg: "",
contactsMsg: "",
addContact: function () {
var mgr = ParseJSON.Utility.ContactsManager();
var jsonString = JSON.stringify(_vm.ViewModel.model.contact);
mgr.addContactAsync(jsonString).done(function (response) {
_vm.ViewModel.contactMsg = response;
});
},
addContacts: function () {
var mgr = ParseJSON.Utility.ContactsManager();
var jsonString = JSON.stringify(_vm.ViewModel.model.contacts);
mgr.addContactsAsync(jsonString).done(function (response) {
_vm.ViewModel.contactsMsg = response;
});
}
})
};
Заметьте: если вы хотите связать функцию addContact или addContacts с какой-нибудь кнопкой, то должны вызвать функцию WinJS.Utilities.requireSupportedForProcessing, передав ей ссылку на функцию в своей модели представления.
Последний шаг — добавление соответствующих элементов и атрибутов в HTML для поддержки связывания. Элемент div действует как главный контейнер привязки для связываемых элементов и помечен атрибутом data-win-bindsource="Application.Pages.Home.ViewModel". Затем элементы header связываются со своими членами данных, предоставляя соответствующие значения атрибутам data-win-bind:
<section aria-label="Main content" role="main">
<div data-win-bindsource=
"Application.Pages.Home.ViewModel">
<h2 data-win-bind="innerText: contactMsg"></h2>
<hr />
<h2 data-win-bind="innerText: contactsMsg"></h2>
</div>
</section>
Вот и все! Создание приложений Windows Store на JavaScript дает возможность использовать существующие навыки разработки веб-приложений, но между двумя платформами имеется ряд различий. Низкоуровневая поддержка разбора JSON-данных доступна через пространство имен Windows.Data.Json, но вы можете добавить более богатую поддержку, введя несколько расширений в существующие объекты.