Опубликован: 02.04.2013 | Уровень: для всех | Доступ: платный
Лекция 6:

Коллекции и элементы управления для вывода коллекций

Как, на самом деле, работают шаблоны

Ранее, когда мы рассматривали шаблон Приложение таблицы, я упоминал, что вы можете использовать функцию вместо декларативного описания шаблона для свойств наподобие itemTemplate (FlipView и ListView) и groupHeaderTemplate (ListView). Это важная возможность, так как она позволяет вам динамически выводить отдельные элементы в коллекции, используя содержимое каждого из них для настройки его внешнего вида. Это, кроме того, позволяет вам инициализировать отдельные элементы способами, которые нельзя выполнить в декларативной форме, такими, как объединение ячеек, отложенная загрузка изображений, добавление обработчиков событий для отдельных частей элемента и оптимизация производительности.

Мы вернемся к некоторым из этих вопросов позже. Сейчас полезно будет точно понять, что происходит с декларативными шаблонами и как это связано с пользовательскими функциями шаблонов.

Ссылки на шаблоны

Как я упоминал выше, что если вы ссылаетесь на декларативный шаблон в элементах управления FlipView или ListView, то вы ссылаетесь на элемент, а не на id элемента. id работает, так как хост-процесс приложения создаёт переменные с подобными именами для элементов, которые они идентифицируют. Однако, мы не рекомендуем подобный подход, особенно внутри элементов управления страниц (что вы, вероятно, часто используете). Первое ограничение касается того, что только один элемент может иметь конкретный id, что означает, что вы столкнётесь с по-настоящему странным поведением, если вы попытаетесь вывести элемент управления страницы дважды в той же самой DOM.

Второе ограничение касается проблем с выравниванием по времени. Переменная, соответствующая id элемента, которую предоставляет хост-процесс приложения, не создаётся до тех пор, пока HTML-код, содержащий элемент, будет добавлен в DOM. В случае с элементом управления страницы, WinJS.UI.processAll вызывается до этого, что означает, что переменные, соответствующие id элемента для шаблонов в данной странице еще не будут доступны. В результате, любые элементы управления, использующие id для шаблона, либо станут причиной выдачи исключения, либо - показа пустого экрана. Оба происшествия весьма нежелательны.

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

<div data-win-control="WinJS.Binding.Template" class="myItemTemplate" ...></div>

Затем, в декларативном описании элемента управления, используйте синтаксис select("<selector>") в записях свойств, где <selector> - это всё что угодно, поддерживаемое element.querySelector:

<div data-win-control="WinJS.UI.ListView"
data-win-options="{ itemTemplate: select('.myItemTemplate') }"></div>

Здесь происходит больше всего, нежели просто вызов querySelector. Функция select внутри параметров, выполняет поиск, начиная с корня его элемента управления страницы. Если совпадения не найдено, она ищет другой элемент управления страницы, выше в DOM, затем просматривает его, продолжая процесс до нахождения совпадения. Это позволяет вам безопасно использовать два элемента управления страницы, оба из которых содержат те же имена классов для разных шаблонов, и каждая страница будет использовать локальный шаблон.

Кроме того, вы можете получить элемент шаблона, используя напрямую в коде querySelector и присваивая результат свойству itemTemplate. Обычно это может быть выполнено в функции страницы ready, как показано в проекте Приложение таблицы, и выполнение этого удовлетворяет обоим ограничениям, приведенным здесь, так как querySelector будет ограничен содержимым страницы и будет исполнен после WinJS.UI.processAll.

Элементы шаблона и рендеринг

