Опубликован: 02.04.2013 | Доступ: свободный | Студентов: 1122 / 54 | Длительность: 20:18:00
Специальности: Программист
Лекция 5:

Элементы управления, их стилизация и привязка данных

Привязка данных

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

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

Основная идея привязки данных (data binding) заключается в соединении вместе или "привязывании" свойств двух различных объектов, обычно объекта данных (контекста) и объекта пользовательского интерфейса, о которых обычно говорят как об объекте-источнике и об объекте-приемнике (целевом объекте). Ключевой момент здесь заключается в том, что привязка данных обычно осуществляется для свойств, но не для объектов.

Привязка, так же, может привлекать конверсию значений из одного типа в другой, такую, как конверсию набора различных свойств из источника в единую строку, подходящую для приёмника. Кроме того, к одному и тому же источнику можно прикрепить несколько целевых объектов, или один объект-приёмник соединить с несколькими источниками данных. Эта гибкость может привести к тому, что привязка данных может выглядеть несколько туманным предметом для обсуждений, так как она включает в себя огромное количество возможностей! Тем не менее, в большинстве случаев мы можем обойтись достаточно простыми решениями.

Обычный сценарий привязки данных показан на Рис. 4.10, где у нас есть свойства дух элементов пользовательского интерфейса - span и img, привязанные к свойствам объекта, содержащего данные. Таким образом, здесь три связи: (1) свойство span.innerText привязано к свойству source.name; (2) свойство img.src привязано к свойству source.photoUrl; и (3) свойство span.style.color связано с выходом функции преобразования данных, которая преобразует свойство source.userType в цвет.

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

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

Как эти связи ведут себя во время выполнения программы, зависит от направления (direction) каждой из связей, которое может быть одним из следующих:

Единовременная привязка (One-time): значение в свойстве источника (возможно, с конверсией) копируется в целевое свойство в некоторый момент, после чего между ними больше нет никаких взаимоотношений. Это то, что вы автоматически выполняете, передавая переменные конструктору элемента управления, например, или просто присваивая целевому свойству значения, используя свойство-источник. Удобно то, что для подобного рода присваивания применяется декларативный подход, позволяющий обращаться напрямую к атрибутам элементов, как мы увидим позже.

Объект данных (Data Object)

Элемент управления пользовательского интерфейса (UI/Control)

Значение элемента (Item Value)

Инициализация (Initialization)

Нет непрерывного соединения (No ongoing connection)

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

Объект данных (Data Object)

Элемент управления пользовательского интерфейса (UI/Control)

Значение элемента (Item Value)

Инициализация (Initialization)

Изменения в данных находят отнажение в пользовательском интерфейсе (Changes to data are reflected in UI)

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

Объект данных (Data Object)

Элемент управления пользовательского интерфейса (UI/Control)

Значение элемента (Item Value)

Инициализация (Initialization)

Изменения в любом месте отражаются и в другом (Changes in either place are reflected in the other)

Привязка данных в WinJS

Сейчас, когда мы узнали о том, что такое привязка данных, мы можем посмотреть, как она может быть реализована в приложениях для Windows 8. Если хотите, вы можете создать любую схему для привязки данных или использовать библиотеку JavaScript стороннего разработчика для работы: всё это, в конечном счете, касается связи свойств объекта-источника данных со свойствами объекта-приёмника.

Если вы похожи на моих предков по отцовской линии, которые не полагались ни на кого, кроме себя, и делали всё сами (бурили скважины, добывали уголь и делали запасные части для машин), вы будете довольны, разрабатывая собственное решение для привязки данных. Но если вы более умеренны по своей природе, как я (благодаря материнской линии), вам понравится, как и мне, если кто-то достаточно умный выполнит какую-то работу за вас. Я благодарен команде разработки WinJS, которая, зная основные нужды привязки данных, создала API WinJS.Binding. Оно поддерживает и единовременную, и одностороннюю привязку, как декларативно, так и программно, вместе с функциями преобразования значений. Сейчас WinJS не предоставляет инструментов для двусторонней привязки, но подобные структуры несложно создать в коде.

