Элементы управления, их стилизация и привязка данных
Пользовательские элементы управления
Как ни обширен набор элементов управления HTML и WinJS, всегда будет что-то, что вам нужно, но чего система делать не умеет. "Есть ли здесь элемент управления для календаря?", - это вопрос, который мне часто приходится слышать. "А как насчёт элементов для вывода графиков?". Всё это не включено в Windows 8, и несмотря на пожелания обратного, это означает, что вы, или другие разработчики, нуждаются в создании пользовательских элементов управления.
К счастью, всё, что мы уже узнали, особенно об элементах управления WinJS, применимо к пользовательским элементам управления. На самом деле, элементы управления WinJS полностью реализованы с использованием той же модели, которую вы можете использовать напрямую, и если вы посмотрите код WinJS, вы сможете найти множество примеров подобной реализации.
Возвращаясь к уже пройденному материалу, отметим, что элементы управления - это лишь декларативная разметка (создающая элементы в DOM), плюс - применимый CSS, методы, свойства и события, доступные из JavaScript. Для создания подобного элемента управления в модели WinJS, воспользуйтесь следующей стандартной последовательностью шагов:
- Задайте пространство имен для вашего элемента (элементов), используя WinJS.Namespace.define для обеспечения области видимости имён и для того, чтобы не включать дополнительные идентификаторы в глобальное пространство имен. (Не добавляйте элементы управления в пространство имен WinJS). Помните, что вы можете вызывать WinJS.Namespace.define множество раз для добавления новых членов, таким образом, приложение обычно имеет единое пространство имен для всех его пользовательских элементов управления.
- Внутри этого пространства имен задайте конструктор, используя WinJS.Class.define (или derive), присваивая возвращаемое значение имени, которое вы хотите использовать в атрибутах data-win-control. Его полное имя будет выглядеть как <namespace>.<constructor>.
- Внутри конструктора (в форме <constructor>(element, options)):
- Вы можете распознать любой необходимый вам набор параметров, переданный конструктору; он может быть произвольным. Просто игнорируйте то, что вы не распознали.
- Если element равен null или undefined, создайте div для использования вместо него.
- Считая element корневым элементом, содержащим элемент управления, убедитесь, что установили element.winControl=this и this.element=element для того, чтобы ваше решение соответствовало шаблону WinJS.
- Внутри WinJS.Class.define, второй аргумент - это объект, который содержит ваши публичные методы и свойства (доступные в созданном экземпляре объекта). Третий аргумент - это объект со статическими методами и свойствами (которые доступны путём обращения к имени класса, без необходимости вызывать new).
- Для собственных событий, дополните (WinJS.Class.mix) ваш класс результатами из WinJS.Utilities.createEventProperties(<events>), где <events> это массив имён событий (без префиксов on). Это создаст свойства on<event> в вашем классе для каждого имени из списка.
- Кроме того, дополните ваш класс WinJS.UI.DOMEventMixin для того, чтобы добавить стандартные реализации addEventListener, removeEventListener, dispatchEvent и setOptions.1Обратите внимание на то, что существует команда WinJS.Utilities.eventMixin, которая похожа на описываемую (но без setOptions) и полезна для создания объектов, которые не являются элементами управления, не присутствуют в DOM, но нуждаются в вызове событий. Здесь реализация не принимает участие в восходящей (bubbling) и нисходящей (tunneling) маршрутизации событий DOM.
- В вашей реализации (в разметке и коде), ссылайтесь на определенные вами классы в применяемых по умолчанию таблицах стилей, но это может быть переназначено тем, кто использует элемент управления. Используйте существующие win-* классы для того, чтобы ваши элементы управления соответствовали общему подходу к стилизации.
- Обычно хорошим подходом считается организация пользовательских элементов управления в отдельных папках, которые содержат все необходимые HTML, CSS, JS-файлы. Помните, кроме того, что вызов WinJS.Namespace.define для одного и того же пространства имён аддитивен, таким образом, вы можете заполнить одно пространство имён элементами управления, определенными в различных файлах.
Вы можете решить использовать WinJS.UI.Pages, если всё, что вам нужно - это фрагмент HTML/CSS/JavaScript, подходящий для повторного использования, для которого нет особой необходимости во множестве методов, свойств и событий. WinJS.UI.Pages, на самом деле, реализован как пользовательский элемент управления. Точно так же, если то, что вам нужно - это участок HTML, подходящий для повторного использования, с которым вы хотите связать некоторые данные во время выполнения программы, обратитесь к WinJS.Binding.Template, о котором мы поговорим далее. Это не элемент управления - в том виде, как мы его здесь описали - он не поддерживает события, например, но он может быть в точности тем, что вам нужно.
Важно напомнить, что всё в WinJS, вроде WinJS.Class.define и WinJS.UI.DOMEventMixin - это лишь вспомогательные функции для широко распространённых шаблонов. Вам не нужно безальтернативно использовать их, так как в итоге, пользовательский элемент управления - это лишь элемент DOM, такой же, как и все остальные и вы можете создавать такие элементы и управлять ими так, как вы того хотите. Утилиты WinJS лишь ускоряют и упрощают выполнение основных задач.
Примеры пользовательских элементов управления
Вот пара примеров, которые помогут увидеть эти рекомендации в действии. Первый - это то, что Крис Траверс, один из инженеров WinJS, который оказал мне неоценимую помощь с этим курсом, назвал "самым тупым элементом управления, который можно себе представить". Однако, он чётко показывает самые главные структуры:
WinJS.Namespace.define("AppControls", { HelloControl: WinJS.Class.define(function (element, options) { element.winControl = this; this.element = element; if (options.message) { element.innerText = options.message; } }) });
После того, как элемент определен, вы можете использовать следующую разметку, чтобы WinJS.UI.process/processAll создали экземпляр объекта элемента управления (как встроенного элемента, так как мы использует span в качестве корневого элемента)
<span data-win-control="AppControls.HelloControl" data-win-options="{ message: 'Hello, World'}"> </span>
Обратите внимание на то, что код определения элемента должен быть исполнен перед вызовами WinJS.UI.process/processAll, то есть, чтобы функция конструктора, упомянутая в data-win-control, уже существовала в этот момент.
Для того, чтобы увидеть более полно реализованный элемент управления, вы можете взглянуть на пример "HTML SemanticZoom для пользовательских элементов управления" (http://code.msdn.microsoft.com/windowsapps/SemanticZoom-for-custom-4749edab). Мой друг Кеничиро Танака, из отделения Microsoft в Токио, кроме того, создал элемент управления, показанный на Рис. 4.9 и предоставил упражнение CalendarControl для этой лекции. (Обратите внимание на то, что этот пример лишь частично восприимчив к локализованным параметрам календаря, его не собирались делать полнофункциональным).
Следуя указаниям, данным выше, этот элемент управления определен с использованием WinJS.Class.define внутри пространства имен Control (calendar.js, строки 4-10 показанные здесь [комментарии опущены]).
WinJS.Namespace.define("Controls", { Calendar : WinJS.Class.define( function (element, options) { this.element = element || document.createElement("div"); this.element.className = "control-calendar"; this.element.winControl = this;
Оставшаяся часть конструктора (строки 12 - 63) создают дочерние элементы, которые определяют элемент управления, обеспечивая то, что каждый участок имеет собственное имя класса, будучи помещенным в пространство имен класса control-calendar, помещенного в корневой элемент, что позволяет независимо стилизовать отдельные части. Таблица стилей по умолчанию, применяемая здесь - это calendar.css; некоторые параметры переназначены в default.css, это отличает два элемента управления, приведенных на Рис. 4.9.
Внутри конструктора вы, кроме того, можете видеть, что элемент управления подключает собственные обработчики событий для элементов-потомков, таких, как кнопки предыдущий/следующий и каждая ячейка с датой. В последнем случае, щелчок по ячейке использует dispatchEvent для того, чтобы вызвать событие элемента управления dateselected.
Строки 63 - 127 определяют членов элемента управления. Здесь присутствуют два внутренних метода, _setClass и _update, за которыми следуют два публичных метода nextMonth и prevMonth, и три публичных свойства - year, month и date. Эти свойства можно установить посредством строки data-win-options в разметке, или напрямую через объект элемента управления, как мы скоро увидим.
В конце calendar.js вы можете увидеть два вызова WinJS.Class.mix для добавления свойств для событий (здесь лишь одно свойство), и методов стандартных событий DOM, таких, как addEventListener, removeEventListener и dispatchEvent вместе с setOptions:
WinJS.Class.mix(Controls.Calendar, WinJS.Utilities.createEventProperties("dateselected")); WinJS.Class.mix(Controls.Calendar, WinJS.UI.DOMEventMixin);
Очень хорошо то, что всё это весьма просто добавлять. WinJS, спасибо!2С технической точки зрения, WinJS.Class.mix принимает различное число аргументов, таким образом вы вполне можете объединить два вышеприведенных вызова в один.
Между calendar.js и calendar.css у нас есть объявление элемента управления. В default.html и default.js мы можем увидеть, как используется элемент управления. На Рис. 4.9 элемент управления слева объявлен в разметке и его экземпляр создан благодаря вызову WinJS.UI.processAll в default.js.
<div id="calendar1" class="control-calendar" aria-label="Calendar 1" data-win-control="Controls.Calendar" data-win-options="{ year: 2012, month: 5, ondateselected: CalendarDemo.dateselected}"> </div>
Вы можете видеть, как мы используем полное имя конструктора, так же, как и обработчика событий, который мы назначаем событию ondateselected. Но помните, что функции, на которые мы ссылаемся в разметке, как на эти, должны быть маркированы для строгой обработки. Конструктор автоматически маркирован с помощью WinJS.Class.Define, но обработчик события требует дополнительных усилий: мы помещаем функцию в пространство имён (для того, чтобы обеспечить её глобальную видимость) и используем WinJS.UI.eventHandler для того, чтобы выполнить маркировку:
WinJS.Namespace.define("CalendarDemo", { dateselected: WinJS.UI.eventHandler(function (e) { document.getElementById("message").innerText = JSON.stringify(e.detail) + " selected"; }) });
Опять же, если вы забыли маркировать функцию подобным образом, экземпляр элемента управления не будет создан. (Уберите контейнер WinJS.UI.eventHandler для того, чтобы это увидеть).
Для демонстрации создания элемента управления за пределами разметки, элемент управления справа на Рис. 4.9 создан так, как показано ниже в div-элементе calendar2:
//Как только мы создали данный элемент управления в коде, мы не зависим от WinJS.UI.processAll. var element = document.getElementById("calendar2"); //Как только мы предоставили элемент, он будет автоматически добавлен в DOM var calendar2 = new Controls.Calendar(element); //Так как этот обработчик не участвует в процессе обработки разметки, calendar2.ondateselected = function (e) { document.getElementById("message").innerText = JSON.stringify(e.detail) + " selected"; }
Всё готово!
Пользовательские элементы управления в Blend
Blend - это отличный дизайнерский инструмент, который позволяет работать с элементами управления напрямую, в монтажной панели, поэтому вам может быть интересно, как пользовательские элементы управления могут участвовать в подобном.
Для начала, так как пользовательские элементы управления это лишь элементы в DOM, Blend работает с ними так же, как и с другими частями DOM. Попытайтесь загрузить пример с календарём в Blend для того, чтобы увидеть это своими глазами.
Далее, элементы управления могут определять, исполняются ли они внутри Blend, в режиме дизайна, если свойство Windows.ApplicationModel.DesignMode.designModeEnabled установлено в true. Один из случаев, когда это весьма полезно, заключается в обработке строк ресурсов. Мы не будем подробно говорить о ресурсах до "Макет" курса "Программная логика приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript и их взаимодействие с системой", но это важно знать уже сейчас, что разрешение ресурсов посредством Windows.ApplicationModel.Resources.ResourceLoader, не работает в режиме дизайна в Blend, как это происходит при реальном исполнении приложения. Система напрямую заявляет об этом, выдавая исключение! Таким образом, вы можете использовать в режиме дизайна флаг для того, чтобы предоставить некоторые данные по умолчанию, вместо того, чтобы выполнять поиск данных.
Например, один из первых партнёров, с которым я работал, имел метод для извлечения локализованного URI со своего сервиса поддержки приложения, который не работал в режиме дизайна. Используя флаг режима дизайна, таким образом, мы просто изменили код, приведя его к такому виду:
WinJS.Namespace.define("App.Localization", { getBaseUri: function () { if (Windows.ApplicationModel.DesignMode.designModeEnabled) { return "www.default-base-service.com"; } else { var resources = new Windows.ApplicationModel.Resources.ResourceLoader(); var baseUri = resources.getString("baseUrl"); return baseUri; } } });
И, наконец, возможно сделать так, чтобы пользовательские элементы управления отображались на закладке Активы (Assets) среди прочих HTML-элементов и элементов управления WinJS. Для того, чтобы это сделать, вам, во-первых, нужен файл OpenAjax Metadata XML (OAM) (http://www.openajax.org/member/wiki/OpenAjax_Metadata_1.0_Specification_Descriptive), который предоставляет всю необходимую информацию для элемента управления, и у вас уже есть достаточно мест, где можно посмотреть подобные файлы. Для того, чтобы их найти, выполните поиск файлов по запросу *._oam.xml в папке Program Files (x86). Вы найдёте кое-что в папке Microsoft Visual Studio 11.0 и в папках с большей глубиной вложенности, в Microsoft SDKs, где располагаются метаданные WinJS. И там и там вы, кроме того, найдёте множество примеров значков размерами 12х12 и 16х16, которые понадобятся для вашего элемента управления.
Если вы посмотрите в папку controls/calendar упражнения CalendarControl к этой лекции, вы найдёте файл calendar_oam.xml и два значка вместе с файлами .js и .css. OAM-файл (который должен иметь окончание имени файла в виде _oam.xml) сообщает Blend о том, как сохранять элемент управления в его панели Активы (Assets), и какой код следует вставить, когда вы перетаскиваете элемент в HTML-файл. Вот содержимое этого файла:
<?xml version="1.0" encoding="utf-8"?> <!--Используйте символы подчеркивания или точки в id и name, но не пробелы. --> <widget version="1.0" spec="1.0" id="http://www.kraigbrockschmidt.com/scehmas/ProgrammingWin8_JS/Controls/Calendar" name="ProgWin8_JS.Controls.Calendar" xmlns="http://openajax.org/metadata"> <author name="Kenichiro Tanaka" /> <!-- title предоставляет имя, которое появляется в панели Активы (Assets) в Blend (иначе используется widget.name). --> <title type="text/plain"><![CDATA[Calendar Control]]></title> <!--обеспечивает подсказку для панели Активы (Assets). --> <description type="text/plain"><![CDATA[A single month calendar]]></description> <!-- значок (12x12 и 16x16) предоставляет маленький значок в панели Активы (Assets). --> <icons> <icon src="calendar.16x16.png" width="16" height="16" /> <icon src="calendar.12x12.png" width="12" height="12" /> </icons> <!-- Этот элемент описывает то, что будет вставлено в .html файл; закомментируйте всё лишнее --> <requires> <!-- Код элемента управления --> <require type="javascript" src="calendar.js" /> <!-- Код таблицы стилей элемента --> <require type="css" src="calendar.css" /> <!-- Встроенные скрипты для заголовка документа --> <require type="javascript"><![CDATA[WinJS.UI.processAll();]]></require> <!-- Встроенный CSS для раздела стилей в заголовке документа --> <!--<require type="css"><![CDATA[.control-calendar{}]]></require>--> </requires> <!-- Что вставлять в тело элемента управления; убедитесь в том, что это корректный HTML иначе Blend не позволит вставить его --> <content> <![CDATA[ <div class="control-calendar" data-win-control="Controls.Calendar" data-win-options="{ year: 2012, month: 6 }"></div> ]]> </content> </widget>
Когда вы добавите все пять файлов в проект в Blend, вы увидите значок и подпись элемента управления в панели Активы (Assets) (а если поместить указатель мыши над элементом, появится всплывающая подсказка):
Если вы перетащите этот элемент управления на HTML-страницу, вы увидите, как в её различные части добавлено следующее:
<!DOCTYPE html> <html> <head> <!-- ... --> <script src="calendar.js" type="text/javascript"></script> <link href="calendar.css" rel="stylesheet" type="text/css"> </head> <body> <div class="control-calendar" data-win-control="Controls.Calendar" data-win-options="{month:6, year:2012}"></div> </body> </html>
Но подождите! Что случилось с вызовом WinJS.UI.processAll(), который отмечен в XML тегом script в заголовке? Blend проверил этот участок кода на предмет того, встречается ли подобный вызов в уже загруженных скриптах. Если это так (как обычно бывает с шаблонами проектов), Blend не станет повторять его. Если он сочтёт нужным включить данный вызов, или если вы зададите здесь другой код, Blend вставит его в тег <script> в заголовке.
Кроме того, ошибки в вашем OAM-файле принудят Blend не вставлять элемент управления, таким образом, вам нужно исправить эти ошибки. Когда вы вносите изменения, Blend не перезагружает метаданные до тех пор, пока вы не перезагрузите проект или не переименуете OAM-файл (сохранив часть _oam.xml). Я нахожу последнее более простым, так как blend не заботит то, как выглядит остальная часть имени файла. В процессе переименования, так же, если вы обнаружите, что элемент управления исчез из панели Активы (Assets), это означает, что имеются ошибки в самой структуре OAM XML, такие, как неподходящие символы в значениях атрибутов. В подобных случаях вам понадобится метод проб и ошибок, и, конечно, вы можете обратиться к уже существующих на вашем компьютере OAM-файлам для того, чтобы разобраться с подробностями.
Кроме того, вы можете сделать ваш элемент управления доступным для всех проектов в Blend. Для того, чтобы сделать это, перейдите в папку Program Files (x86)\Microsoft Visual Studio 11.0\Blend, создайте папку Addins, если её там еще нет, создайте в ней подпапку для вашего элемента управления (используя подходящее уникальное имя), и скопируйте все активы вашего элемента управления туда. Когда вы перезапустите Blend, вы увидите, что ваш элемент управления находится в списке в категории Add-ins в панели Активы (Assets):
Подобным подходом можно воспользоваться, если вы создаете элементы управления для использования их другими разработчиками. Вашей программе установки следует просто расположить ваши активы в папку Addins. Что касается использования подобных элементов управления, когда вы перетаскиваете элемент в HTML-файл, необходимо, чтобы активы, необходимые элементу управления (но не значки и не OAM-файл), были скопированы в корневую папку проекта. Затем вы можете перемещать их так, как вам нужно, меняя ссылки на файлы, конечно.