Следующий интересный вопрос о шаблонах заключается в том, что мы, на самом деле, получаем, когда создаём экземпляр объекта WinJS.Binding.Template (http://msdn.microsoft.com/library/windows/apps/br229723.aspx)?

Это, в большей или меньшей степени, другой элемент управления WinJS, который превращается в элемент при вызове WinJS.UI.processAll. Разница в том, что он удаляет все дочерние элементы из DOM, таким образом, сам по себе он никогда не отображается. Он даже не устанавливает свойство winControl элемента, который содержит его.

Что у него действительно имеется, однако, это весьма полезная функция, которая называется render. Получая контекст данных (объект со свойствами) и элемент, render создаёт полную копию шаблона внутри элемента, разрешая любые взаимоотношения в шаблоне, касающиеся привязки данных (и в атрибуте data-win-bind, и в data-win-options), используя контекст данных. Коротко говоря, воспринимайте декларативные шаблоны как набор инструкций, которые использует метод render для выполнения всех необходимых методов createElement вместе с установкой свойств и выполнением привязки данных.

Как показано в материале "Использование шаблонов для привязки данных" (http://msdn.microsoft.com/library/windows/apps/hh700356.aspx), вы можете просто создать экземпляр шаблона и отобразить шаблон везде, где хотите:

var templateElement = document.getElementById("templateDiv");
var renderHere = document.getElementById("targetElement");
renderHere.innerHTML = "";

WinJS.UI.process(templateElement).then(function (templateControl) {
templateControl.render(myDataItem, renderHere);
});

Должно быть полностью очевидно, что это то, что действительно выполняют элементы управления FlipView и ListView для каждого элемента в заданном источнике данных. В случае с FlipView, он вызывает метод render каждый раз, когда вы переходите на другой элемент в источнике данных. ListView перебирает itemDataSource и вызывает renderer шаблона элемента для каждого элемента, и делает что-то подобное для собственных groupDataSource и groupHeaderTemplate.

Функции шаблонов (Часть 1)

Зная теперь, что элемент управления WinJS.Binding.Template, это, в своей основе, просто набор декларативных инструкций для его функции render, вы можете создать пользовательскую функцию, которая напрямую выполняет то же самое. Таким образом, в дополнение к элементу, свойство itemTemplate у FlipView и ListView и свойство ListView groupHeaderTemplate может так же принимать функцию рендеринга (отображения). Элементы управления используют typeof во время выполнения для того, чтобы определить, что вы присвоили данным свойствам, в итоге, если вы предоставите элемент шаблона, элемент управления вызовет его метод render. Если вы предоставили функцию, элемент управления просто вызовет данную функцию для каждого элемента, который нужно отобразить. Это предоставляет большую гибкость в настройке шаблона, основываясь на индивидуальных данных элемента.

На самом деле, функция рендеринга позволяет вам индивидуально контролировать не только то, как конструируются отдельные части каждого элемента, но и то, когда это происходит. Как таковая, функция рендеринга - это основное средство, с помощью которого вы можете реализовать пять прогрессивных уровней оптимизации, особенно для ListView. Осторожно! Впереди promise-объекты! Хорошо, я оставил большую часть этого разговора для конца лекции, потому что прежде нам нужно взглянуть на другие особенности ListView. Но здесь давайте, по крайней мере, взглянём на базовую структуру функции рендеринга, которая применима и к FlipView и к ListView, и которую вы можете видеть в примерах "Шаблоны элемента для HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-item-templates-7d74826f) и "Оптимизация производительности HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-performance-39fb71f0). Ниже мы воспользуемся кодом, взятым из них.

Для начинающих, вы можете указать функцию для рендеринга, используя её имя в data-win-options и для элемента управления ListView, и для элемента управления FlipView. Эта функция должна быть отмечена для обработки, как обсуждалось в "Элементы управления, их стилизация и привязка данных" , так как она участвует в WinJS.UI.processAll, в итоге, это отличное место для использования WinJS.Utilities. markSupportForProcessing. Обратите внимание на то, что если вы присваиваете данную функцию itemTemplate или groupHeaderTemplate в JavaScript, она не нуждается в маркировке.

В базовой форме, функция шаблона принимает promise-объект элемента в качестве первого аргумента и возвращает promise-объект, обработчик завершения которого создаёт его отдельные части. Ха? Да, меня это тоже смущает! Посмотрим на базовую структуру в составе двух функций:

function basicRenderer(itemPromise) {
return itemPromise.then(buildElement);
};

function buildElement (item) {
var result = document.createElement("div");

//Создаёт элемент, обычно используя innerHTML
return result;
}

Функция рендеринга здесь первая. Она просто сообщает нам: "Когда itemPromise будет исполнен, что означает доступность элемента, вызвать функцию buildElement для данного элемента". Возвращая promise-объект из itemPromise.then (не done, обратите внимание), мы позволяем элементу управления для коллекций, который использует данную функцию рендеринга, объединить в цепочку promise-объекты элементов и promise-объекты, создающие элементы. Это особенно полезно, когда данные для элементов поступают из некоего сервиса или другого потенциально медленного источника данных, и это весьма полезно при инкрементной загрузке страницы, так как это позволяет элементу управления отменить promise-цепочку, если страница прокручена в другое место до того, как данная операция завершится. Коротко говоря, это - хорошая идея.

Просто для демонстрации, здесь показано, как мы можем сделать функцию рендеринга напрямую доступной из разметки, как в data-win-options = "{itemTemplate: Renderers.basic }":

WinJS.Namespace.define("Renderers", {
basic: WinJS.Utilities.markSupportedForProcessing(function (itemPromise) {
return itemPromise.then(buildElement);
})	
 }

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

function basicRenderer(itemPromise) {
return itemPromise.then(function (item) {
var result = document.createElement("div");

//Создание элемента, обычно с использованием innerHTML

return result;
})
};

То, что вы затем делаете внутри функции создания элемента (либо именованной, либо анонимной), определяет макет и внешний вид элемента. Возвращаясь к Сценарию 8, который мы добавили к примеру использования FlipView, мы можем взять следующий декларативный шаблон, где мы должны воспользоваться некоторыми хитростями для того, чтобы заставить работать привязку данных:

<div id="pictures_ItemTemplate" data-win-control="WinJS.Binding.Template">
<div class="overlaidItemTemplate">
<img class="image" data-win-bind="src: thumbnail InitFunctions.thumbURL;
alt: name WinJS.Binding.oneTime" />
<div class="overlay">
<h2 class="ItemTitle" data-win-bind="innerText: name WinJS.Binding.oneTime"></h2>
</div>
</div>
</div>

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

//Ранее: присваивание шаблона в коде
myFlipView.itemTemplate = thumbFlipRenderer;


//Функция рендеринга (смотрите "Функции шаблонов (Часть 2) ниже для оптимизации)
function thumbFlipRenderer(itemPromise) {	
return itemPromise.then(buildElement);	
};	

//Функция, которая строит дерево элементов	
function buildElement (item) {	
var result = document.createElement("div");
result.className = "overlaidItemTemplate";	

var innerHTML = "<img class='thumbImage'>";	
var innerHTML += "<div class='overlay'>";	
innerHTML += "<h2 class='ItemTitle'>" + item.data.name + "</h2>";
innerHTML += "</div>";	

result.innerHTML = innerHTML;

//Задание слушателя для thumbnailUpdated который осуществляет вывод в элемент img 
var img = result.querySelector("img");
WinJS.UI.StorageDataSource.loadThumbnail(item, img).then();

return result;
}

Так как у нас уже есть отдельные элементы, нам не нужно отвлекаться на детали декларативной привязки данных и конвертеров: мы можем просто напрямую использовать нужные нам свойства из item.data. Как и ранее, помните, что свойство thumbnail (эскиз) элемента FileInformation может быть еще не установлено. Это то место, где мы можем использовать метод StorageDataSource.loadThumbnail для прослушивания события FileInformation.onthumbnailupdated. Эта вспомогательная функция выведет эскиз в наш элемент img, когда эскиз станет доступен (с небольшой анимацией в придачу!).

Совет. Вы могли, кроме того, заметить, что я создавал большинство элементов, используя корневое свойство div.innerHTML вместо вызова createElement и appendChild и установки конкретных свойств напрямую. За исключением очень простых структур, установка innerHTML в корневом элементе более эффективна, так как мы минимизируем количество вызовов API DOM. Это не играет большой роли для элемента управления FlipView, элементы которого выводятся по одному за раз, но это становится очень важным для ListView, который вполне может иметь тысячи элементов. На самом деле, когда мы начинаем задумываться об оптимизации производительности, мы, кроме того, хотим выводить элемент на разных стадиях, как при отложенной загрузке изображений. Мы увидим подробности в разделе "Функции шаблонов (Часть 2): Promise-объекты!" в конце этой лекции.
Владимир Мороз
Владимир Мороз
Украина, Киев, Киевская государственная академия водного транспорта имени Гетмана Петра Конашевича-Сагайдачного, 2012
Сергей Ширяев
Сергей Ширяев
Россия, г. Москва