Внутри структур WinJS, множественные целевые элементы могут быть привязаны к единственному источнику данных. WinJS.Binding, на самом деле, предоставляет то, что называется шаблонами (templates), обычно - коллекции целевых элементов, которые привязаны к одному и тому же источнику данных. Хотя мы не рекомендуем это делать, однако, можно привязать один целевой элемент к нескольким источникам, но такой конструкцией сложно управлять. Лучший подход в подобной ситуации заключается в том, чтобы заключить разные источники данных в оболочку одного объекта и привязать целевой объект именно к нему.

Лучший способ понять WinJS.Binding - посмотреть, для начала, на то, как мы пишем собственный код для связывания и потом изучить решение, которое предлагает WinJS. Для этого примера мы использует тот же самый сценарий, который показан на Рис. 4.10, где мы имеем объект-источник, привязанный к двум отдельным элементам пользовательского интерфейса, с одним конвертером, который превращает одно из свойств источника в цвет.

Единовременная привязка

Единовременная привязка, как упоминалось выше, это то, что вы делаете, просто присваивая значения свойствам элемента. Вот HTML-код для этого примера:

<!-- Разметка: элемент пользовательского интерфейса, который мы привяжем к объекту-источнику данных -->
<section id="loginDisplay1">	
<p>You are logged in as <span id="loginName1"></span></p>
<img id="photo1"></img>	
</section>

И объект-источник данных:

var login1 = { name: "liam", id: "12345678",
photoURL: "http://www.kraigbrockschmidt.com/images/Liam07.png", userType: "kid"};

Мы можем связать их следующим образом, использовав, кроме того, функцию-конвертер:

//"Связывание" одного свойства за один раз, с функцией-конвертером, вызываемой напрямую
var name = document.getElementById("loginName1");	
name.innerText = login1.name;	
name.style.color = userTypeToColor1(login1.userType);	
document.getElementById("photo1").src = login1.photoURL;	
function userTypeToColor1(type) {
return type == "kid" ? "Orange" : "Black";
}

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

Этот код можно найти в Test 1, в упражнении BindingTests для этой лекции. С помощью WinJS мы можем выполнить то же самое, используя декларативный синтаксис и функцию обработки. В разметке, мы используем атрибут data-win-bind для того, чтобы отобразить целевые свойства содержащего их элемента на свойства объекта-источника, который передан функции обработки WinJS.Process.All.

Значение data-win-bind - это строка с парами свойств. Синтаксис каждой пары выглядит как <target property> : <source property> [<converter>], где converter необязателен. Каждый идентификатор свойства может использовать запись с помощью точек, если нужно, и пары свойств разделены двоеточиями, как показано в HTML:

<section id="loginDisplay2">	
<p>You are logged in as	
<span id="loginName2"	
data-win-bind="innerText: name; style.color: userType Tests.userTypeToColor">
</span>	
</p>	
<img id="photo2" data-win-bind="src: photoURL"/>	
</section>

Обратите внимание на то, что обращение к массиву свойств источника с использованием [ ] не поддерживается, хотя конвертер может это сделать. В случае с целью, если этот объект имеет JavaScript-свойство, на который вы хотите сослаться, используя идентификатор, написанный через дефис, вы можете использовать следующий синтаксис:

<span data-win-bind="this['funky-property']: source"></span>

Похожий синтаксис нужен для атрибутов цели привязки данных, таких, как атрибуты aria-* для реализации специальных возможностей. Так как это не свойства JavaScript, на которые можно сослаться, используя идентификатор с дефисами, необходим специальный конвертер (или инициализатор, как его лучше называть), который называется WinJS.Binding.setAttribute.

<label data-win-bind="this['aria-label']: title WinJS.Binding.setAttribute"></label>

Кроме того, обратите внимание на WinJS.Binding.setAttributeOneTime для единовременной привязки атрибутов.

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

var login2 = { name: "liamb", id: "12345678",
photoURL: "http://www.kraigbrockschmidt.com/images/Liam07.png", userType: "kid"};

Мы превращаем разметку в реальные связи, используя WinJS.Binding.processAll.

//processAll сканирует дерево элементов в поисках data-win-bind, используя заданный объект как контекст данных
WinJS.Binding.processAll(document.getElementById("loginDisplay2"), login2);

Этот код, Test 2 в упражнении, производит тот же результат, что и Test1. Единственное, что нужно добавить - это объявление функции конвертера, которая должна быть глобально доступной и маркирована для обработки. Это можно сделать с помощью пространства имен, которое содержит функцию (опять же, её называют инициализатором, как мы обсудим в разделе "Инициализаторы привязки" ближе к концу этой лекции), созданную с помощью WinJS.Binding.converter:

//Используем пространство имен для экспорта функции из текущего модуля, 
//таким образом WinJS.Binding может найти её
WinJS.Namespace.define("Tests", {	
userTypeToColor: WinJS.Binding.converter(function (type) {	
return type == "kid" ? "Orange" : "Black";	
})
});

Как и в случае с конструктором элементов управления WinJS.Class.define, WinJS.Binding.converter автоматически маркирует функцию, которую он возвращает, как безопасную для обработки.

Кроме того, мы можем поместить объект-источник данных и применимый конвертер в одно и то же пространство имен.3Чаще встречается вариант, когда конвертеры принадлежат пространствам имен, где определены соответствующие элементы пользовательского интерфейса, так как они больше относятся к пользовательскому интерфейсу, нежели к источнику данных. Например (в Test 3), мы можем поместить объект данных login и фукнцию userTypeToColor в пространство имен LoginData, в итоге, разметка и код будут выглядеть так:

<span id="loginName3"
data-win-bind="innerText: name; style.color: userType LoginData.userTypeToColor">
</span>


WinJS.Binding.processAll(document.getElementById("loginDisplay3"), LoginData.login);

WinJS.Namespace.define("LoginData", {	
login : {	
name: "liamb", id: "12345678",	
photoURL: "http://www.kraigbrockschmidt.com/images/Liam07.png",

userType: "kid"	
},	

userTypeToColor: WinJS.Binding.converter(function (type) {
return type == "kid" ? "Orange" : "Black";	
})	
});

В итоге, для единовременной привязки данных, WinJS.Binding просто даёт вам декларативный синтаксис, который выполняет в точности то, что вы можете сделать самостоятельно в коде, но делает это в немного меньшем объеме кода. Так как это - лишь разметка и функция для обработки, здесь нет ничего волшебного, хотя подобные полезные утилиты по-своему волшебны! На самом деле, код здесь реализует лишь единовременную привязку, без вызова источником данных каких-либо событий при изменении данных. Мы увидим, как сделать это с помощью WinJS.Binding.as совсем скоро, после дополнительной пары замечаний.

Во-первых, WinJS.Binding.processAll, на самом деле, асинхронная функция, которая возвращает promise-объект. Любой обработчик завершения, переданный его методу done, будет вызван при завершении обработки, если у вас есть дополнительный код, который основан на данном состоянии. Во-вторых, вы можете вызвать WinJS.Binding.processAll больше, чем один раз для того же самого целевого элемента, каждый раз задавая различные объекты-источники (контексты данных). Это не заменит существующие привязки, учтите - лишь добавит новые, подразумевая, что вы хотите привязать то же самое целевое свойство к более, чем одному источнику, что, в итоге, приведет к большому беспорядку. Итак, снова, лучше всего собрать эти источники в один объект и привязать целевой объект к нему, используя запись с точкой для идентификации вложенных свойств.

Врезка: свойства привязки данных элеменов управления WinJS

Когда вы задаете в качестве целевых свойства элементов управления WinJS, а не корневого (содержащего их) элемента, имена целевых свойств должны начинаться с winControl. В противном случае вы осуществите привязку к несуществующим свойствам корневого элемента. При использовании winControl, привязываемое свойство служит той же цели, что и задаваемое фиксированное значение в data-win-options. Например, разметка, использованная выше в разделе "Пример: Элемент управления WinJS.UI.Rating (Оценки)" может использовать привязку данных для своих свойств averageRating и userRating, как показано ниже (подразумевая, что myData - это подходящий источник данных):

<div id="rating1" data-win-control="WinJS.UI.Rating"	
data-win-options="{onchange: changeRating}"	
data-win-bind="{winControl.averageRating: myData.average,
winControl.userRating: myData.rating}">	
</div>