Введение в разработку приложений для Windows 8 с использованием HTML, CSS и JavaScript

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

Разбить на страницы
Показывать лекцию целиком

Файлы к данной лекции Вы можете скачать здесь.

Можно с уверенностью говорить о том, что везде, где бы вы ни были, вы окружены множеством коллекций. Книги, которые вы обычно читаете - это коллекция глав, а глава - это коллекция страниц. Страницы, в свою очередь, являются коллекциями абзацев, абзацы - коллекции слов, слова - коллекции букв, а буквы (если вы читаете это в электронном виде) - это коллекции пикселей. Всё дальше и дальше…

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

Мы воспринимаем всё это как коллекции, так как мы знаем, как обобщить отдельные объекты - как листья или страницы, или игрушки - в категории или группы. Это даёт нам мощный инструмент для организации этих вещей и управления ими (за исключением одежды в моём шкафу, моя жена может это подтвердить). И так же, как физический мир вокруг нас во многом состоит из коллекций, цифровой мир, который мы используем для того, чтобы представить объекты реального мира, так же полон коллекций. Языки программирования, наподобие JavaScript, имеют конструкции, такие, как массивы, для организации коллекций данных и управления ими, и окружение, наподобие Windows 8, обеспечивает элементы управления для коллекций, с помощью которых мы можем визуализировать данные и управлять ими.

В данной лекции мы обратим наше внимание на два элемента управления для коллекций, которые предоставлены WinJS. Это FlipView (представление отражения), который отображает один элемент коллекции, и ListView (представление списка), который отображает множество элементов, организуя их по-разному. Как вы можете ожидать, ListView обладает большей функциональностью. Так как он является одной из центральных деталей во многих подходах к проектированию приложений, мы потратим основное количество времени на то, чтобы глубоко его изучить, а так же изучим концепцию и реализацию семантического масштабирования (semantic zoom) (на самом деле, другого элемента управления).

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

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

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

Для рассмотрения основ элементов управления для коллекций, посмотрим сначала на FlipWiew, который позволит нам начать разговор о шаблонах элементов и источниках данных. Затем мы увидим, как это можно применить к ListView, затем разберем группировку элементов в ListView.

Быстрый старт №1: пример использования элемента управления FlipWiew

Как показано на Рис. 5.1, пример "Элемент управления FlipView" (http://code.msdn.microsoft.com/windowsapps/FlipView-control-sample-18e434b4) содержит неплохой код для этого элемента управления и его визуальное представление, позволяющее исследовать этот элемент управления. (Я испытываю особую благодарность за то, что мне не пришлось писать подобные примеры для этого курса!). Для целей быстрого старта, посмотрим на первый сценарий, касающийся заполнения элемента управления из простого источника данных и использования шаблона для рендеринга этого элемента, так как такие же механизмы применяются в ListView. Позже мы вернемся к другим сценариям использования FlipView.

(рис 5.1) Пример использования элемента управления FlipView, где элемент управления отображает картинку

Так как FlipView - это элемент управления WinJS, с конструктором WinJS.UI.FlipView, мы объявляем его в разметке с атрибутами data-win-control и data-win-options (смотрите html/simpleFlipView.html):

<div id="simple_FlipView" class="flipView" data-win-control="WinJS.UI.FlipView"
data-win-options="{ itemDataSource: DefaultData.bindingList.dataSource,	
itemTemplate: simple_ItemTemplate }">	
</div>

И, конечно, в процессе загрузки сраницы вызывается WinJS.UI.processAll для создания экземпляра элемента управления. В параметрах FlipView мы можем сразу же увидеть два критически важных участка, благодаря которым он работает: это источник данных, который предоставляет содержимое, нужное каждому элементу, и шаблон для вывода элемента.

Если вы обратили внимание на конец лекции 4, вы, возможно, догадываетесь, что шаблон - это экземпляр WinJS.Binding.Template. И вы правы! Эта часть разметки, на самом деле, расположена перед объявлением элемента управления в html/simpleFlipView.html.

<div id="simple_ItemTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="overlaidItemTemplate">
<img class="image" data-win-bind="src: picture; alt: title" />
<div class="overlay">
<h2 class="ItemTitle" data-win-bind="innerText: title"></h2>
</div>
</div>
</div>

Обратите внимание на то, что шаблон должен быть всегда объявлен в разметке до элемента управления, который на него ссылается: WinJS.UI.processAll должен создать экземпляр шаблона прежде чем элемент управления запросит шаблон для вывода своего содержимого для каждого элемента источника данных. Так же вспомните, из лекции 4, что создание экземпляра шаблона убирает его содержимое из DOM, и, таким образом, оно не может быть изменено во время выполнения программы. Вы можете видеть это, исполняя пример: разверните узлы в Проводнике DOM (DOM Explorer) в Visual Studio, или в панели Динамическая DOM (Live DOM) в Blend, и вы увидите лишь корневой элемент шаблона div, который не имеет элементов-потомков.

В примере, прозаически названный Обратите внимание на то, что для того, чтобы подобные элементы управления были полностью интерактивными, присвойте им класс win-intaractive, иначе окружающий их элемент управления (это применимо и для ListView) поглотит события ввода до того, как они достигнут данных элементов управления..

Кроме того, вы увидите, что шаблон использует атрибуты привязки данных WinJS, где свойства img.src, img.alt и h2.innerText привязаны к свойствам источника данных, которые называются picture и title. Это показывает, как свойства двух целевых элементов могут быть привязаны к одному источнику. (Помните, что если вы осуществляете привязку к свойствам элемента управления WinJS, а не к его элементам-потомкам, эти свойства должны начинаться с winControl).

Что касается источника данных, то параметру itemDataSource элемента управления FlipView присвоено значение DefaultData.bindingList.dataSource, описание источника вы можете найти в js/DefaultData.js:

var array = [
{ type: "item", title: "Cliff", picture: "images/Cliff.jpg" },
{ type: "item", title: "Grapes", picture: "images/Grapes.jpg" },
{ type: "item", title: "Rainier", picture: "images/Rainier.jpg" },
{ type: "item", title: "Sunset", picture: "images/Sunset.jpg" },
{ type: "item", title: "Valley", picture: "images/Valley.jpg" }
];
var bindingList = new WinJS.Binding.List(array);

WinJS.Namespace.define("DefaultData", {
bindingList: bindingList, 
array: array
});

Мы кратко ознакомились с WinJS.Binding.List в конце лекции 4. Его цель заключается в том, чтобы превратить массив, хранящийся в памяти, в наблюдаемый источник данных для односторонней привязки данных. Контейнер WinJS.Binding.List, кроме того, необходим, так как FlipView и ListView не могут работать напрямую с простыми массивами, даже при единовременной привязке данных. Они ожидают от источников данных наличия у них методов интерфейса WinJS.UI.IListDataSource. Свойство dataSource WinJS.Binding.List, как в bindingList.dataSource, предоставляет то же самое, и вы всегда будете использовать это свойство вместе с FlipView и ListView (Оно, на самом деле, существует лишь для этого). Если вы об этом забудете и попытаетесь осуществить прямую привязку к WinJS.Binding.List, вы увидите исключение, в котором говорится о том, что "Объект не поддерживает свойство или метод 'createListBinding'" ("Object doesn't support property or method 'createListBinding'.")

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

Отметим, что WinJS.Binding.List полностью поддерживает динамические данные. Если вы посмотрите на его описание (http://msdn.microsoft.com/library/windows/apps/hh700774.aspx) в документации, вы увидите, что он выглядит практически так же, как JavaScript-массив, со свойством length и полным набором методов массивов - от concat и indexOf до push, pop и unshift. Они устроенны именно так, как можно этого ожидать: от вас не требуется повторное изучение основ.

Кроме того, важно отметить, что и для FlipView, и для ListView, подобная установка свойства элемента управления itemDataSource автоматически создаёт одностороннюю привязку данных, в итоге, любое изменение в объекте-списке, или даже в массиве, на основании которого он построен, запустит автоматическое обновление связанного элемента управления.

Быстрый старт №2a: основные возможности HTML ListView

Как я уже говорил раньше, основные механизмы, касающиеся источников данных и шаблонов, применимые к ListView, это те же, что применимы к FlipView. Вы можете увидеть всё это в примере "Основные возможности HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-basic-usage-sample-fcc451db), Рис. 5.2. Обратите внимание на первых два сценария, которые касаются создания элемента управления и ответа на события отдельных элементов, которые он отображает.

Так как ListView способен одновременно отображать множество элементов, ему нужно еще кое-что, в дополнение к источнику данных и шаблону. Что-то, что описывало бы, как эти элементы визуально соотносятся друг с другом. Это - свойство layout элемента управления ListView, которое мы увидим в разметке, в Сценарии 1 данного примера, вместе с некоторыми другими параметрами поведения (html/scenario1.html).

<div id="listView" data-win-control="WinJS.UI.ListView"	
data-win-options="{ itemDataSource: myData.dataSource,	
itemTemplate: select('#smallListIconTextTemplate'), selectionMode: 'none',	
tapBehavior: 'none', swipeBehavior: 'none', layout: { type: WinJS.UI.GridLayout } }">
</div>
(рис 5.2) Пример "Основные возможности HTML ListView"

Конструктор ListView, WinJS.UI.ListView, конечно, вызывается вездесущим WinJS.UI.processAll, когда загружен элемент управления страницей. Источник данных для данного списка установлен в myData.dataSource, где myData - это, опять же, WinJS.Binding.List (определенный в js/data.js, на основе обычного массива) и его свойство dataSource обеспечивает необходимый интерфейс.

Шаблон отдельного элемента определен ранее в default.html, с параметром id, равным smallListIconTextTemplate, и это, в основном, то же самое, что мы видим в FlipView (img и текстовые элементы), поэтому я не привожу здесь их описание.

В параметрах элемента управления мы видим три свойства, влияющих на поведение элемента: selectionMode, tapBehavior и swipeBegavior. Все они в этом примере установлены в 'none' для того, чтобы полностью отключить возможность выделения элементов и щелчков мышью по ним, превращая ListView в средство пассивного отображения объектов. Содержимое в нём можно перематывать, но элементы не отвечают на ввод. (Смотрите, кроме того, врезку "Стилизация элементов при зависании над ними указателя мыши")

Что касается свойства layout, то это объект, свойство type которого показывает, какой макет использовать. WinJS.UI.GridLayout, который используется здесь, отображает элементы сверху вниз и слева направо, что подходит для горизонтальной прокрутки. WinJS предоставляет и другой тип макета, который называется WinJS.UI.ListLayout. Это одномерный список, размещающий элементы сверху вниз, который подходит для вертикальной прокрутки, особенно - в прикрепленном режиме просмотра. (Мы скоро увидим это при рассмотрении шаблона Приложение таблицы. Пример ListView, который мы здесь рассматриваем, не имеет хорошего варианта для прикрепленного режима просмотра).

Сейчас элемент управления ListView в Сценарии 1 лишь отображает элементы, часто нужно, чтобы они реагировали на щелчок мыши или прикосновения. Сценарий 2 показывает это, здесь свойство tapBehavior установлено в 'invoke' (смотрите html/scenario2.htm). Это равносильно использованию WinJS.UI.tapBehaviortoggleSelect, как показано в справке по перечислению tapBehavior (http://msdn.microsoft.com/library/windows/apps/hh701303.aspx) для "invoke". Данный вариант поведения позволяет выделить элемент или снять с него выделение в зависимости от его состояния, и затем активирует его. Другой вариант - это directSelect, где элемент всегда выделен, и затем активируется, при этом invokeOnly исполняется лишь тогда, когда элемент активирован без изменения состояния выделения. Кроме того, вы можете установить параметры поведения элемента в none, в итоге прикосновение или щелчок мышью будут проигнорированы.

Когда отображаемый элемент активируется, элемент управления ListView запускает событие itemInvoked. Вы можете подключить обработчик события, используя либо addEventListener, либо свойство oniteminvoked элемента ListView. Вот как это выполняется в Сценарии 2 (слегка реорганизованный код из js/scenario2.js):

var listView = element.querySelector('#listView').winControl;
listView.addEventListener("iteminvoked", itemInvokedHandler, false);
}

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

В вышеприведенном коде, вы можете так же назначить обработчик, используя напрямую свойство listView.oniteminvoked , или вы можете задать обработчик в свойстве itemInvoked в data-win-options (в таком случае функция должна быть отмечена безопасной для обработки). Объект события, который вы затем получите в обработчике, содержит promise-объект для активированного элемента, а не сам элемент, поэтому вам нужно вызвать его метод done или then для того, чтобы получить данные элемента. Кроме того, полезно знать, что вам никогда не следует менять свойство источника данных ListView напрямую, внутри обработчика itemInvoked, так как это, возможно, вызовет исключение. Если вам нужно это сделать, заключите код, вносящий изменения, в setImmediate, так вы сможете вернуться в поток пользовательского интерфейса.

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

В то время, как отключение возможности выделения и реакции на прикосновения в ListView приводит к созданию пассивного элемента управления, зависание над элементом указателя мыши (или соответствующее событие на подходящем для этого сенсорном оборудовании), приводит к подсветке каждого элемента. Вернитесь к Рис. 5.2. Вы можете управлять этим, используя псевдо-селектор .win-container:hover для элементов управления. Например. следующее правило стиля полностью убирает эффект, проявляющийся при зависании указателя мыши:

#myListView .win-container:hover {
background-color: transparent;
outline: 0px;	
	}

Быстрый старт №2b: пример группировки элементов в ListView

Отображение списка элементов - это замечательно, но чаще коллекции нуждаются в ином уровне организации - в том, что мы называем группировкой. Это вполне очевидно, когда я открываю ящик с документами около рабочего стола, который содержит набор документов, одни из которых более важные, другие - менее. На ярлыках папок с документами я вижу надписи, показывающие их отношение к той или иной группе: Налоги, Финансы, Сообщества, Страхование, Машина, Литературный проект и Разное (среди прочих). Очевидно, нам нужно средство для группировки элементов внутри элемента управления и ListView счастлив нам в этом помочь.

Базовую демонстрацию группировки можно найти в примере "Группировка в HTML ListView и семантическое масштабирование" (http://code.msdn.microsoft.com/windowsapps/ListView-grouping-and-6d032cc1) (Рис. 5.3). Как и в случае с примером, демонстрирующим основные возможности, код в js/groupedData.js содержит массив, располагаемый в памяти, на основе которого мы создаём WinJS.Binding.List. Вот выдержка, показывающая структуру элемента (я показал бы весь массив, но после этого мне захочется чего-нибудь сладкого!):

var myList = new WinJS.Binding.List([	
{ title: "Banana Blast", text: "Low-fat frozen yogurt", picture: "images/60Banana.png" },
{ title: "Lavish Lemon Ice", text: "Sorbet", picture: "images/60Lemon.png" },	
{ title: "Creamy Orange", text: "Sorbet", picture: "images/60Orange.png" },	
...

Здесь есть набор элементов со свойствами title, text и picture. Мы можем сгруппировать их так, как нам захочется, и даже изменить группировку "на лету". Как показывает Рис. 5.3, здесь образцы сгруппированы по первым буквам свойства title.

(рис 5.3) Группировка HTML ListView и пример использования семантического масштабирования

Если вы взглянете на описание ListView (http://msdn.microsoft.com/library/windows/apps/br211833.aspx), вы увидите, что элемент управления работает с двумя шаблонами и двумя коллекциями: то есть, наряду со свойствами itemTemplate и itemDataSource, это свойства groupHeaderTemplate (шаблон заголовка группы) и groupDataSource (источник данных группы). Они используются с gridLayout элемента управления ListView (по умолчанию) для организации групп и создания заголовков над элементами.

Шаблон заголовка в html/scenario1.html очень прост (и шаблон элемента очень похож на то, что мы уже видели):

<div id="headerTemplate" data-win-control="WinJS.Binding.Template">
<div class="simpleHeaderItem">	
<h1 data-win-bind="innerText: title"></h1>	
</div>	
</div>

На него есть ссылка в объявлении элемента управления (другие параметры опущены):

<div id="listView" data-win-control="WinJS.UI.ListView"	
data-win-options="{ groupDataSource: myGroupedList.groups.dataSource,
groupHeaderTemplate: headerTemplate }">	
</div>

В случае с источником данных, вы можете видеть, что мы используем переменную, которая называется myGroupedList со свойством внутри, которое называется groups. Что всё это значит?

Давайте устроим короткое концептуальное отступление. Хотя компьютеры без проблем обрабатывают большие объёмы исходных данных, вроде массива myList, людям удобнее видеть информацию в более организованном виде. Три основных способа достижения этого - группировка (grouping), сортировка (sorting) и фильтрация (filtering). Группировка организует элементы в группы, как показано на Рис. 5.3. Сортировка располагает их в соответствии с различными правилами. Фильтрация позволяет выбирать подмножества элементов, которые удовлетворяют определенным критериям. Во всех трёх случаях, однако, вам не нужно, чтобы эти операции меняли базовые данные: пользователь может захотеть группировать, сортировать или фильтровать одни и те же данные различными способами в разное время.

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

Объект WinJS.Binding.List предоставляет методы для создания этих проекций: createGrouped (http://msdn.microsoft.com/library/windows/apps/Hh700742.aspx), createSorted (http://msdn.microsoft.com/library/windows/apps/hh700743.aspx) и createFiltered (http://msdn.microsoft.com/library/windows/apps/hh700741.aspx). Каждый из методов создаёт особую форму WinJS.Binding.List: GroupedSortedListProjection, SortedListProjection и FilteredListProjection соответственно. Таким образом, каждая из проекций представляет собой список, подходящий для привязки данных, с некоторыми дополнительными методами и свойствами, которые специфичны для каждой из проекций. Вы даже можете создать одну проекцию из другой. Например, команда createGrouped(...).createFiltered(...) создаст отфильтрованную проекцию на основе сгруппированной проекции. (Обратите внимание на то, что метод sort не создаёт проекцию. Он применяет операцию сортировки на проекции, для которой вызывается, как команда sort для массивов JavaScript).

Теперь, когда мы знаем о проекциях, мы можем увидеть, как создан myGroupedList:

var myGroupedList = myList.createGrouped(getGroupKey, getGroupData, compareGroups);

Этот метод принимает в качестве параметров три функции. Первая - функция ключа группы (group key), связывает элемент с группой: она получает элемент и возвращает подходящую строку группы, известную как ключ. Ключ, который должен быть строкой, может быть чем-то, что прямо включено в элемент, или может быть получен из свойств элемента. В примере, функция getGroupKey возвращает первый символ свойства элемента title (в верхнем регистре). Обратите, однако, внимание, что исходный пример просто использует charAt для получения характеристики группировки, однако этот подход не работает для большого количества языков. Вместо этого, используйте класс Windows.Globalization.Collation.CharacterGroupings (http://msdn.microsoft.com/library/windows/apps/windows.globalization.collation.charactergroupings.aspx) и его метод lookup (http://msdn.microsoft.com/library/windows/apps/windows.globalization.collation.charactergroupings.lookup.aspx), как показано ниже, который нормализует регистр символов автоматически, в итоге, вызов toLocaleUpperCase (http://msdn.microsoft.com/library/6t6xaca8.aspx) необязателен:

var cg = Windows.Globalization.Collation.CharacterGroupings();

function getGroupKey(dataItem) {
return cg.lookup(dataItem.title);
}

Этот код, а так же другие изменения, сделанные ниже, можно найти в измененной версии данного примера, который включён в дополнительные материалы к курсу.

Знайте, что эта первая функция, которую мы называем функция ключа группы, определяет только связь между элементом и группой, и ничего больше. Она так же вызывается для каждого элемента в коллекции, когда осуществляется вызов createGrouped, таким образом, она должна работать быстро. По этой причине создание CharacterGroupings осуществляется за пределами функции. Совет. Если извлекать ключ группы из элемента во время выполнения приложения нужно для обеспечения работоспособности связанных механизмов, вы улучшите общую производительность, сохраняя заранее полученный ключ в элементе, вместо того, чтобы просто получать его из функции ключа группы.

Данные для групп, которые являются коллекциями, к которым присоединен шаблон заголовка, на самом деле, не создаются до того, как будет вызван метод проекции группы group, что происходит, когда обрабатывается параметр groupedDataSource элемента управления ListView. В этот момент вызывается вторая функция, переданная createGrouped - функция данных группы. Она вызывается лишь однажды для каждой группы с представляющим (representative) элементом для данной группы. В ответ, функция возвращает объект для данной группы, который содержит все свойства, которые нужны для привязки данных.

В примере, функция gerGroupData (переданная createGroup) просто возвращает объект с единственным свойством groupTitle, которое является тем же самым, что и ключ группы, но, конечно, вы можете сделать это значение любым. Этот код так же модифицирован, в сравнении с исходным примером, для того, чтобы подходить для целей глобализации. Мы добиваемся этого, повторно используя getGroupKey:

function getGroupData(dataItem) {
return {	
groupTitle: getGroupKey(dataItem)
};	
}	

В модифицированном примере я изменил имя свойства объекта данных этой группы title на более явное groupTitle для того, чтобы было совершенно понятно, что он ни коим образом не влияет на свойство title отдельных элементов. Это подразумевает изменение шаблонов заголовков в html/scenario1.html и html/scenario2.html, чтобы они ссылались на groupTitle. Это поможет нам быть уверенными в том, что контексты данных шаблона элемента и заголовка отличаются. В случае с шаблоном заголовка, это коллекция, созданная из значений, возвращённых функцией данных группы. В случае с шаблоном элемента, это проекция группы из WinJS.Binding.List.createGrouped. Две разные коллекции - помните об этом.

А почему функция данных группы отделена от других? Почему бы просто не создать эту коллекцию автоматически из ключей групп? Это потому что часто нужно включать дополнительные свойства в данные группы для использования их в шаблоне заголовка, или в масштабированном виде (с помощью функции семантического масштабирования). Воспринимайте функцию данных группы как о том, что обеспечивает общую информацию для каждой группы. (Текст заголовка - это наиболее часто используемый элемент подобной информации). Так как эта функция вызывается лишь раз для каждой группы, вместо того, чтобы вызываться для каждого элемента, её вызов - это подходящее время для вычисления или получения из других источников сводных данных общего уровня. Например, для того, чтобы показать количество элементов в заголовке группы, нам лишь нужно включить данное свойство в объект, который возвращает функция данных группы, затем осуществить привязку данных к элементу в заголовке, который должен быть связан с данным свойством.

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

function getGroupData(dataItem) {
var key = getGroupKey(dataItem);
	
//Получает отфильтрованную проекцию для нашего списка, проверяет на совпадение ключей
var filteredList = myList.createFiltered(function (item) {
return key == getGroupKey(item);
});	
return {
title: key,
count: filteredList.length
};	
	}

Что касается свойства count в коллекции, мы можем использовать его в шаблоне заголовка:

<div id="headerTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
<div class="simpleHeaderItem">	
<h1 data-win-bind="innerText: groupTitle"></h1>	
<h6><span data-win-bind="innerText: count"></span> items</h6>	
</div>	
</div>

После незначительных манипуляций в css/scenario1.css - изменения высоты класса simpleHeaderItem до 65 пикселей для того, чтобы получить немного дополнительного места - список будет выглядеть так, как показано ниже:

И, наконец, вернемся к Это действие полностью отделено от создания отсортированной проекции отдельных элементов, для которого вы использовали бы WinJS.Binding.List.createSorted.. Эта функция принимает два ключа группы и возвращает ноль, если они равны, отрицательное число, если первый ключ при сортировке расположен перед вторым, и положительное число, если второй ключ расположен перед первым. Функция compareGroups в примере выполняет сортировку по алфавиту, которую я обновил в модифицированной версии для того, чтобы она соответствовала особенностям приложений, рассчитанных на глобальный рынок:

function compareGroups(left, right) {
return groupCompareGlobalized(left, right);
}

function groupCompareGlobalized(left, right) {
var charLeft = cg.lookup(left);
var charRight = cg.lookup(right);
// Если оба имеют одинаковый символ группы, воспринимает их как одинаковые
if (charLeft.localeCompare(charRight) == 0) {	
return 0;	
}	

// В разных группах, мы должны полагаться на сортировку с учетом языкового стандарта так как
// имена групп не сортируются так же, как сами группы, для некоторых языков.	
return left.localeCompare(right);	
 }

Для двухуровневой сортировки, сначала - по убыванию номера группы, потом по первому символу, мы можем написать следующее (это модифицированный пример, на этот код надо сослаться в myListcreateGrouped для того, чтобы увидеть его в действии).

function compareGroups2(left, right) {
var leftLen = filteredLengthFromKey(left);
var rightLen = filteredLengthFromKey(right);

if (leftLen != rightLen) {
return rightLen - leftLen;
}
return groupCompareGlobalized(left, right);
}
function filteredLengthFromKey(key) {	
var filteredList = myList.createFiltered(function (item) {
return key == getGroupKey(item);	
});	
return filteredList.length;
}

Отладка функций группировки

Если кажется, что различные функции группировки работают неправильно, вы можете устанавливать точки останова и пошагово исполнять код несколько раз, но это может превратиться в утомительную задачу, так как функции вызываются очень много раз даже для коллекций умеренного размера. Вместо этого, попробуйте воспользоваться console.log для того, чтобы узнать о том, какие параметры передаются данным функциям и/или какие значения возвращаются, что позволит вам получить итоговый результат гораздо быстрее. Для того, чтобы увидеть, что происходит в функции сортировки группы, например, попробуйте такой код:

console.log("Comparing left = " + left + " to right = " + right);

Элемент управления ListView в шаблоне Приложение таблицы

Теперь, когда мы рассмотрели подробности об элементе управления ListView и об источниках данных, расположенных в памяти, мы можем полностью понять устройство шаблона Приложение таблицы (Grid App) в Visual Studio и Blend. Как было упомянуто в подразделе "Процесс и стили навигации" раздела "Элементы управления страниц и навигация" лекции 3, этот шаблон проекта предоставляет структуру приложения, построенную вокруг навигации по страницам: домашняя страница (pages/groupedItems) отображает коллекцию примеров данных (js/data.js) в элементе управления ListView, где каждое представление элемента описано с помощью WinJS.Binding.Template, так же, как и заголовки групп. Рис. 5.4 показывает макет домашней страницы и идентифицирует соответствующие элементы ListView. Как мы уже обсуждали, жест прикосновения к элементу вызывает перемещение на страницу pages/groupDetail, и сейчас мы сможем увидеть, как всё это работает с ListView.

Элемент управления ListView на Рис. 5.4 занимает нижнюю часть области содержимого приложения. Так как он поддерживает горизонтальную прокрутку, он, на самом деле, выходит за края. Использованы различные CSS-поля для выравнивания первого элемента и элементов макета, которые позволяют ему заходить за левый край при прокрутке ListView.

(рис 5.4) Элементы в ListView на домашней странице шаблона Приложение таблицы (Все цветные элементы - это добавленные для разъяснений метки и линии)

Заголовки групп (Group headers)

Элементы, выведенные из шаблона (Items rendered from template)

Элемент управления ListView (Полностью примыкает к краям) (ListView Control (full bleed to sides))

С ListView в этом проекте происходит не так уж и много всего, поэтому рассмотрим происходящее пошагово. Для начинающих, разметка элемента управления в pages/groupedItems/groupedItems.html весьма стандартна, среди параметров - лишь тот, который указывает на то, что элементы никак не реагируют на выделение:

<div class="groupeditemslist win-selectionstylefilled"
aria-label="List of groups" data-win-control="WinJS.UI.ListView" 
data-win-options="{ selectionMode: 'none' }">
</div>

Переходя к pages/groupedItems/groupedItems.js, мы можем сказать, что метод ready обрабатывает инициализацию:

ready: function (element, options) {	
var listView = element.querySelector(".groupeditemslist").winControl;	
listView.groupHeaderTemplate = element.querySelector(".headerTemplate");
listView.itemTemplate = element.querySelector(".itemtemplate");	
listView.oniteminvoked = this._itemInvoked.bind(this);	
// (Инициализация обработчика событий клавиатуры опущена)...

this.initializeLayout(listView, appView.value);
listView.element.focus();
},

Здесь вы можете видеть, что шаблон элемента управления может быть задан в коде так же просто, как это делается в разметке, и в этом случае мы используем класс для того, чтобы определить местоположение элемента шаблона, вместо использования id. Почему это работает? Потому, что мы, на самом деле, ссылаемся на элемент всё время: хост-процесс приложения автоматически создаёт переменную для элемента, которая имеет то же имя, что и id. Это - одно и то же. В коде вы, кроме того, можете предоставить функцию вместо декларативно описанного шаблона, которая позволит вам динамически выводить каждый элемент по отдельности. Больше об этом позже.

Вы можете, кроме того, видеть, как эта страница назначает обработчик для событий itemInvoked (выше ready), вызывая WinJS.Navigation.navigate для того, чтобы перейти к страница groupDetail или itemDetail, как мы видели в лекции 3:

_itemInvoked: function (args) {
if (appView.value === appViewState.snapped) {
// Если страница в прикрепленном режиме, пользователь открывает группу. 
var group = Data.groups.getAt(args.detail.itemIndex); this.navigateToGroup(group.key);
} else {
// Если страница не в прикрепленном режиме, пользователь открывает элемент. 
var item = Data.items.getAt(args.detail.itemIndex); nav.navigate("/pages/itemDetail/itemDetail.html", {
item: Data.getItemReference(item) });
}
}

navigateToGroup: function (key) {
nav.navigate("/pages/groupDetail/groupDetail.html", { groupKey: key });
},

В таком случае, мы получаем данные элемента из коллекции (методы getAt) вместо того, чтобы использовать данные самого элемента. Это так, потому что необходимая информация о группе, необходимая для первого случая, не является, напрямую, частью элемента. Кроме того, мы видим здесь, что страницы по-разному интерпретируют активацию, в зависимости от состояния просмотра. Это так, потому что переход в новый режим просмотра меняет и макет, и источник данных. Это обрабатывается во внешнем методе страницы _initializeLayout, вызываемом и при старте приложения, и из функции страницы updateLayout:

initializeLayout: function (listView, viewState) {
if (viewState === appViewState.snapped) { listView.itemDataSource = Data.groups.dataSource; 
listView.groupDataSource = null;
listView.layout = new ui.ListLayout();
} else {
listView.itemDataSource = Data.items.dataSource;
listView.groupDataSource = Data.groups.dataSource;
listView.layout = new ui.GridLayout({ groupHeaderPosition: "top" });
}
},

Макет элемента управления ListView может быть изменен в любое время путём установки его свойства property. Когда программа находится в прикрепленном режиме просмотра, он установлен в WinJS.UI.ListLayout, в иных случаях - в WinJS.UI.GridLayout (свойство которого groupHeaderPosition может быть "top" или "left"). Кроме того, вы можете увидеть, что вы можете "на лету" поменять источник данных для ListView: в прикрепленном режиме просмотра это - список групп, в противном случае - список элементов.

Надеюсь, теперь вы понимаете, почему я как следует разъяснил особенности навигации по страницам, прежде чем мы добрались до ListView, так как этот проект, при ближайшем рассмотрении, выглядит довольно сложно. В любом случае, посмотрим сейчас на шаблоны для этой страницы (pages/groupedItems/groupedItems.html):

<div class="headertemplate" data-win-control="WinJS.Binding.Template">	
<button class="group-header win-type-x-large win-type-interactive"	
data-win-bind="groupKey: key" role="link" tabindex="-1" type="button"	
onclick="Application.navigator.pageControl.navigateToGroup(event.srcElement.groupKey)" >	
<span class="group-title win-type-ellipsis" data-win-bind="textContent: title"></span>
<span class="group-chevron"></span>	
</button>	
</div>	
<div class="itemtemplate" data-win-control="WinJS.Binding.Template">
<div class="item">
<img class="item-image" src="#" data-win-bind="src: backgroundImage; alt: title" />
<div class="item-overlay">
<h4 class="item-title" data-win-bind="textContent: title"></h4>
<h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6>
</div>
</div>
</div>

Опять же, мы так же используем WinJS.Binding.Template и различные части синтаксиса привязки данных, разбросанные по разметке, не говоря уже об обработчике click, присвоенном тексту заголовка, который, как и элемент в прикрепленном режиме просмотра, ведет нас к странице подробной информации о группе.

Как и в случае с данными (которые вы, вероятнее всего, замените), это задаётся в js/data.js, как массив, расположенный в памяти, который используется для WinJS.Binding.List. В массиве sampleItems каждый элемент заполнен либо внутренними данными, либо значениями других переменных. Каждый элемент, кроме того, имеет свойство group, которое берется из массива sampleGroups. К несчастью, этот последний массив имеет почти такие же свойства, как и массив элементов, что может показаться запутанным. Для того, чтобы помочь в чётком различении этих массивов, вот полная структура свойств элемента:

{
group : { 
key, 
title, 
subtitle,
backgroundImage,
description
}, title, 
subtitle, 
description, 
content,
backgroundImage
}

Как мы видели в примере группировки ListView ранее, проект на основе шаблона Приложение таблицы использует createGrouped для задания источника данных. Интересно увидеть здесь, что он задаёт изначально пустой список, создаёт сгруппированную проекцию (опуская необязательную функцию сортировки) и затем добавляет элементы, используя метод списка push:

var list = new WinJS.Binding.List();	
var groupedItems = list.createGrouped(	
function groupKeySelector(item) { return item.group.key; },
function groupDataSelector(item) { return item.group; }	
);	
generateSampleData().forEach(function (item) {
list.push(item);
});

Это чётко указывает на динамическую природу списков и ListView: вы можете добавлять и удалять элементы из источника данных, а односторонняя привязка данных позволит сохранять уверенность в том, что ListView соответствующим образом обновится. В подобном случае вам не нужно обновлять макет ListView - это произойдёт автоматически. Я говорю это, так как обычно имеется некоторое непонимание в вопросе использования метода ListView forceLayout, который вам нужно вызывать лишь тогда, как сказано в документации: "когда вы делаете ListView снова видимым после того, как его свойство style.display будет установлено в 'none'". Вы обнаружите, однако, что код из шаблона Приложение таблицы вовсе не использует этот метод.

В js/data.js имеются и другие полезные функции, такие, как getItemsFromGroup, которая использует WinJS.Binding.List.createFiltered, как мы делали ранее. Другие функции предназначены для организации взаимосвязи между группами и элементами, которая нужна для перемещения между списком элементов, подробностями о группах (подобная страница отображает лишь элементы в определенной группе), и подробностями об элементах. Все эти функции заключены в пространство имен, которое называется Data, в верхней части js/data.js, в итоге, ссылка на всё, что описано в этом файле, должна иметь префикс Data.

И, имея эти знания, я думаю, вы сможете понять всё, что происходит в приложении, построенном по шаблону Приложение таблицы, для того, чтобы адаптировать его под свои нужды. Просто помните о том, что все данные-образцы, вроде логотипа по умолчанию и изображения экрана-заставки, нужно полностью заменить реальными данными, полученными из других ресурсов, наподобие файла или WinJS.xhr, и их вы можете заключить в WinJS.Binding.List. Некоторые дальнейшие руководства вы можете найти в материале "Создание программы для чтения блогов" (http://msdn.microsoft.com/library/windows/apps/Hh974582.aspx) в Центре разработчиков Windows, и хотя в руководстве используется шаблон проекта Приложение с разделением (Split App), здесь много общего с шаблоном Приложение таблицы, и этот рассказ, на самом деле, применим и к тому и к другому шаблонам.

Элемент управления Semantic Zoom (семантическое масштабирование)

С тех пор, как мы загрузили пример "Группировка HTML ListView и семантическое масштабирование" (http://code.msdn.microsoft.com/windowsapps/ListView-grouping-and-6d032cc1), и завершили первый разговор об элементах управления для коллекций, сейчас подходящее время для того, чтобы рассмотреть еще один очень интересный элемент управления WinJS: SemanticZoom (http://msdn.microsoft.com/library/windows/apps/br229690.aspx).

Семантическое масштабирование позволяет пользователям легко переключаться между двумя режимами просмотра одних и тех же данных, это режим детализированного представления (zoomed-in), который отображает детали, и режим общего представления (zoomed-out), который предоставляет более общую информацию. Основная область использования семантического масштабирования - это длинные списки элементов (особенно - несгруппированных), где пользователю, скорее всего, надоест постоянно прокручивать контент из конца в конец, не взирая даже на то, что довольно забавно водить пальцем по сенсорному экрану. С помощью семантического зуммирования вы можете уменьшить масштаб для того, чтобы увидеть заголовки, категории, или другие формы краткого представления общей информации о данных, и затем, кликнув по одному из отображаемых элементов, вернуться в его раздел или группу. Руководство по дизайну рекомендует поддерживать режим общего представления данных, в котором они занимают от одного до трёх экранов максимум, что делает очень простым просмотр всего набора данных и понимание их особенностей.

Попробуем в деле семантическое масштабирование из Сценария 2 примера, посвященного группировке HTML ListView и семантическому масштабированию. Для переключения между режимами просмотра, используйте жесты сведения и разведения пальцев (pinch-zoom), сочетания клавиш Ctrl+/Ctrl-, нажатие клавиши Ctrl, сопровождающееся прокруткой колеса мыши, либо маленькие кнопки масштабирования, которые автоматически появляются в правом нижнем углу элемента управления, как показано на Рис. 5.5. Когда вы переходите к режиму общего представления (zoom-out), вы видите отображение заголовков групп, что так же показано на рисунке.

(рис 5.5) Семантическое масштабирование между двумя режимами просмотра в примере о группировке ListView и применении семантического масштабирования

Режим детализированного представления (Zoomed in view)

Переход в режим общего представления (Zoom out)

Элемент управления масштабированием (наложение)(Zoom control (overlay))

Режим общего представления (Zoomed out view)

Прикоснитесь к элементу или выполните команду увеличения масштаба, когда фокус установлен на данном элементе (Tap an item or zoom in with focus on that item)

Элемент управления довольно просто и понятно использовать. Объявите в разметке элемент управления WinJS, используя конструктор WinJS.UI.SemanticZoom. Внутри элемента вы, затем, объявите два (и только два) дочерних элемента: первый определяет режим детализированного представления, второй - режим общего представления - всегда в таком порядке. Здесь показан пример работы с двумя элементами управления ListView (плюс - шаблон, используемый для режима общего представления; Я показал код в измененном примере, который включен в дополнительные материалы к курсу):

<div id="semanticZoomTemplate" data-win-control="WinJS.Binding.Template" >	
<div class="semanticZoomItem">	
<h2 class="semanticZoomItem-Text" data-win-bind="innerText: groupTitle"></h2>
</div>	
</div>	

<div id="semanticZoomDiv" data-win-control="WinJS.UI.SemanticZoom">
<div id="zoomedInListView" data-win-control="WinJS.UI.ListView"
data-win-options="{ itemDataSource: myGroupedList.dataSource, itemTemplate: mediumListIconTextTemplate,
groupDataSource: myGroupedList.groups.dataSource, groupHeaderTemplate: headerTemplate,
selectionMode: 'none', tapBehavior: 'none', swipeBehavior: 'none' }">
</div>

<div id="zoomedOutListView" data-win-control="WinJS.UI.ListView"
data-win-options="{ itemDataSource: myGroupedList.groups.dataSource, itemTemplate: semanticZoomTemplate,
selectionMode: 'none', tapBehavior: 'invoke', swipeBehavior: 'none' }" >
</div>
</div>

Первый дочерний элемент zoomedInListView, очень похож на ListView из Сценария 1, с заголовками групп и элеметами. Второй элемент zoomedOutListView, использует группы в роли элементов и выводит их с использованием другого шаблона. Элемент управления семантического масштабирования просто перключается между двумя режимами отображения в ответ на соответствующие жесты. Когда масштаб отображения меняется, элемент управления SemanticZoom вызывает событие zoomchanged, у которого значение args.detail равняется true в случае общего представления, и false при детализированном представлении. Вы можете использовать это событие для того, чтобы сделать, для различных режимов отображения, доступными определенными команды панели приложения, например - в режиме общего представления активировать команды для изменения сортировки или фильтрации, результат работы который затем подействует на режим детализированного представления. Панели приложения (app bar) мы рассмотрим в лекции 1 курса "Пользовательский интерфейс приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript".

Элемент управления имеет некоторые другие свойства, такие, как enableButtonl (логическое значение, управляющее видимостью накладываемых кнопок; значение по умолчанию - true), locked (логическое значение, отключающее изменение масштаба в обоих направлениях, может быть установлено динамически - для блогировки текущего режима отображения; по умолчанию - false), и zoomedOut (логическое, показывает, находится ли элемент в режиме общего представления, таким образом, вы можете инициализировать его подобным способом; по умолчанию - false). У него есть метод forceLayout, который используется в том же случае, что и метод forceLayout элемента управления ListView: а именно, когда вы отключаете стиль display: none.

Свойство zoomFactor весьма интересно, оно определяет, какая анимация используется при переходе между двумя режимами просмотра. Анимация - это комбинация масштабирования и затухания, которые делают переход в режим общего представления выглядящим как опускание вглубь плоскости экрана или подъем оттуда, в завиимости направления переключения, в то время как переход в режим детального представления может выглядеть, как "утопание" или "поднятие". Если быть более точным, то в режим детализированного просмотра масштабирование производится между 1/zoomFactor и 1, в то время, как значение прозрачности лежит между 0 и 1. Значение zoomFactor по умолчанию - 0.65, что создаёт эффект умеренной силы. Более низкие значения (минимум - 0.2) усиливают эффект, более высокие (максимум - 0.8) ослабляют его.

Когда особенности стилизации понятны, вы можете сделать большинство из того, что вам нужно, прямо с дочерними элементами SemanticZoom. Тем не менее, стилизуя элемент управления SemanticZoom вы можете переопределить стили в win-semanticzoom (для всего элемента) и win-semanticzoomactive (для активного режима просмотра). Стиль win-semanticzoombutton позволяет вам, если нужно, стилизовать кнопки для управления масштабом.

Важно понимать, что SemanticZoom предназначен для переключения между двумя режимами отображения одних и тех же данных, а не для переключения между полностью различными наборами данных (смотрите "Руководство по контекстному масштабированию" (http://msdn.microsoft.com/ru-ru/library/windows/apps/hh465319.aspx)). Кроме того, элемент управления не поддерживает вложенность (то есть - многократное изменение масштаба для разных уровней данных). Однако, это не означает, что вы вынуждены использовать элементы управления одного вида и для одного, и для другого режимов просмотра: режим детального просмотра может быть списком, а режим общего просмотра - диаграммой, календарём, или чем угодно другим. Режим общего просмотра, другими словами, это отличное место для показа сводных данных, которые сложно получить в режиме детального просмотра. Например, используя те же модификации кода, которые мы использовали для включения количества элементов в данные групп для Сценария 1 (смотрите "Быстрый старт №2b, выше), нам достаточно лишь внести некоторые дополнения в шаблон элемента для режима общего просмотра (как сделано в модифицированном примере к этой лекции в дополнительных материалах к курсу):

Кроме того, вам нужно знать, что элемент управления ZemanticZoom не работает с любыми дочерними элементами. Об этом вам может сообщить исключение, вызванное, если элемент не имеет свойства zoomableView. Каждый дочерний элемент должен обеспечивать реализацию интерфейса WinJS.UI.IZoomableView (http://msdn.microsoft.com/library/windows/apps/br229794.aspx) посредством свойства zoomableView. Среди всех встроенных элементов управления HTML и WinJS этому требованию удовлетворяет лишь ListView, и именно поэтому его обычно используют с ZoomView. Однако, вы можете обеспечить данный интерфейс в пользовательском элементе управления, когда объект, возвращаемый конструктором, содержит свойство zoomableView, которое является объектом, содержащим методы интерфейса. Среди этих методов задачи beginZoom (начало масштабирования) и endZoom (завершение масштабирования) вполне ясны, методы getCurrentItem (получить текущий элемент) и setCurrentItem (установить текущий элемент) позволяют элементу управления выполнять масштабирование для правильной группы, когда пользователь коснётся её в режиме общего просмотра.

Подробности вы можете найти в примере "HTML SemanticZoom для пользовательских элементов управления" (http://code.msdn.microsoft.com/windowsapps/SemanticZoom-for-custom-4749edab), который, кроме того, предоставляет другие образцы использования пользовательских элементов управления.

Особенности FlipView и его стилизация

Нам не стоит забывать о скромном элементе управления FlipView, рассматривая ListView - один из самых сложных и богатых возможностями элементов управления WinJS. Поэтому, прежде чем мы продолжим углубляться в его особенности, посвятим несколько страниц описанию FlipView, через рассмотрение сценариев его использования, показанных в примере "Элемент управления FlipView" (http://code.msdn.microsoft.com/windowsapps/FlipView-control-sample-18e434b4). Так же важно отметить, что хотя этот пример показыват возможности элемента управления в сравнительно малой области, FlipView может иметь любой физический размер, даже занимать большую часть экрана. Обычное использование этого элемента управления, на самом деле, позволяет пользователю просматривать полноразмерные изображения в фотогалерее, переходя между ними путём пролистывания. Конечно, FlipView можно использовать везде, где он может понадобиться, в большом или малом размере. Смотрите материал "Руководство по элементу управления FlipView" (http://msdn.microsoft.com/library/windows/apps/hh850405) для того, чтобы узнать, как им лучше пользоваться.

Во всяком случае, Сценарий 2 в примере ("Ориентация и расстояние между элементами") демонстрирует свойство orientation этого элемента управления. Оно определяет местоположение элементов управления со стрелкой: слева и справа (horizontal), или сверху и снизу (vertical), как показано ниже. Кроме того, оно определяет начальные и конечные анимации элемента, и то, использует ли элемент управления клавиши вправо/влево или вверх/вниз для навигации с помощью клавиатуры. Данный сценарий так же позволит вам установить свойство itemspacing, которое задаёт размер свободного места между элементами, когда вы перематываете их, используя сенсорные жесты (ниже справа) Эти эффекты невидны при использовании клавиатуры или мыши для тех же целей. Для того, чтобы их увидеть, вам понадобится использовать эмуляцию сенсорного дисплея в имитаторе Visual Studio для перемещения между элементами.

Сценарий 3 ("Использование интерактивного содержимого") показывает использование функции шаблона (template function) вместо декларативно описанного шаблона. Больше об этом мы поговорим в разделе "Как, на самом деле, работают шаблоны" ниже в этой лекции, но, говоря простым языком, функция шаблона или визуализатор (renderer) создаёт элементы и устанавливает их свойства программно, делая то, что обычно выполняет WinJS.Binding.Template на основе разметки шаблона, которую вы ему предоставляете. Это позволяет вам выводить элементы по-разному (то есть, создавать разные элементы классов настройки стилей) в зависимости от их реальных данных. В Сценарии 3, источник данных содержит "оглавление" элементов в начале, для которого визуализатор (функция, которая называется myTemplate в js/interactiveContent.js) создаёт полностью различные элементы:

Сценарий так же задаёт прослушиватель события click в записях "оглавления", обработчик которого переходит на соответствующий элемент, уставливая свойство currentPage элемента управления FlipView. Элемент-изображение имеет обратную ссылку на "оглавление". Посмотрите функцию clickHandler в коде для того, чтобы увидеть оба этих действия.

Сценарий 4 ("Создание контекстного элемента управления") показывает добавление навигационных элементов управления путём их наложения на каждый из элементов:

Элементы выводятся с использованием декларативного шаблона, который в данном случае содержит элемент-заполнитель для навигационых элементов управления (html/contextControl.html), div, который называется ContextContainer.

<div>	
<div id="contextControl_FlipView" class="flipView" data-win-control="WinJS.UI.FlipView"
data-win-options="{ itemDataSource: DefaultData.bindingList.dataSource,	
itemTemplate: contextControl_ItemTemplate }">	
</div>	
<div id="ContextContainer"></div>	
</div>

Когда элемент управления инициализируется в методе processed в js/contextControl.js, в примере вызывается асинхронный метод FlipView count. Обработчик завершения, countRetrieved, затем создаёт навигационные элементы управления, используя строку стилизованных переключателей. Обработчик onpropertychange для каждого переключателя, затем, устанавливает свойство currentPage FlipView.

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

Сценарий 5 ("Стилизация навигационных кнопок") показывает особенности стилизации FlipView, включающую в себя различные win-* стили и псевдо-классы, как показано здесь:

Используется для границ, отбивок и так далее (Use for margins, padding, etc.)

Для фона, если расстояние между элементами больше 0, задаёт стиль конкретного элемента управления (For background with item spacing > 0, style the specific control).

Если вы, в шаблоне, используете собственные навигационные кнопки (подключённые к методам next и previous), скройте кнопки по умолчанию, добавив display.none к правилу стиля <control selector> .win-navbutton.

И, наконец, есть еще несколько методов и событий элемента управления FlipView, которые не используются в примере:

  • pageCompleted - это событие, которое вызывается, когда переход к новому элементу полностью завершен (то есть, новый элемент визуализирован). В противоположность этому, вышеупомянутое событие pageselected вызывается, когда анимируется элемент-заполнитель (placeholder) (не полностью визуализированный). Смотрите "Функции шаблонов (Часть 2)" в конце этой лекции.
  • datasourcecountchanged - это событие, вызываемое по вполне понятной причине (изменение количества элементов в источнике данных), что-то похожее Сценарий 4 использует для обновления элементов управления навигации, если элементы добавляют в источник данных или убирают из него.
  • next и previous - это методы для перемещения между элементами (наподобие currentPage), которые полезны, если вы реализуете собственные навигационные кнопки.
  • forceLayout - это метод, который вызывается, когда вы делаете элемент управления FlipView, убирая стиль display: none. (В примере этот метод вызывается, когда вы переключаетесь между сценариями, однако, в этом нет необходимости, так как здесь никогда не меняется стиль).
  • setCustomAnimations позволяет вам контролировать анимации, используемые при пролистывании вперед, назад, и при переходе на произвольный элемент.
  • Для того, чтобы узнать об этом подробности, обратитесь к документации по WinJS.UI.FlipView (http://msdn.microsoft.com/library/windows/apps/br211711.aspx).

    Источники данных

    Во всех случаях, которые мы рассмотрели, мы использовали источники данных, построенные на основе WinJS.Binding.List и находящиеся в памяти. Очевидно, существуют и другие типы источников данных, и их не обязательно сразу же загружать в память. Как же работать с такими источниками?

    WinJS предоставляет некоторую помощь в этой области. Первое средство - это объект WinJS.UI.StorageDataSource, который работает с файлами в файловой системе, как в следующем разделе, в демонстрации работы FlipView с библиотекой изображений. Следующее средство - это WinJS.UI.VirtualizedDataSource, которое подразумевает использование базового класса для пользовательских источников данных, это расширенный сценарий, которого мы коснёмся лишь кратко.

    FlipView и библиотека изображений

    Всё, что мы видели в примере, касающемся FlipView, ведет нас к совершенно очевидной возможности: перемещаться по файлам изображений в папке. Используя то, что мы узнали, как нам реализовать это? У нас уже есть шаблон элемента, который содержит тег img, таким образом, возможно, нам лишь нужны некоторые URI для таких файлов. Возможно, нам следует создать их массив, используя API наподобие Windows.Storage.KnownFolders.picturesLibrary.getFilesAsync (конечно, объявив возможность Библиотека изображений (Pictures Library) в манифесте). Это даст нам множество объектов StrorageFile, для которых мы можем вызвать URL.createObjectURL. Найденные URI мы можем сохранить в массиве и затем поместить их в WinJS.Binding.List:

    var myFlipView = document.getElementById("pictures_FlipView").winControl;
    
    Windows.Storage.KnownFolders.picturesLibrary.getFilesAsync()
    .done(function (files) {
    var pixURLs = [];
    
    files.forEach(function (item) {
    var url = URL.createObjectURL(item, {oneTimeOnly: true });
    pixURLs.push({type: "item", title: item.name, picture: url });
    });
    
    var pixList = new WinJS.Binding.List(pixURLs);
    myFlipView.itemDataSource = pixList.dataSource;
    });

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

    Лучший подход заключается в использовании WinJS.UI.StorageDataSource (http://msdn.microsoft.com/library/windows/apps/br212650.aspx), который работает напрямую с файловой системой, вместо того, чтобы работать с массивом, расположенным в памяти. Я реализовал это в виде Сценария 8 в измененном примере использования FlipView в дополнительных материалах к этой лекции (Другие материалы можно найти в примере "Использование StorageDataSource и GetVirtualizedFilesVector" (http://code.msdn.microsoft.com/windowsapps/Data-source-adapter-sample-3d32e535)). Здесь мы можем использовать короткое имя для того, чтобы получить источник данных из библиотеки изображений:

    myFlipView.itemDataSource = new WinJS.UI.StorageDataSource("Pictures");

    "Pictures" это короткое имя, так как первый аргумент StorageSource, это файловый запрос, который исходит из API Windows.Storage.Search, подробнее об этом мы поговорим в лекции 2 курса "Пользовательский интерфейс приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript". Эти запросы, которые попадают в мощную функцию Windows.Storage.StorageFolder.createFileQueryWithOptions (http://msdn.microsoft.com/library/windows/apps/br211591.aspx) - это способы для перебора файлов в папках вместе с метаданными - такими, как обложки альбомов, подробности о записях, и эскизы, которые кадрированы так, чтобы сохранить соотношение сторон. Короткие имена, наподобие"Pictures" (а так же "Music", "Documents", "Videos" которые нуждаются в соответствующих возможностях, объявленных в манифесте) лишь создают типичные запросы для подобных библиотек документов.

    Нужно отметить, что StorageDataSource не поддерживает напрямую одностороннюю привязку данных, поэтому вы получите исключение, если вы попытаетесь сослаться на элемент напрямую в шаблоне. Чтобы это обойти, нужно явно использовать функцию-инициализатор WinJS.Binding.oneTime для каждого свойства:

    <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>

    В случае со свойством img.src, файловый запрос возвращает нам элементы типа Windows.Storage.BulkAccess.FileInformation (http://msdn.microsoft.com/library/windows/apps/windows.storage.bulkaccess.fileinformation.aspx) (переменная source в коде ниже), которая содержит эскизы изображений, а не URI. Для того чтобы конвертировать эти данные об изображениях в URI, нам нужно использовать свой собственный инициализатор привязки:

    WinJS.Namespace.define("InitFunctions", {	
    thumbURL: WinJS.Binding.initializer(function (source, sourceProp, dest, destProp) {
    if (source.thumbnail) {	
    dest.src = URL.createObjectURL(source.thumbnail, { oneTimeOnly: true });	
    }	
    })	
    });

    В этом инициализаторе часть data-win-bind src : thumbnail, на самом деле, игнорируется, так как мы устанавливаем свойство изображения src напрямую в значение source.thumbnail. Это - лишь форма односторонней привязки данных.

    Обратите внимание на то, что эскизы не всегда сразу доступны в объекте FileInformation, именно поэтому мы проверяем, действительно ли у нас имеется эскиз, прежде чем создаём URI для него. Это означает, что быстрое перелистывание изображений может показать пользователю пустые места для изображений. Для того, чтобы справиться с этим, мы можем прослушивать событие FileInformation.onthumbnailupdated и обновлять элементы в это время. Лучший способ добиться этого заключается в использовании вспомогательного метода StorageDataSource.loadThumbnail (лекции 3).

    Вы можете использовать этот метод внутри инициализатора привязки, как показано в Сценарии 1 вышеупомянутого примера "Использование StorageDataSource и GetVirtualizedFilesVector" (http://code.msdn.microsoft.com/windowsapps/Data-source-adapter-sample-3d32e535), или внутри функции рендеринга, которая имеется в декларативном шаблоне. Мы сделаем это для нашего примера использования FlipView позже, в разделе "Как, на самом деле, работают шаблоны", что, кроме того, позволит нам избежать трюков с односторонней привязкой данных.

    В качестве последнего замечания, Сценарий 6 в примере работы с FlipView, содержит некоторые образцы использования различных источников данных, особенно те, которые работают со службой поиска Bing. Поэтому, взглянем на пользовательские источники данных.

    Пользовательские источники данных

    Теперь, когда мы видели элемент управления для коллекций, наподобие FlipView, который работал с двумя разными источниками данных, вы, вероятно, начинаете догадываться о том, что все источники данных имеют некоторые общие характеристики и общий программный интерфейс. Это снова продемонстрировано в Сценарии 6 примера использования FlipView, так же, как и в примере "Работа HTML ListView с источниками данных" (http://code.msdn.microsoft.com/windowsapps/ListView-custom-data-4dcfb128), показанного на Рис. 5.6, который мы рассмотрим в этом разделе.

    (рис 5.6) Пример работы HTML ListView с источниками данных

    Сценарии 2 и 3 этого примера работают на основе источника данных WinJS.Binding.List, как мы уже видели, и предоставляют кнопки для управления этим источником данных. Эти изменения отражаются на выходных данных. Разница между двумя сценариями заключается в том, что Сценарий 2 манипулирует данными посредством методов WinJS.Binding.List наподобие move, в то время как Сценарий 3 манипулирует источником данных, на котором он основан, посредством более общего API ListDataSource (http://msdn.microsoft.com/library/windows/apps/br211786.aspx).

    Из-за привязки данных, изменения в данных отражаются на элементе управления ListView в любом случае, но есть три важных различия. Первое, интерфейс ListDataSource - обычный для всех источников данных, в итоге любой код, написанный с его использованием, будет работать для любого источника данных. Второе - его методы обычно асинхронны, так как источник данных может быть подключён к онлайновому сервису или другому подобному ресурсу. Третье - ListDataSource обеспечивает вызов beginEdits для пакетного изменения данных, что подавляет любые сообщения об изменениях, направленные внешним присоединённым объектам, до вызова endEdits. Это позволяет вам выполнять изменение больших объемов данных способом, который может улучшить производительность ListView.

    Сценарии 1 и 4 в примере показывают, как можно создать пользовательский источник данных. Сценарий 1 создаёт источник данных для поиска Bing. Сценарий 4 создаёт один источник в виде массива в памяти, который вы можете приспособить для работы с некоторыми полученными извне данными, которые поступают от некоего сервиса небольшими порциями. Важно здесь то, что при всех этих подходах реализуется то, что называется адаптером обработки данных (data adapter), который является объектом с методами интерфейса WinJS.UI.IListDataAdapter (http://msdn.microsoft.com/library/windows/apps/br212603.aspx). Это обеспечивает такие возможности, как кэширование, виртуализация, обнаружение изменений и так далее. К счастью, вы получаете большинство из этих методов, наследуя ваш класс от WinJS.UI.VirtualizedDataSource (http://msdn.microsoft.com/library/windows/apps/hh701413.aspx) и затем реализуя те методы, которые вам нужно настроить. В примере, например, bindingImageDataSource определен так, как показано ниже (смотрите js/BingImageSearchDataSource.js):

    bingImageSearchDataSource = WinJS.Class.derive(WinJS.UI.VirtualizedDataSource, function (devkey, query) {
    this._baseDataSourceConstructor(new bingImageSearchDataAdapter(devkey, query));
    });

    Где класс bingImageSearchDataAdapter реализует напрямую лишь методы getCount и itemsFromIndex.

    Для получения более глубоких знаний по данному вопросу, выходящих за пределы этого примера, я отсылаю вас к сессии конференции Build 2011 года: "APP210-T: Построение коллекций, управляемых данными и приложений со списками с использованием ListView в HTML5" (http://channel9.msdn.com/Events/BUILD/BUILD2011/APP-210T). Кое-что с тех пор изменилось (так, ArrayDataSource теперь WinJS.Binding.List), но в целом здесь хорошо разъяснены механизмы. Кроме того, полезно помнить, что вы так же можете использовать другие языки, наподобие C# или C++ для того, чтобы описать пользовательский источник данных. Подобные языки могут предложить более высокую производительность внутри источника данных и позволят получить доступ к более производительным API, нежели те, к которым даёт доступ JavaScript.

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

    Ранее, когда мы рассматривали шаблон Приложение таблицы, я упоминал, что вы можете использовать функцию вместо декларативного описания шаблона для свойств наподобие 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. Эта функция должна быть отмечена для обработки, как обсуждалось в лекции 4, так как она участвует в 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-объекты!" в конце этой лекции.

    Особенности и стилизация ListView

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

    Когда ListView - это неправильный выбор?

    ListView - это один из богатейших по возможностей элементов управления во всей Windows. Он очень мощный, гибкий, и, как мы уже знаем, весьма глубокий и сложный. Но по всем этим причинам, иногда, он является не лучшим выбором! В зависимости от дизайна, может быть проще использовать обычный HTML/CSS макет

    Концептуально ListView определяется путём задания взаимоотношений трех частей: источника данных, шаблонов и макета. То есть, объекты из источника данных, которые могут быть сгруппированы, отсортированы и отфильтрованы, выводятся с использованеим шаблонов и организуются с помощью макета (обычно - с помощью групп и заголовков групп). В подобном определении, ListView предназначен для того, чтобы помочь в визуализации коллекций похожих и/или взаимосвязанных объектов, где группировка тоже подразумевает взаимоотношения определенного рода.

    Имея это в виду, можно признать следующие факторы убедительно свидетельствующими о том, что ListView - это хороший выбор для вывода некоторой коллекции:

  • Коллекция может содержать различное чисто объектов для вывода, возможно, очень большое количество, которых больше, чем приложение, запущенное на большом экране, способно отобразить в одном окне.
  • Имеет значение организация и реорганизация элементов в различных группах.
  • Заголовки групп поомогают уточнить общие свойства элементов в группах, и их можно использовать для навигации к странице детальной информации о группе.
  • Имеет значение сортировка или/и фильтрация элементов в соответствии с различными условиями.
  • Различная группировка элементов и информации об этих группах предлагает способы работы, в которых опыт взаимодействия пользователя и программы способно улучшить семантическое масштабирование.
  • Группы имеют некоторые общие черты, в том смысле, что каждая из них связана с похожими объектами. Различные имена мест, например, сходны; лента новостей, список друзей и календарь праздников не схожи.
  • Элементы поддерживают индивидуальное или групповое выделение, таким образом, что команды на панели приложения позволяют выполнять с ними какие-то действия.
  • С другой стороны, противоположные факторы говорят о том, что ListView не является правильным выбором:

  • Коллекция содержит ограниченное или фиксированное количество элементов, или это совсем не коллекция взаимосвязанных элементов.
  • Неважна возможность реорганизации группировки объектов, фильтрация или сортировка элементов.
  • Вам не нужны заголовки групп.
  • Вы не нуждаетесь в применении семантического масштабирования.
  • Группы весьма различны - то есть, группам не имеет смысла находиться бок о бок при отсутствии заголовков.
  • Позвольте мне пояснить, что я не говорю о дизайне - ваш дизайнер может передать вам любой макет, который он хочет, и, как разработчик, именно вы должны решить, как реализовать его! Я говорю о том, как вы выбираете подход для подобной реализации, с использованием ли элементов управления наподобие ListView, или с применением обычного HTML/CSS макета.

    Я говорю об этом, так как в работе с разработчиками, которые создают самые первые приложения для Магазина Windows, мы часто видим, как они пытаются использовать ListView в ситуации, где он просто не нужен. Домашняя страница приложения, например, может содержать комбинацию из лент новостей, списков друзей и календаря. Страница детальной информации об элементе может включать в себя изображение, текстовое описание и медиа-библиотеку. В обоих случаях, страница содержит ограниченное количество разделов и разделы содержат очень разное содержимое, о котором можно сказать, что оно не имеет межгрупповых связей. Поэтому, использование ListView более сложно, чем простое использование одного div, поддерживающего прокрутку с CSS-сеткой, в которой вы можете разместить любые разделы, которые вам нужны.

    Внутри данных разделов, конечно, вы можете использовать элементы управления ListView для отображения коллекций элементов, но если говорить обо всей странице, то обычный макет, основанный на div - это всё, что вам нужно. Я показал подобный выбор на Рис. 5.7, используя изображение из материала "Проектирование навигации для приложений Магазина Windows" (http://msdn.microsoft.com/library/windows/apps/hh761500), так как вы, возможно, получите похожие изображения от вашего дизайнера. За исключением навигационных стрелок, хаб приложения и страницы детальной информации обычно используют div в качестве корневого элемента, в то время как страница раздела обычно представляет собой ListView. Внутри корневого узла приложения и страниц детальной информации могут быть элементы управления ListView, но там, где нужно отобразить фиксированное содержимое (вроде отдельного элемента), лучше подойдёт div.

    (рис 5.7) Разбиение типичного дизайна Хаб-Раздел-Сведения (Hub-Section-Details), спроектированного с применением элементов div и элементов управления ListView

    Корневой раздел приложения (хаб): страница - это div; разделы в сетке - это либо div'ы, либо элементы управления ListView; если первый раздел содержит различные элементы, ей следует содержать ListView (Hub page: page is a div; sections in a grid are either divs or ListViews; if the first section has variable items, it could be a ListView).

    Элемент div с макетом (div with layout)

    Страница раздела: тело страницы (исключая заголовок) - это один ListView (Section page: the body of the page (excluding a page header) is a single ListView)

    Страница детальной информации (сведений): страница - это div; разделы в сетке - либо элементы div, либо - элементы управления ListView (Detail page: page is a div; sections in a grid are either divs or ListViews)

    Подсказывает о том, что вы идете по неверному пути, кстати, тот факт, что вы пытаетесь скомбинировать несколько коллекций несвязанных данных в единый источник данных, привязывая этот источник к ListView и реализуя функцию рендеринга для того, чтобы снова разделить эти данные, чтобы всё вывелось верно. Всей этой дополнительной работы можно избежать, просто используя макет HTML/CSS.

    Для того, чтобы узнать больше о проектировании ListView, обратитесь к материалу: "Руководство и контрольный список для элементов управления ListView" (http://msdn.microsoft.com/library/windows/apps/hh465465.aspx).

    Параметры, выделения и методы отдельного элемента

    В предыдущем разделе мы уже видели некоторые параметры, которые вы можете использовать при создании ListView, параметры, которые связаны со свойствами элемента управления и доступны, кроме того, из JavaScript. Посмотрим на полный список свойств, методов и событий, которые я организовал в несколько групп, в конце концов, эти свойства и методы превратились во что-то вроде коллекции! Подробности о них вы можете найти на странице WinJS.UI.ListView (http://msdn.microsoft.com/library/windows/apps/br211837.aspx), самое важное сейчас - понять как взаимосвязаны члены данных групп:

  • Адресация элементов. Свойство currentItem позволяет получить или задать элемент, имеющий фокус, а методы elementFromIndex и indexOfElement позволяют установить взаимосвязь между элементами DOM и их индексами. Последний может быть полезен, если у вас есть другие элементы управления в шаблоне элемента, и вам нужно определить окружающий элемент в обработчике события.
  • Видимость элементов. Свойства indexOfFirstVisible и indexOfLastVisible позволяют узнать индексы видимых элементов, или они могут быть использованы для прокрутки ListView к заданному элементу. Метод ensureVisible приводит к отображению заданного элемента, если он загружен. Здесь имеется свойство scrollPosition, которое содержит дистанцию в пикселях между первым элементом в списке и текущей отображаемой областью. Последством этого свойства вы можете задать позицию прокрутки ListView, это работает только в том случае, если значение loadingState (смотрите группу "Состояние загрузки" ниже") установлено в ready, в противном случае ListView может еще не знать своих истинных размеров. Рекомендовано, вместо этого, использовать ensureVisible или indexOfFirstVisible для управления позицией прокрутки.
  • Активизация элементов. Событие itemInvoked, как мы уже видели, вызывается, когда пользователь касается элемента, если только свойство tapBehavior не установлено в none - в подобном случае активации не происходит. Другое значение tapBehavior из перечисления WinJS.UI.tapBehavior (http://msdn.microsoft.com/library/windows/apps/hh701303.aspx) всегда приводит к вызову данного события, но определяет, как касание влияет на выделение. Заметьте, что вы можете переопределить поведение при выделении для каждого элемента в отдельности, используя событие selectionchanging и подавляя анимацию, если это нужно. Смотрите врезку "Поведение при щелчке и прикосновении" ниже.
  • Выделение элементов. Свойство selectionMode содержит значение из перечисления WinJS.UI.selectionMode (http://msdn.microsoft.com/library/windows/apps/br229687.aspx), показывающее режим выделения - одиночный, множественный или запрещающее выделение. Во всех случаях свойство selection содержит объект ListViewItems (http://msdn.microsoft.com/library/windows/apps/br211809.aspx), методы которого позволяют вам получать список выделенных элементов и управлять ими (например, настраивать выделенные элементы, используя их метод set). Изменения в выделении вызывают события selectionchanging и selectionchanged. В случае с selectionchanging, его свойство args.detail.newSelection содержит вновь выделенные элементы. Больше об этом можно узнать в примере "Настройка интерактивного поведения HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-selection-detail-95e06ade).
  • Прокрутка. Свойство swipeBehavior, связанное с выделением элемента, содержит значение из перечисления WinJS.UI.SwipeBehavior (http://msdn.microsoft.com/library/windows/apps/hh701287.aspx). "Прокрутка" (swiping) или "скольжение по диагонали" (cross-slide) это сенсорный жест на элементе для его выделения, когда жест производится перпендикулярно направлению прокрутки списка. Если данное свойство установлено в none, прокрутка не воздействует на элемент и жест маршрутизируется к родительским элементам, позволяя осуществлять прокрутку вертикально ориентированного ListView или страницы, на которой он расположен. Если это свойство установлено в select, жест обрабатывается элеменом и приводит к его выделению.
  • Источники данных и шаблоны. Мы уже видели свойства groupDataSource, groupHeaderTemplate, itemDataSource, и itemTemplate. Два связанных свойства - это resetGroupHeader и resetItem, которые содержат функции, которые ListView может вызывать при повторном использовании элементов. Это разъясняется в разделе "Функции шаблонов (Часть 2)"
  • Макет. Как мы уже видели, свойство layout (и объект) описывают организацию элементов в ListView, о чём больше сказано в разделе "Макеты и объединение ячеек" ниже. Мы, кроме того, рассматривали функцию forceLayout, которая используется, когда из ListView удаляется стиль display: none и элемент управления нуждается в повторной визуализации.
  • Поведение при загрузке. Как будет обяснено в разделе "Оптимизация производительности ListView" ниже, эта группа определяет, как ListView загружает страницы элементов (то, почему ensureVisible не всегда работает, если страница не загружена). Когда свойство loadingBehavior установлено в "randomaccess" (по умолчанию), полоса прокрутки ListView отражает общее число элементов только на пяти полных страницах элементов (максимум - 1000), хранящихся в памяти в любое время, когда пользователь перемещается по содержимому. (Пять страниц - это текущая страница и по две буферизованных страницы позади текущей и перед ней). Другое значение, "incremental", подразумевает изначальную загрузку некоторого количества страниц и затем, загрузку дополнительных страниц, когда пользователь прокрутит список до конца (сохраняя, после этого, все страницы в памяти). Инкрементная загрузка работает со свойствами automaticallyLoadPages, pagesToLoad, и pagesToLoadThreshold, вместе с методом loadMorePages, как мы увидим далее.
  • Состояние загрузки. Свойство только для чтения loadingState содержит либо "itemsLoading" (список запрашивает элементы и заголовки из источника данных), либо "viewportLoaded" (все элементы и заголовки, которые видимы, были загружены), либо "itemsLoaded" (все оставшиеся невидимые буферизованные элементы загружены), или "complete" (все элементы загружены, содержимое в шаблонах выведено и анимация завершена). Когда это свойство изменяется, что обычно происходит, когда ListView нуждается в обновлении макета при прокрутке, вызывается событие loadingStateChange.
  • Разное. Стандартные методы DOM addEventListener, removeEventListener, и dispatchEvent служат для обработки и вызова событий. Они могут быть использованы с любыми событиями, которые поддерживает ListView, в том числе contentanimating, которое возникает при исполнении элементом управления анимации при появлении или смене элементов, что позволяет вам либо предотвращать, либо задерживать подобные анимации. Свойство zoomableView содержит реализацию IZoomableView, которая нужна для контекстного масштабирования (приложения никогда не манипулируют данным свойством).
  • Врезка: Поведение при щелчке и прикосновении

    Когда вы прикасаетесь к элементу в ListView или щёлкаете по нему мышью, при том, что свойство tapBehavior установлено во что-то кроме none, происходит небольшая анимация, путём примерно 97% масштабирования для подтверждения данного действия. Если в списке есть некоторые элементы, которые не могут быть активированы (такие, как некоторые группы или те, которые вы показываете как деактивированные, так как данные, лежащие в их основе, пока не доступны), они продолжают показывать эту анимацию, так как установка tapBehavior применяется к элементу управления в целом. Для того чтобы отключить анимацию для любого конкретного объекта, вы можете добавить класс win-interactive к его элементу внутри функции рендеринга, что является способом указания на то, что объект самостоятельно обрабатывает события щелчка или прикосновения, даже если не делает ничего, кроме их приёма. Если позже элемент можно будет активировать, вы можете, конечно, убрать этот класс.

    Если вам нужно предотвратить выделение элемента, добавьте обработчик для события ListView selectionchanging и вызовите его метод args.detail.preventtapBehavior. Это работает для всех методов выделения, включая сенсорный жест прокрутки, щелчок мыши и нажатие на клавишу клавиатуры Enter.

    Стилизация

    После того, как мы коснулись этого в лекции 4 и в последнем разделе, посвященном ListView, стилизацию лучше рассматривать на иллюстрациях, как на Рис. 5.8, где я применил некоторые яркие CSS-стили к некоторым из стилей win-* так, чтобы они выделялись. Я рекомендую вам взглянуть на материал "Стилизация ListView и его элементов" (http://msdn.microsoft.com/library/windows/apps/hh850406.aspx) в документации, где детализированы некоторые дополнительные стили, не показанные здесь

    (рис 5.8) Классы стиля, использованные элементом управления ListView

    Весь элемент управления (entire control)

    Зона, где не осуществляется прокрутка (non-scrolling area)

    Зона вокруг элемента, на самом деле, лишь для стилизации полей и прозрачности (the area around an item really only to style margin and transparency)

    Зона внутри элемента (the area inside an item)

    Зона, где осуществляется прокрутка (the scrollable area)

    Некоторые замечания о стилизации

  • Помните, что в этом деле Blend - ваш лучший друг!
  • Как и в случае со стилизацией FlipView, класс наподобие win-listview наиболее полезен для стилей полей и отбивок, в то время как свойства, вроде цвета фона, на самом деле, не отображаются (в отличие от win-viewport и win-surface).
  • Фон, не поддерживающий прокрутку, задаваемый win-viewport, используется редко, возможно, из-за неподвижного фонового изображения. Стиль win-surface позволяет настроить прокручиваемую фоновую область.
  • win-container существует, преимущественно, ради двух целей. Первая - это создавать свободное пространство между элементами, используя стили margin, и второй - переназначать фоновый цвет, назначенный по умолчанию, часто делая фон прозрачным. Таким образом, фон, задаваемый win-surface или win-viewport виден через него. Обратите внимание, что если вы устанавливаете стиль padding вместо margin, вы создаёте области, по которым пользователь будет перемещаться как по элементам, которые и активируются как элементы. Это не очень хорошо. Поэтому всегда используйте margin для создания свободного пространства между элементами.
  • Хотя win-item перечислен как стиль, его использовать не рекомендуется, он может быть удалён в будущем: просто напрямую стилизуйте шаблон элемента.
  • Документация указывает на то, что стили вроде win-container и win-surface могут быть использованы несколькими элементами управления WinJS. (FlipView использует несколько таких). Если вы хотите переопределить стили для ListView, убедитесь в том, что их область видимости соответствует другим классам, наподобие .win-listview или конкретным id или классам элемента управления.
  • Высота ListView по умолчанию равна 400 пикселям, и элемент управления не подгоняет свой размер автоматически под размер содержимого. Вы практически всегда будете нуждаться в переопределении данного стиля в CSS или устновки его в JavaScript, если вы знаете, какое пространство должен занимать ListView, как показано в лекции 6, "Макет".
  • Стили, не показанные на рисунке, но описанные в материале "Стилизация ListView и его элементов" (http://msdn.microsoft.com/library/windows/apps/hh850406.aspx), включают win-focusedoutline, win-selection, win-selected, win-selectionborder, win-selectionbackground, и win-selectionhint. Кроме того, имеется класс win-selectionstylefilled, который вы можете добавить к элементу для использования заполненного (filled) стиля выделения вместо стиля с границей (bordered), который применяется по умолчанию, как показано здесь:
  • Заставки

    Есть еще один визуальный элемент ListView, который похож на стилизацию, но стилизация на него не воздействует. Его называют заставкой (backdrop), это эффект, который включен по умолчанию при использовании gridLayout. На аппаратном обеспечении невысокой производительности, особенно на мобильных устройствах, быстрая прокрутка содержимого ListView может легко опередить возможности элемента управления по загрузке и выводу элементов. Для того, чтобы пользователь видел результат своих действий, gridLayout показывает обычные заставки для элементов, основанные на размерах элементов по умолчанию и прокручивает их до момента вывода элементов. Как мы увидим в следующем разделе, вы можете выключить эту возможность с помощью свойства gridLayout disableBackgdrop и переопределить его серый цвет, применяемый по умолчанию, с помощью свойства backdropColor.

    Макеты и объединение ячеек

    Свойство layout элемента управления ListView, которое вы можете задать в любое время, содержит обект, который используется для организации элементов списка. WinJS предоставляет два предустановленных макета: WinJS.UI.GridLayout и WinJS.UI.ListLayout. Первый, уже описанный ранее, обеспечивает горизонтальную прокрутку двумерного макета, который располагает элементы в столбцах (сверху вниз) и затем в строках (слева направо). Второй - это одномерный макет, располагающий элементы сверху вниз, подходит для вертикальных списков (как в прикрепленном режиме просмотра). И тот и другой следуют рекомендованным для представления коллекций подходам к дизайну.

    Говоря техническим языком, свойство layout - это объект, содержащий некоторое количество других параметров вместе со свойством type. Обычно вы видите синтаксис layout: { type: <layout> } в строке data-win-options ListView, где <layout> - это WinJS.UI.GridLayout или WinJS.UI.ListLayout (технически - имя функции-конструктора). При декларативном использовании, layout может так же содержать особенные параметры, зависящие от его типа. Например, следующий код конфигурирует gridLayout, содержащий заголовки слева и четыре строки:

    layout: { type: WinJS.UI.GridLayout, groupHeaderPosition: 'left', maxRows: 4 }

    Если вы создаете объект макета в JavaScript, используя new для вызова конструктора напрямую (и присваивая её свойству layout), вы можете задать дополнительные параметры в конструкторе. Это исполняется в приложении, построенном по шаблону Приложение таблицы в методе initializeLayout в файле pages/groupedItems/groupedItems.js:

    listView.layout = new ui.GridLayout({ groupHeaderPosition: "top" });

    Кроме того, вы можете задать свойства объекта layout элемента управления ListView в JavaScript, при его создании, если хотите воспользоваться подобным подходом. Обычно изменение свойств обновляет макет.

    В любом случае, каждый макет имеет собственные уникальные параметры. Для gridLayout это следующие:

  • groupHeaderPosition управляет расположением заголовков по отношению к их группам; может быть установлен в значения "left" или "top".
  • maxRows управляет количеством элементов, которые макет расположит вертикально, прежде чем начнёт располагать их в следующем столбце.
  • backdropColor предназначен для настройки цвета заставки по умолчанию (смотрите "Заставки" в предыдущем разделе), и disableBackdrops полностью отключает данный эффект.
  • groupInfo определяет функцию, которая возвращает объект, свойства которого показывают, следует ли использовать объединение ячеек и размер ячеек (смотрите ниже). Данный вызов осуществляется лишь однажды при обработке макета.
  • itemInfo определяет функцию для использования вместе с объединением ячеек, которая возвращает объект, свойства которого описывают точный размер для каждого элемента, и то, следует ли разместить элемент в новой колонке (смотрите ниже).
  • gridLayout так же имеет свойство только для чтения, которое называется horizontal и всегда имеет значение true. Для ListLayout свойство horizontal всегда false и не имеет других настраиваемых параметров.

    Теперь, так как свойство layout ListView - это лишь объект (или имя конструктора для подобного объекта), можете ли вы создать собственную функцию, определяющую макет? Да, вы можете: создайте класс, который обеспечивет те же самые открытые методы, что и встроенные макеты, как описано в WinJS.UI.Layout (http://msdn.microsoft.com/library/windows/apps/br211781.aspx) (данная тема пока недостаточно документирована). Здесь объект макета может предоставлять любые другие параметры (свойства и методы), которые к нему применимы.

    Сейчас, прежде чем вы начали размышлять над тем, нужен ли вам макет собственной разработки, хочу отметить, что gridLayout предоставляет кое-что, называемое объединением ячеек (cell spanning), что позволяет вам создавать элементы различных размеров (для ListLayout это неприменимо). Именно для этого существуют свойства groupInfo и itemInfo, как показано в Сценариях 4 и 5 примера "Шаблоны элемента для HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-item-templates-7d74826f), как показано на Рис. 5.9.

    (рис 5.9) Пример шаблонов элемента ListView показывает использование элементов различных размеров с использованием объединения ячеек

    Основная идея объединения ячеек - это задать ячейку для gridLayout, основываясь на размере наименьшего элемента (включая стили отбивки (padding) и полей(margin)). Для лучшей производительности делайте ячейки настолько большими, насколько это возможно, чтобы размер любого другого элемента в ListView был кратен размеру этого элемента.

    Включается возможность объединения ячеек посредством свойства groupInfo gridLayout. Это функция, которая возвращает объект с треямя свойствами: enableCellsSpanning, которое следует установить в true, cellWidth и cellHeight, которые содержат размеры минимальной ячейки в пикселях (что, кстати, использует возможность gridLayout для вывода заставки в подобной ситуации). В примере (смотрите js/data.js), эта функция названа groupInfo, так же как свойство макета. Здесь, для ясности, я дал ей другое имя:

    function cellSpanningInfo() {
    return {	
    enableCellSpanning: true,
    cellWidth: 310,	
    cellHeight: 80	
    };	
    }

    Затем эту функцию задают как часть свойства layout в data-win-options:

    layout: { type: WinJS.UI.GridLayout, groupInfo: cellSpanningInfo }

    Или вы можете задать layout.groupInfo из JavaScript. В любом слуае, как только вы объявили об использовании объединения ячеек, шаблон элемента должен установить свойства каждого элемента style.width и style.height, и - подходящие значения отбивки, для увеличения ваших cellWidth и cellHeight в соответствии со следующими формулами (которые являются разными представлениями одной и той же формулы):

    templateSize = ((cellSize + margin) x multiplier) - margin (размерШаблона = ((размерЯчейки + поле) х множитель) - поле)
    
    cellSize = ((templateSize + margin) / multiplier) - margin
    (размерЯчейки = ((размерШаблона+поле)/множитель) - поле)

    В примере, эти стили установлены путём задания каждому из элементов одного из трёх имён классов: smallListIconTextItem, mediumListIconTextItem, и largeListIconTextItem, CSS-код которых приведен ниже: (из css/scenario4.css иcss/scenario5.css):

    .smallListIconTextItem {
    width: 300px;	
    height: 70px;	
    padding: 5px;	
    }
    .mediumListIconTextItem {
    width: 300px;	
    height: 160px;	
    padding: 5px;	
    }
    .largeListIconTextItem {
    width: 300px;	
    height: 250px;	
    padding: 5px;	
    }

    Так как каждый из этих классов имеет отбивку, их реальные размеры из CSS равняются 310х80, 310х170 и 310х260. Поля, которые используются в формуле, исходят из стиля win-container в таблице стилей WinJS, где они равны 5 пикселей (px). Таким образом:

    ((80 + 10) * 1) - 10 = 80; минус 5px отбивка сверху и снизу =  высота 70px в CSS 
    ((80 + 10) * 2) - 10 = 170; минус 5px отбивка = высота 160px
    ((80 + 10) * 3) - 10 = 260; минус 5px отбивка = высота 250px

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

    Что касается функции itemInfo, это способ оптимизации производительности listView при использовании объединения ячеек. Без присваивания функции этому свойству, gridLayout "вручную" определяет ширину и высоту каждого элемента при выводе и это может замедлить прокрутку, если вы быстро прокручиваете большое число элементов. Так как вам, возможно, уже известны размеры элементов, вы можете предоставить эту информацию посредством функции itemInfo. Эта функция принимает индекс элемента и возвращает объект, содержащий свойства элемента width и height. (Скоро мы увидим работающий пример).

    function itemInfo(itemIndex) {
    //определяет значения itemWidth и itemHeight по заданному itemIndex
    return {	
    newColumn: false,	
    itemWidth: itemWidth,	
    itemHeight: itemHeight	
    };	
    }

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

    Возможно, вы так же заметили свойство newColumn в возвращаемом значении. Как вы можете догадаться, установка его в true принуждает gridLayout начать новый столбец с этой ячейки, позволяя вам управлять данным поведением. Вы даже можете использовать newColumn самостоятельно, если хотите, с самыми маленькими списками.

    Сейчас вы можете задаться вопросом: что произойдёт, если я задам различные размеры в шаблоне элемента, но не объявлю об объединении ячеек? Всё закончится перекрывающими (и весьма странно выглядящими) ячейками. Это происходит потому, что gridLayout воспринимает первый элемент в группе в качестве базового, задающего размеры всех остальных элементов (и, так же, сетки из заставок). Он не пытается автоматически изменить размер каждого элемента в зависимости от его содержимого. Испытайте это в Сценариях 4 и 5: удалите свойство layout.group из data-win-options ListView в html/scenario4.html или html/scenario5.html и перезапустите приложение. Вы увидите, как элементы среднего и большого размера заходят друг на друга, как показано ниже:

    Затем, пройдите в js/data.js и установите стиль первого элемента в массиве myCellSpanningData на largeListIconTextItem, и перезапустите приложение. ListView теперь будет иметь макет с данным размером, установленным в качестве базового размера элемента:

    Использование размеров первого элемента, как здесь, подчёркивает тот факт, что ListView с объединением ячеек требует больше времени на вывод, так как он должен измерить каждый из элементов в том виде, в котором он размещается, либо с помощью функции itemInfo, либо без неё. По этой причине, возможно, лучше избегать объединения ячеек при работе с большими списками.

    Гораздо интереснее это всё становится, если подумать о том, как gridLayout поступает с элементами разной ширины. Да и в примере этого нет. В базовом алгоритме он всё еще располагает элементы сверху вниз и слева направо, но сейчас он заполняет пустые пространства небольшими элементами, когда большие элементы создают подобные пустоты. Для того, чтобы это показать, изменим пример таким образом, чтобы наименьший элемент имел размеры 155х80 (половина оригинального размера), средний был размером 310х80, и большой - 310х160. Вот какие изменения позволят нам это реализовать:

  • Отменим любые изменения из предыдущих испытаний: в html/scenario4.html, вернем назад groupInfo в data-win-options, и в js/data.js, изменим класс первого элемента myCellSpanningData обратно к значению smallListIconTextItem.
  • В js/data.js, изменим cellWidth в groupInfo на 155 (половина от 310), и оставим cellHeight в значении 80. Для ясности, кроме того, добавим значение приращения к началу текста каждого элемента в массиве myCellSpanningData.
  • В css/scenario4.css:
  • Изменим ширину (width) smallListIconTextItem до 145px. Применим формулу, ((145+10)*1)-10=145. Высоту (height) изменим на 70px.
  • Изменим ширину mediumListIconTextItem на 310px, высоту - на 70px.
  • Изменим ширину largeListIconTextItem на 310px, выс оту на 160px. Для высоты применим формулу: ((80+10)*2)-10=170px.
  • Установим стиль width в правиле #listview в значение 800px, height - в 600px (для того, чтобы было больше места, в котором можно видеть макет)
  • Рекомендую выполнять эти изменения в Blend, где ваши правки отражаются на результате гораздо быстрее, чем когда вы, для проверки, запускаете приложение из Visual Studio. В любом случае, результаты, которые показаны на Рис. 5.10, где числа показывают нам порядок, в котором расположены элементы (извиняюсь за обрезанный текст… чем-то приходится жертвовать). Копию этого примера вы можете найти в дополнительных материалах к курсу.

    (рис 5.10) Измененный пример работы с шаблонами элементов ListView, более полно показывающий объединение ячеек

    В измененный пример я включил функцию itemInfo в js/data.js, как вы уже могли заметить. Она возвращает размеры элементов в соответствии с типами, заданными для этих элементов:

    function itemInfo(index) {	
    //getItem(index).data получает массив элементов из WinJS.Binding.List
    var item = myCellSpanningData.getItem(index).data;	
    var width, height;	
    switch (item.type) {	
    case "smallListIconTextItem":
    width = 145;	
    height = 70;	
    break;	
    case "mediumListIconTextItem":
    width = 310;	
    height = 70;	
    break;	
    case "largeListIconTextItem":
    width = 310;	
    height = 160;	
    break;	
    }	
    return {	
    newColumn: false,	
    itemWidth: width,	
    itemHeight: height
    };	
    }

    Вы можете установить в этой функции точку останова и убедиться в том, что она вызывается для каждого элемента. Кроме того, вы можете увидеть, что это приводит к одному и тому же результату. Теперь измените возвращаемое значение newColumn так, как показано ниже, для того, чтобы принудительно начать вывод с новой колонки перед элементом №7 и №15 на Рис. 5.10, так как они странно располагаются по столбцам.

    newColumn: (index == 6 || index == 14),  //Разрыв на элементах 7 и 15 (индексы 6 и 14)

    Результат этого изменения показан на Рис. 5.11.

    (рис 5.11) Вывод с новой колонки при объединении ячеек на элементах 7 и 15

    Последнее, что я отметил, экспериментируя с данным примером, это то, что если размеры элементов в правиле стиля наподобие smallListIconTextItem меньше, чем размер дочернего элемента, такого, как .regularlistIconTextItem (который включает поля и отбивки) больший размер выигрывает в макете. В ходе собственных экспериментов вы можете захотеть удалить поля по умолчанию в 5px, которые установлены для win-container. Это то, что создаёт пустые пространства между элементами на Рис. 5.10, но это должно быть добавлено в уравнение. Следующее правило устанавливает это поле в 0px:

    #listView > .win-horizontal .win-container {
    margin: 0px;
    }

    Оптимизация производительности ListView

    Я часто говорю людям, что с ListView можно столько всего делать, о нём можно столько всего узнать, что он достоен отдельной книги! На самом деле, Microsoft могла просто создать базовый элемент управления, который позволял бы вам создавать элементы, настроенные по шаблону и на этом остановиться. Однако, зная, что ListView будет в центре огромного количества приложений (возможно, в составе большинства приложений вне игровых приложений), ожидая, что ListView будет вызываться для управления тысячами, или даже десятками тысяч элементов, высокопрофессиональная и увлеченная своим делом группа инженеров сделала всё возможное для обеспечения множества уровней усовершенствований, которые позволят вашим приложениям наилучшим образом выполнять своё предназначение.

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

    Мне хотелось бы отметить, что материал "Использование элемента управления ListView" (http://msdn.microsoft.com/library/windows/apps/Hh781224.aspx) содержит даже больше советов, чем я способен привести здесь. (Мне нужно писать и другие главы!). Я надеюсь, что вы изучите данный материал, и кто знает, может быть вы станете тем, кто напишет всеобъемлющую книгу по ListView! Более того, дополнительное руководство по производительности приложения в целом можно найти в материале "Рекомендации по повышению производительности приложений Магазина Windows" (http://msdn.microsoft.com/library/windows/apps/hh465194.aspx), который содержит и подраздел об использовании ListView.

    Произвольный доступ

    Если вы похожи на меня и на других членов моей семьи, возможно, у вас есть постоянно увеличивающийся запас цифровых фотографий, который заставляет вас радоваться тому, что жёсткие диски объёмом больше гигабайта падают в цене. Другими словами, для многих пользователей вполне нормально работать с коллекциями, содержащими десятки тысяч элементов, которые они могут захотеть просмотреть в ListView. Но только представьте себе нагрузку от попытки загрузить эскизы для каждого из этих элементов в память для отображения их в списке. На низкопроизводительном аппаратном обеспечении, ограниченном в потреблении электроэнергии, это, возможно, приведет к необходимости срочного завершения работы нескольких приостановленных приложений, и результат может быть вовсе не "быстрым и гибким". Пользователь может столкнуться с необходимостью ждать по-настоящему долго до того момента, пока элемент управления приобретет интерактивность и может по-настоящему устать, глядя на индикатор выполнения.

    Именно поэтому свойство loadingBehavior установлено по умолчанию в значение "randomaccess". В этом режиме полоса прокрутки ListView отражает общий размер списка, в итоге, пользователь сможет оценить его размер, но ListView в любой момент полностью хранит в памяти лишь пять полных экранов элементов (с общим лимитом в 1000 элементов). Для большинства страниц это означает видимую страницу (в области просмотра) и две буферных страницы впереди и позади неё. (Если вы просматриваете первую страницу, то буфер простирается на четыре страницы вперед; если вы на последней странице, то буфер простирается на четыре страницы позади неё - вы поняли идею).

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

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

    Инкрементная загрузка

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

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

    Работа инкрементной загрузки показана в Сценариях 2 и 3 примера "Режимы работы загрузки данных ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-loading-behaviors-718a4673) (Сценарий 1 посвящен произвольному доступу, но там нет ничего нового). Режим инкрементной загрузки активирует следующие характеристики:

  • Свойство ListView pagesToLoad показывает, как много страниц или полных экранов загружаются одновременно. Значение по умолчанию - пять.
  • Свойство automaticallyLoadPages показывает, следует ли ListView загружать новые страницы автоматически, когда вы перемещаетесь по списку. Если оно установлено в true (по умолчанию), как показано в Сценарии 2, когда вы прокручиваете список до конца, вы увидите изменение полосы прокрутки при загрузке новых страниц. Если в false, что показано в Сценарии 3, новые страницы не загружаются до тех пор, пока вы не вызовете метод loadMorePages.
  • Если свойство automaticallyLoadPages установлено в true, свойство pagesToLoadThreshold показывает, как близко пользователь должен подойти к текущему концу списка, прежде чем будет запущена загрузка новых страниц. Значение по умолчанию - два.
  • Когда новые страницы начинают загружаться (либо автоматически, либо в ответ на loadMorePages), ListView начинает обновлять свойство loadingState, вызывая события loadingstatechanged, как было уже описано.
  • Функции шаблонов (Часть 2): Promise-объекты!

    Как мы только что обсудили, параметры поведения ListView при загрузке данных имеют отношение к инкрементной загрузке страниц. Будет полезным скомбинировать их с инкрементной загрузкой элементов. Для этого нам нужно взглянуть на то, что называется конвейером визуализации (rendering pipeline), в том виде, в котором это реализовано в функциях шаблонов.

    Когда мы ранее впервые смотрели на функции шаблонов (посмотрите "Как, на самом деле, работают шаблоны"), я отмечал, что они дают нам возможность контролировать и то, как конструируется элемент, и то, когда это происходит, и то, что подобные функции называют визуализаторами (renderers). Это - средство, с помощью которого вы можете реализовать пять прогрессивных уровней оптимизации для ListView (и для FlipView, хотя это распространено меньше). Сам факт использования визуализатора, с чем мы уже сталкивались, - это Уровень 1. Теперь мы готовы увидеть оставшиеся четыре уровня. Это захватывающая тема, так как она показывает усовершенствования, которые реализованы для нас в ListView!

    В этом рассказе мы можем опираться на пример "Оптимизация производительности HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-performance-39fb71f0), который демонстрирует все эти уровни и позволяет вам видеть их эффект. Вот обзор этих возможностей:

  • Простой (simple) или базовый визуализатор позволяет управлять выводом на поэлементной основе.
  • Визуализатор элементов-заполнителей (placeholder) разделяет создание элементов на два этапа. На первом этапе возвращаются лишь те объекты, которые определяют форму элементов. Это позволяет ListView быстро выполнить вывод шаблона, до того, как будут получены все подробные сведения, особенно когда данные поступают из потенциально медленного источника. Когда данные элемента доступны, начинается вторая фаза, запускается копирование этих данных в элементы и создаются дополнительные элементы, которые не влияют на форму элементов.
  • Визуализатор с повторным использованием элементов-заполнителей (recycling placeholder) добавляет возможность повторного использования существующего содержимого DOM для элементов, что быстрее, чем создание их с нуля. Для этих целей, ListView, зная, что его будут быстро пролистывать, хранит некоторое количество элементов, когда они оказываются не видны. В визуализаторе, вы добавляете ветвь кода для очистки повторно используемых элементов, если они к вам попадают, и для возвращения их в виде элементов-заполнителей. Затем вы заполняете их реальными значениями на второй стадии визуализации.
  • Многошаговый (multistage) визуализатор расширяет визуализатор с повторным использованием элементов-заполнителей отложенной загрузкой изображений и других данных тогда, когда элемент полностью готов в ListView. Кроме того, он задерживает любые действия, связанные с визуальным оформлением списка, такие, как анимации, до тех пор, пока элемент не появится на экране.
  • И, наконец, многошаговый пакетный (multistage batch) визуализатор добавляет возможность пакетного добавления изображений и других ресурсов, тем самым, отрисовывая их и получая возможность анимировать их появление в ListView в виде группы, в итоге ресурсы видеокарты (GPU) могут быть использованы более эффективно.
  • Используя любой из этих визуализаторов, вы должны стремиться к тому, чтобы сделать их как можно более быстрыми. В особенности это касается минимизации использования вызовов DOM API, которые включают в себя установку индивидуальных свойств. Используйте строку innerHTML, там, где это возможно, для создания элементов, вместо того, чтобы выполнять отдельные вызовы, и сведите к минимуму использование getElementById, querySelector и других вызовов, предусматривающих обход DOM, кэшируя элементы, к которым вы обращаетесь чаще всего. Это значительно улучшит производительность. Для того, чтобы показать эффект от этих улучшений, следующий рисунок показывает пример того, как происходит вывод данных в неоптимизированном ListView:

    Жёлтые столбцы показывают исполнение JavaScript-кода приложения - то есть - время, потраченное внутри визуализатора. Бежевые столбцы показывают время, потраченное в DOM-макете, и столбцы цвета морской волны показывают вывод данных на экран. Как вы можете видеть, когда элементы добавляются по одному, есть перерывы в исполнении кода, и сложность здесь в том, что большинство дисплеев обновляются лишь каждые 10-20 миллисекунд (50-100 Гц). В результате мы имеем множество разрывов в процессе визуализации.

    После улучшений, график может выглядеть так, как показано ниже, когда работа приложения скомбинирована в одном блоке, таким образом, значительно уменьшилась нагрузка, связанная с DOM-макетом (бежевые столбцы):

    Как и другое изображение, это получено от инструмента для анализа производительности, который называется XPerf и является частью Windows SDK (смотрите врезку). Без изучения деталей, самое важное, что мы здесь разобрали - это шаги, которые нужно предпринять для достижения подобного результата. А именно, различные формы визуализиторов, которые вы можете применять, как показано в примере.

    Врезка: XPerf и msWriteProfilerMark

    Средство XPerf в Windows SDK, документацию по которому можно найти на странице "Инструменты анализа производительности Windows" (http://msdn.microsoft.com/en-US/performance/cc825801.aspx), могут хорошо помочь вам понять реальное поведение вашего приложения в конкретной системе. Помимо прочего, он записывает в журнал вызовы, которые вы выполняете к msWriteProfilerMark (http://msdn.microsoft.com/library/windows/apps/dd433074.aspx), вы можете это заметить, просматривая исходный код WinJS. Для того чтобы отобразить это с помощью xperf, однако, вам нужно запустить протоколирование такой командой:

    xperf -start user -on PerfTrack+Microsoft-IE:0x1300

    и завершить протоколирование нижеприведенной командой, где <trace_filename> - это любой путь и имя файла по вашему выбору:

    xperf -stop user -d <trace_filename>.etl

    Открытие .etl-файла, который вы сохранили, приведет к запуску Windows Perfomance Analyzer (Анализатора производительности Windows) и отобразит график событий. Щелчкните правой кнопкой мыши по графику, затем щёлкните Summary Table (Сводная таблица). В этой таблице разверните Microsoft-IE и затем разверните узел Mshtml_DOM_CustomSiteEvents. Столбец Field3 должна содержать текст, который вы передаете msWriteProfilerMark, а столбец Time(s) поможет вам определить, сколько времени заняло действие.

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

    function simpleRenderer(itemPromise) {
    return itemPromise.then(function (item) {
    var element = document.createElement("div");
    element.className = "itemTempl";
    element.innerHTML = "<img src='" + item.data.thumbnail +
    "' alt='Databound image' /><div class='content'>" + item.data.title + "</div>";
    return element;
    });
    }

    Эта структура ожидает доступности данных элемента и возвращает promise-объект для элемента, данные которого будут получены.

    Визуализатор с повторным использованием элементов-заполнителей создаёт элемент в два этапа. Возвращаемое значение - это объект, который содержит минимальный элемент-заполнитель в свойстве element, и promise-объект renderComplete, который выполняет, если необходимо, остальную работу:

    function placeholderRenderer(itemPromise) {	
    // создает базовый шаблон для элемента, не зависящий от данных
    var element = document.createElement("div");	
    element.className = "itemTempl";	
    element.innerHTML = "<div class='content'>...</div>";	
    // Возвращает элемент в виде заполнителя, и обратный вызов для его обновления, когда данные будут 
    //доступны
    return {
    element: element,
    
    // задаёт promise-объект, который завершит работу, когда завершится визуализация
    // itemPromise завершит работу, когда будут доступны данные
    renderComplete: itemPromise.then(function (item) {
    // изменяет элемент для включения данных
    element.querySelector(".content").innerText = item.data.title;
    element.insertAdjacentHTML("afterBegin", "<img src='" +
    item.data.thumbnail + "' alt='Databound image' />");
    })
    };
    }

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

    Визуализатор с повторным использованием элементов-заполнителей теперь знает о существования второго параметра, который называется recycled, и который ListView (но не FlipView) может предоставить вашей функции визуализации, когда параметр ListView loadingBehavior установлен в "randomaccess". Если задан recycled, вы можете просто очистить элементы, вернув их в качестве элементов-заполнителей, и затем заполнить данными внутри promise-объекта renderComplete, как ранее.

    Если он не предоставлен (когда ListView только что создан, или если loadingBehavior установлено в значение "incremental"), вы создаёте новый элемент:

    function recyclingPlaceholderRenderer(itemPromise, recycled) {
    var element, img, label;
    if (!recycled) {
    // создает базовый шаблон для элемента, не зависящий от данных 
    element = document.createElement("div");
    element.className = "itemTempl";
    element.innerHTML = "<img alt='Databound image' style='visibility:hidden;'/>" + "<div class='content'>...</div>";
    }
    else {
    // очищает элемент, после чего мы можем повторно использовать его 
    element = recycled;
    label = element.querySelector(".content");
    label.innerHTML = "...";
    img = element.querySelector("img");
    img.style.visibility = "hidden";
    }
    return {
    element: element,
    renderComplete: itemPromise.then(function (item) {
    // изменяет элемент для включения данных 
    if (!label) {
    label = element.querySelector(".content");
    img = element.querySelector("img");
    }
    label.innerText = item.data.title; img.src = item.data.thumbnail; img.style.visibility = "visible";
    })
    };
    }

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

    Если вы хотите очистить элементы, которые используются повторно, вы можете предоставить функцию для свойства ListView resetItem, которая будет содержать тот же код, как показано выше для данного случая. То же самое справедливо для свойства resetGroupHeader, так как вы можете использовать функции шаблона для заголовков групп, так же, как и для элементов. Мы не говорим много об этом, так как заголовков групп обычно немного, и они обычно не играют в производительности такой же роли. Несмотря на это, подобная возможность присутствует.

    Далее, у нас есть многошаговый визуализатор, который добавляет к повторно используемым элементам-заполнителям отложенную загрузку изображений и других данных тогда, когда элемент полностью представлен в DOM, и, далее, задерживает эффекты, наподобие анимации, до тех пор, пока элемент не окажется видимым на экране.

    Ключевую роль здесь играют три метода: ready, loadImage и isOnScreen, которые прикрепляются к элементу, предоставленному itemPromise. Следующий код показывает, как этим пользоваться (здесь element.querySelector обходит лишь небольшую часть DOM, в итоге, это не проблема):

    renderComplete: itemPromise.then(function (item) {	
    // преобразует элемент для обновления его названия (title)	
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;	
    // использует promise-объект item.ready для того, чтобы отложить более
    // ресурсоёмкие процедуры
     return item.ready;
    // использует возможность объединения promise-объектов в цеочку
    // для получения возможности отмены задания
    }).then(function (item) {
    //использует загрузчик изображений для того, чтобы
    //поставить в очередь загрузку изображения
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
    //как только загружено, проверяет видимость элемента 
    return item.isOnScreen();
    });
    }).then(function (onscreen) {
    if (!onscreen) {
    //если элемент не видим, его прозрачность не анимируется 
    img.style.opacity = 1;
    } else {
    //если элемент видим, анимировать прозрачность изображения
    WinJS.UI.Animation.fadeIn(img);
    }
    })

    Хочу предупредить, что в данной оптимизации производительности используется много promise-объектов! Но всё, что здесь имеется - это стандартная структура promise-объектов, объединенных в цепочку. Первая асинхронная операция в визуализаторе обновляет простые частки элемента, такие, как текст. Затем она возвращает promise-объект в item.ready. Когда данный вызов завершится, или, точнее, если он будет исполнен - вы можете использовать асинхронный метод элемента loadImage для загрузки изображения, возвращая promise-объект item.isOnScreen из его обработчика завершения. Когда и если отложенный результат isOnScreen будет получен, вы можете выполнить необходимые действия, которые нужны только для видимых элементов.

    Я выделял "если" в этих описаниях, так как весьме вероятно то, что пользователь будет прокручивать содержимое ListView, пока всё это происходит. Когда все эти promise-объекты объединены в цепочку, ListView может отменить асинхронные операции в любой момент, когда элемент выходит из области видимости или из области буферных страниц. Достаточно сказать, что элемент управления ListView прошёл через великое множество испытаний производительности!

    Теперь мы пришли к многошаговому пакетному визуализатору, который комбинирует вставку изображений в DOM для минимизации задач, связанных с макетом и перерисовкой. В нашем примере, для этого используется функция, которая называется createBatch, которая использует метод WinJS.Promise.timeout с 64-миллисекундным периодом для комбинации promise-вызовов, загружающих изображения в многошаговом визуализаторе. Честно говоря, вам придётся поверить мне на слово, так как вам надо стать настоящим экспертом в использовании promise-объектов для того, чтобы понять, как это работает!

    //При инициализации (за пределами визуализатора)
    thumbnailBatch = createBatch();
    
    //Внутри цепочки renderComplete 
    //...
    
    }).then(function () {
    return item.loadImage(item.data.thumbnail);
    }).then(thumbnailBatch()
    ).then(function (newimg) {
    img = newimg;
    element.insertBefore(img, element.firstElementChild);
    return item.isOnScreen();
    }).then(function (onscreen) {
    
    //...
    //Реализация createBatch
    
    function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];
    
    function completeBatch() {
    var callbacks = batchedItems;
    batchedItems = [];
    for (var i = 0; i < callbacks.length; i++) {
    callbacks[i]();	
    }	
    }	
    return function () {
    batchTimeout.cancel();
    batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);
    var delayedPromise = new WinJS.Promise(function (c) {
    batchedItems.push(c);	
    });	
    return function (v) { return delayedPromise.then(function () { return v; }); };
    };	
     }

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

    Что мы только что изучили

  • Коллекциями данных в памяти можно управлять с помощью WinJS.Binding.List, который отлично интегрируется с элементами управления для коллекций, наподобие FlipView и ListView. Данные коллекций, размещенных в памяти, могут поступать из WinJS.Xhr и из файлов.
  • Элемент управления WinJS.UI.FlipView отображает один элемент за раз; WinJS.UI.ListView отображает множество элементов в соответствии с конкретным макетом.
  • Центральная идея, общая для обоих элементов управления, заключается в том, что имеется источника данны, для вывода каждого элемента этого источника используется шаблон элемента. Шаблоны могут быть заданы как декларативно, так и программно.
  • ListView работает с добавленным описанием макета. WinJS предоставляет два встроенных макета. GridView - это двумерный, горизонтально прокручиваемый список; ListLayout предназначен для вывода одномерных вертикально прокручиваемых списков. Возможно реализовать и собственный макет.
  • ListView предоставляет возможность отображения элементов в группах; WinJS.BindingList предоставляет методы для создания сгруппированных, отсортированных и отлфильтрованных проекций элементов из источника данных.
  • Элемент управления для семантического масштабирования (WinJS.UI.SemanticZoom) предоставляет интерфейс, посредством которого вы можете переключаться между двумя различными представлениями источника данных, режимом детализированного представления (zoomed-in), который отображает детали, и режимом общего представления (zoomed-out), который предоставляет более общую информацию. Внешний вид этих двух режимов может очень сильно различаться, но они должны отображать связанные данные. Интерфейс IZoomableView нужен для каждого режима просмотра, таким образом, элемент управления SemanticZoom может переключаться между ними и перемещаться к верному элементу.
  • WinJS предоставляет StorageDataSource для создания коллекций на основе элементов StorageFile.
  • Можно реализовать пользовательский источник данных, как показано в примерах Windows SDK.
  • Программно заданные шаблоны реализованы в виде функций шаблонов, или визуализаторов. Эти функции могут реализовывать прогрессивные уровни оптимизации для отложенной загрузки изображений и пакетного добавления элементов в DOM.
  • И FlipView, и ListView предоставляют множество параметров и возможностей по стилизации. ListView, кроме того, позволяет организовывать выделение объектов и применять различные схемы поведения при выделении.
  • Элемент управления ListView обеспечивает встроенную поддержку оптимизации произвольного доступа к большим источникам данных, и, так же, инкрементный доступ к потенциально бесконечным источникам данных.
  • Элемент управления ListView поддерживает понятие объединения ячеек в gridLayout для поддержки отображения элементов разных размеров, каждый из которых должен быть кратен размеру базовой ячейки.
  • Страницы:

    Файлы к данной лекции Вы можете скачать здесь.

    Можно с уверенностью говорить о том, что везде, где бы вы ни были, вы окружены множеством коллекций. Книги, которые вы обычно читаете - это коллекция глав, а глава - это коллекция страниц. Страницы, в свою очередь, являются коллекциями абзацев, абзацы - коллекции слов, слова - коллекции букв, а буквы (если вы читаете это в электронном виде) - это коллекции пикселей. Всё дальше и дальше…

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

    Мы воспринимаем всё это как коллекции, так как мы знаем, как обобщить отдельные объекты - как листья или страницы, или игрушки - в категории или группы. Это даёт нам мощный инструмент для организации этих вещей и управления ими (за исключением одежды в моём шкафу, моя жена может это подтвердить). И так же, как физический мир вокруг нас во многом состоит из коллекций, цифровой мир, который мы используем для того, чтобы представить объекты реального мира, так же полон коллекций. Языки программирования, наподобие JavaScript, имеют конструкции, такие, как массивы, для организации коллекций данных и управления ими, и окружение, наподобие Windows 8, обеспечивает элементы управления для коллекций, с помощью которых мы можем визуализировать данные и управлять ими.

    В данной лекции мы обратим наше внимание на два элемента управления для коллекций, которые предоставлены WinJS. Это FlipView (представление отражения), который отображает один элемент коллекции, и ListView (представление списка), который отображает множество элементов, организуя их по-разному. Как вы можете ожидать, ListView обладает большей функциональностью. Так как он является одной из центральных деталей во многих подходах к проектированию приложений, мы потратим основное количество времени на то, чтобы глубоко его изучить, а так же изучим концепцию и реализацию семантического масштабирования (semantic zoom) (на самом деле, другого элемента управления).

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

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

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

    Для рассмотрения основ элементов управления для коллекций, посмотрим сначала на FlipWiew, который позволит нам начать разговор о шаблонах элементов и источниках данных. Затем мы увидим, как это можно применить к ListView, затем разберем группировку элементов в ListView.

    Быстрый старт №1: пример использования элемента управления FlipWiew

    Как показано на Рис. 5.1, пример "Элемент управления FlipView" (http://code.msdn.microsoft.com/windowsapps/FlipView-control-sample-18e434b4) содержит неплохой код для этого элемента управления и его визуальное представление, позволяющее исследовать этот элемент управления. (Я испытываю особую благодарность за то, что мне не пришлось писать подобные примеры для этого курса!). Для целей быстрого старта, посмотрим на первый сценарий, касающийся заполнения элемента управления из простого источника данных и использования шаблона для рендеринга этого элемента, так как такие же механизмы применяются в ListView. Позже мы вернемся к другим сценариям использования FlipView.

    (рис 5.1) Пример использования элемента управления FlipView, где элемент управления отображает картинку

    Так как FlipView - это элемент управления WinJS, с конструктором WinJS.UI.FlipView, мы объявляем его в разметке с атрибутами data-win-control и data-win-options (смотрите html/simpleFlipView.html):

    <div id="simple_FlipView" class="flipView" data-win-control="WinJS.UI.FlipView"
    data-win-options="{ itemDataSource: DefaultData.bindingList.dataSource,	
    itemTemplate: simple_ItemTemplate }">	
    </div>

    И, конечно, в процессе загрузки сраницы вызывается WinJS.UI.processAll для создания экземпляра элемента управления. В параметрах FlipView мы можем сразу же увидеть два критически важных участка, благодаря которым он работает: это источник данных, который предоставляет содержимое, нужное каждому элементу, и шаблон для вывода элемента.

    Если вы обратили внимание на конец лекции 4, вы, возможно, догадываетесь, что шаблон - это экземпляр WinJS.Binding.Template. И вы правы! Эта часть разметки, на самом деле, расположена перед объявлением элемента управления в html/simpleFlipView.html.

    <div id="simple_ItemTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
    <div class="overlaidItemTemplate">
    <img class="image" data-win-bind="src: picture; alt: title" />
    <div class="overlay">
    <h2 class="ItemTitle" data-win-bind="innerText: title"></h2>
    </div>
    </div>
    </div>

    Обратите внимание на то, что шаблон должен быть всегда объявлен в разметке до элемента управления, который на него ссылается: WinJS.UI.processAll должен создать экземпляр шаблона прежде чем элемент управления запросит шаблон для вывода своего содержимого для каждого элемента источника данных. Так же вспомните, из лекции 4, что создание экземпляра шаблона убирает его содержимое из DOM, и, таким образом, оно не может быть изменено во время выполнения программы. Вы можете видеть это, исполняя пример: разверните узлы в Проводнике DOM (DOM Explorer) в Visual Studio, или в панели Динамическая DOM (Live DOM) в Blend, и вы увидите лишь корневой элемент шаблона div, который не имеет элементов-потомков.

    В примере, прозаически названный Обратите внимание на то, что для того, чтобы подобные элементы управления были полностью интерактивными, присвойте им класс win-intaractive, иначе окружающий их элемент управления (это применимо и для ListView) поглотит события ввода до того, как они достигнут данных элементов управления..

    Кроме того, вы увидите, что шаблон использует атрибуты привязки данных WinJS, где свойства img.src, img.alt и h2.innerText привязаны к свойствам источника данных, которые называются picture и title. Это показывает, как свойства двух целевых элементов могут быть привязаны к одному источнику. (Помните, что если вы осуществляете привязку к свойствам элемента управления WinJS, а не к его элементам-потомкам, эти свойства должны начинаться с winControl).

    Что касается источника данных, то параметру itemDataSource элемента управления FlipView присвоено значение DefaultData.bindingList.dataSource, описание источника вы можете найти в js/DefaultData.js:

    var array = [
    { type: "item", title: "Cliff", picture: "images/Cliff.jpg" },
    { type: "item", title: "Grapes", picture: "images/Grapes.jpg" },
    { type: "item", title: "Rainier", picture: "images/Rainier.jpg" },
    { type: "item", title: "Sunset", picture: "images/Sunset.jpg" },
    { type: "item", title: "Valley", picture: "images/Valley.jpg" }
    ];
    var bindingList = new WinJS.Binding.List(array);
    
    WinJS.Namespace.define("DefaultData", {
    bindingList: bindingList, 
    array: array
    });

    Мы кратко ознакомились с WinJS.Binding.List в конце лекции 4. Его цель заключается в том, чтобы превратить массив, хранящийся в памяти, в наблюдаемый источник данных для односторонней привязки данных. Контейнер WinJS.Binding.List, кроме того, необходим, так как FlipView и ListView не могут работать напрямую с простыми массивами, даже при единовременной привязке данных. Они ожидают от источников данных наличия у них методов интерфейса WinJS.UI.IListDataSource. Свойство dataSource WinJS.Binding.List, как в bindingList.dataSource, предоставляет то же самое, и вы всегда будете использовать это свойство вместе с FlipView и ListView (Оно, на самом деле, существует лишь для этого). Если вы об этом забудете и попытаетесь осуществить прямую привязку к WinJS.Binding.List, вы увидите исключение, в котором говорится о том, что "Объект не поддерживает свойство или метод 'createListBinding'" ("Object doesn't support property or method 'createListBinding'.")

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

    Отметим, что WinJS.Binding.List полностью поддерживает динамические данные. Если вы посмотрите на его описание (http://msdn.microsoft.com/library/windows/apps/hh700774.aspx) в документации, вы увидите, что он выглядит практически так же, как JavaScript-массив, со свойством length и полным набором методов массивов - от concat и indexOf до push, pop и unshift. Они устроенны именно так, как можно этого ожидать: от вас не требуется повторное изучение основ.

    Кроме того, важно отметить, что и для FlipView, и для ListView, подобная установка свойства элемента управления itemDataSource автоматически создаёт одностороннюю привязку данных, в итоге, любое изменение в объекте-списке, или даже в массиве, на основании которого он построен, запустит автоматическое обновление связанного элемента управления.

    Быстрый старт №2a: основные возможности HTML ListView

    Как я уже говорил раньше, основные механизмы, касающиеся источников данных и шаблонов, применимые к ListView, это те же, что применимы к FlipView. Вы можете увидеть всё это в примере "Основные возможности HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-basic-usage-sample-fcc451db), Рис. 5.2. Обратите внимание на первых два сценария, которые касаются создания элемента управления и ответа на события отдельных элементов, которые он отображает.

    Так как ListView способен одновременно отображать множество элементов, ему нужно еще кое-что, в дополнение к источнику данных и шаблону. Что-то, что описывало бы, как эти элементы визуально соотносятся друг с другом. Это - свойство layout элемента управления ListView, которое мы увидим в разметке, в Сценарии 1 данного примера, вместе с некоторыми другими параметрами поведения (html/scenario1.html).

    <div id="listView" data-win-control="WinJS.UI.ListView"	
    data-win-options="{ itemDataSource: myData.dataSource,	
    itemTemplate: select('#smallListIconTextTemplate'), selectionMode: 'none',	
    tapBehavior: 'none', swipeBehavior: 'none', layout: { type: WinJS.UI.GridLayout } }">
    </div>
    (рис 5.2) Пример "Основные возможности HTML ListView"

    Конструктор ListView, WinJS.UI.ListView, конечно, вызывается вездесущим WinJS.UI.processAll, когда загружен элемент управления страницей. Источник данных для данного списка установлен в myData.dataSource, где myData - это, опять же, WinJS.Binding.List (определенный в js/data.js, на основе обычного массива) и его свойство dataSource обеспечивает необходимый интерфейс.

    Шаблон отдельного элемента определен ранее в default.html, с параметром id, равным smallListIconTextTemplate, и это, в основном, то же самое, что мы видим в FlipView (img и текстовые элементы), поэтому я не привожу здесь их описание.

    В параметрах элемента управления мы видим три свойства, влияющих на поведение элемента: selectionMode, tapBehavior и swipeBegavior. Все они в этом примере установлены в 'none' для того, чтобы полностью отключить возможность выделения элементов и щелчков мышью по ним, превращая ListView в средство пассивного отображения объектов. Содержимое в нём можно перематывать, но элементы не отвечают на ввод. (Смотрите, кроме того, врезку "Стилизация элементов при зависании над ними указателя мыши")

    Что касается свойства layout, то это объект, свойство type которого показывает, какой макет использовать. WinJS.UI.GridLayout, который используется здесь, отображает элементы сверху вниз и слева направо, что подходит для горизонтальной прокрутки. WinJS предоставляет и другой тип макета, который называется WinJS.UI.ListLayout. Это одномерный список, размещающий элементы сверху вниз, который подходит для вертикальной прокрутки, особенно - в прикрепленном режиме просмотра. (Мы скоро увидим это при рассмотрении шаблона Приложение таблицы. Пример ListView, который мы здесь рассматриваем, не имеет хорошего варианта для прикрепленного режима просмотра).

    Сейчас элемент управления ListView в Сценарии 1 лишь отображает элементы, часто нужно, чтобы они реагировали на щелчок мыши или прикосновения. Сценарий 2 показывает это, здесь свойство tapBehavior установлено в 'invoke' (смотрите html/scenario2.htm). Это равносильно использованию WinJS.UI.tapBehaviortoggleSelect, как показано в справке по перечислению tapBehavior (http://msdn.microsoft.com/library/windows/apps/hh701303.aspx) для "invoke". Данный вариант поведения позволяет выделить элемент или снять с него выделение в зависимости от его состояния, и затем активирует его. Другой вариант - это directSelect, где элемент всегда выделен, и затем активируется, при этом invokeOnly исполняется лишь тогда, когда элемент активирован без изменения состояния выделения. Кроме того, вы можете установить параметры поведения элемента в none, в итоге прикосновение или щелчок мышью будут проигнорированы.

    Когда отображаемый элемент активируется, элемент управления ListView запускает событие itemInvoked. Вы можете подключить обработчик события, используя либо addEventListener, либо свойство oniteminvoked элемента ListView. Вот как это выполняется в Сценарии 2 (слегка реорганизованный код из js/scenario2.js):

    var listView = element.querySelector('#listView').winControl;
    listView.addEventListener("iteminvoked", itemInvokedHandler, false);
    }

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

    В вышеприведенном коде, вы можете так же назначить обработчик, используя напрямую свойство listView.oniteminvoked , или вы можете задать обработчик в свойстве itemInvoked в data-win-options (в таком случае функция должна быть отмечена безопасной для обработки). Объект события, который вы затем получите в обработчике, содержит promise-объект для активированного элемента, а не сам элемент, поэтому вам нужно вызвать его метод done или then для того, чтобы получить данные элемента. Кроме того, полезно знать, что вам никогда не следует менять свойство источника данных ListView напрямую, внутри обработчика itemInvoked, так как это, возможно, вызовет исключение. Если вам нужно это сделать, заключите код, вносящий изменения, в setImmediate, так вы сможете вернуться в поток пользовательского интерфейса.

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

    В то время, как отключение возможности выделения и реакции на прикосновения в ListView приводит к созданию пассивного элемента управления, зависание над элементом указателя мыши (или соответствующее событие на подходящем для этого сенсорном оборудовании), приводит к подсветке каждого элемента. Вернитесь к Рис. 5.2. Вы можете управлять этим, используя псевдо-селектор .win-container:hover для элементов управления. Например. следующее правило стиля полностью убирает эффект, проявляющийся при зависании указателя мыши:

    #myListView .win-container:hover {
    background-color: transparent;
    outline: 0px;	
    	}

    Быстрый старт №2b: пример группировки элементов в ListView

    Отображение списка элементов - это замечательно, но чаще коллекции нуждаются в ином уровне организации - в том, что мы называем группировкой. Это вполне очевидно, когда я открываю ящик с документами около рабочего стола, который содержит набор документов, одни из которых более важные, другие - менее. На ярлыках папок с документами я вижу надписи, показывающие их отношение к той или иной группе: Налоги, Финансы, Сообщества, Страхование, Машина, Литературный проект и Разное (среди прочих). Очевидно, нам нужно средство для группировки элементов внутри элемента управления и ListView счастлив нам в этом помочь.

    Базовую демонстрацию группировки можно найти в примере "Группировка в HTML ListView и семантическое масштабирование" (http://code.msdn.microsoft.com/windowsapps/ListView-grouping-and-6d032cc1) (Рис. 5.3). Как и в случае с примером, демонстрирующим основные возможности, код в js/groupedData.js содержит массив, располагаемый в памяти, на основе которого мы создаём WinJS.Binding.List. Вот выдержка, показывающая структуру элемента (я показал бы весь массив, но после этого мне захочется чего-нибудь сладкого!):

    var myList = new WinJS.Binding.List([	
    { title: "Banana Blast", text: "Low-fat frozen yogurt", picture: "images/60Banana.png" },
    { title: "Lavish Lemon Ice", text: "Sorbet", picture: "images/60Lemon.png" },	
    { title: "Creamy Orange", text: "Sorbet", picture: "images/60Orange.png" },	
    ...

    Здесь есть набор элементов со свойствами title, text и picture. Мы можем сгруппировать их так, как нам захочется, и даже изменить группировку "на лету". Как показывает Рис. 5.3, здесь образцы сгруппированы по первым буквам свойства title.

    (рис 5.3) Группировка HTML ListView и пример использования семантического масштабирования

    Если вы взглянете на описание ListView (http://msdn.microsoft.com/library/windows/apps/br211833.aspx), вы увидите, что элемент управления работает с двумя шаблонами и двумя коллекциями: то есть, наряду со свойствами itemTemplate и itemDataSource, это свойства groupHeaderTemplate (шаблон заголовка группы) и groupDataSource (источник данных группы). Они используются с gridLayout элемента управления ListView (по умолчанию) для организации групп и создания заголовков над элементами.

    Шаблон заголовка в html/scenario1.html очень прост (и шаблон элемента очень похож на то, что мы уже видели):

    <div id="headerTemplate" data-win-control="WinJS.Binding.Template">
    <div class="simpleHeaderItem">	
    <h1 data-win-bind="innerText: title"></h1>	
    </div>	
    </div>

    На него есть ссылка в объявлении элемента управления (другие параметры опущены):

    <div id="listView" data-win-control="WinJS.UI.ListView"	
    data-win-options="{ groupDataSource: myGroupedList.groups.dataSource,
    groupHeaderTemplate: headerTemplate }">	
    </div>

    В случае с источником данных, вы можете видеть, что мы используем переменную, которая называется myGroupedList со свойством внутри, которое называется groups. Что всё это значит?

    Давайте устроим короткое концептуальное отступление. Хотя компьютеры без проблем обрабатывают большие объёмы исходных данных, вроде массива myList, людям удобнее видеть информацию в более организованном виде. Три основных способа достижения этого - группировка (grouping), сортировка (sorting) и фильтрация (filtering). Группировка организует элементы в группы, как показано на Рис. 5.3. Сортировка располагает их в соответствии с различными правилами. Фильтрация позволяет выбирать подмножества элементов, которые удовлетворяют определенным критериям. Во всех трёх случаях, однако, вам не нужно, чтобы эти операции меняли базовые данные: пользователь может захотеть группировать, сортировать или фильтровать одни и те же данные различными способами в разное время.

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

    Объект WinJS.Binding.List предоставляет методы для создания этих проекций: createGrouped (http://msdn.microsoft.com/library/windows/apps/Hh700742.aspx), createSorted (http://msdn.microsoft.com/library/windows/apps/hh700743.aspx) и createFiltered (http://msdn.microsoft.com/library/windows/apps/hh700741.aspx). Каждый из методов создаёт особую форму WinJS.Binding.List: GroupedSortedListProjection, SortedListProjection и FilteredListProjection соответственно. Таким образом, каждая из проекций представляет собой список, подходящий для привязки данных, с некоторыми дополнительными методами и свойствами, которые специфичны для каждой из проекций. Вы даже можете создать одну проекцию из другой. Например, команда createGrouped(...).createFiltered(...) создаст отфильтрованную проекцию на основе сгруппированной проекции. (Обратите внимание на то, что метод sort не создаёт проекцию. Он применяет операцию сортировки на проекции, для которой вызывается, как команда sort для массивов JavaScript).

    Теперь, когда мы знаем о проекциях, мы можем увидеть, как создан myGroupedList:

    var myGroupedList = myList.createGrouped(getGroupKey, getGroupData, compareGroups);

    Этот метод принимает в качестве параметров три функции. Первая - функция ключа группы (group key), связывает элемент с группой: она получает элемент и возвращает подходящую строку группы, известную как ключ. Ключ, который должен быть строкой, может быть чем-то, что прямо включено в элемент, или может быть получен из свойств элемента. В примере, функция getGroupKey возвращает первый символ свойства элемента title (в верхнем регистре). Обратите, однако, внимание, что исходный пример просто использует charAt для получения характеристики группировки, однако этот подход не работает для большого количества языков. Вместо этого, используйте класс Windows.Globalization.Collation.CharacterGroupings (http://msdn.microsoft.com/library/windows/apps/windows.globalization.collation.charactergroupings.aspx) и его метод lookup (http://msdn.microsoft.com/library/windows/apps/windows.globalization.collation.charactergroupings.lookup.aspx), как показано ниже, который нормализует регистр символов автоматически, в итоге, вызов toLocaleUpperCase (http://msdn.microsoft.com/library/6t6xaca8.aspx) необязателен:

    var cg = Windows.Globalization.Collation.CharacterGroupings();
    
    function getGroupKey(dataItem) {
    return cg.lookup(dataItem.title);
    }

    Этот код, а так же другие изменения, сделанные ниже, можно найти в измененной версии данного примера, который включён в дополнительные материалы к курсу.

    Знайте, что эта первая функция, которую мы называем функция ключа группы, определяет только связь между элементом и группой, и ничего больше. Она так же вызывается для каждого элемента в коллекции, когда осуществляется вызов createGrouped, таким образом, она должна работать быстро. По этой причине создание CharacterGroupings осуществляется за пределами функции. Совет. Если извлекать ключ группы из элемента во время выполнения приложения нужно для обеспечения работоспособности связанных механизмов, вы улучшите общую производительность, сохраняя заранее полученный ключ в элементе, вместо того, чтобы просто получать его из функции ключа группы.

    Данные для групп, которые являются коллекциями, к которым присоединен шаблон заголовка, на самом деле, не создаются до того, как будет вызван метод проекции группы group, что происходит, когда обрабатывается параметр groupedDataSource элемента управления ListView. В этот момент вызывается вторая функция, переданная createGrouped - функция данных группы. Она вызывается лишь однажды для каждой группы с представляющим (representative) элементом для данной группы. В ответ, функция возвращает объект для данной группы, который содержит все свойства, которые нужны для привязки данных.

    В примере, функция gerGroupData (переданная createGroup) просто возвращает объект с единственным свойством groupTitle, которое является тем же самым, что и ключ группы, но, конечно, вы можете сделать это значение любым. Этот код так же модифицирован, в сравнении с исходным примером, для того, чтобы подходить для целей глобализации. Мы добиваемся этого, повторно используя getGroupKey:

    function getGroupData(dataItem) {
    return {	
    groupTitle: getGroupKey(dataItem)
    };	
    }	

    В модифицированном примере я изменил имя свойства объекта данных этой группы title на более явное groupTitle для того, чтобы было совершенно понятно, что он ни коим образом не влияет на свойство title отдельных элементов. Это подразумевает изменение шаблонов заголовков в html/scenario1.html и html/scenario2.html, чтобы они ссылались на groupTitle. Это поможет нам быть уверенными в том, что контексты данных шаблона элемента и заголовка отличаются. В случае с шаблоном заголовка, это коллекция, созданная из значений, возвращённых функцией данных группы. В случае с шаблоном элемента, это проекция группы из WinJS.Binding.List.createGrouped. Две разные коллекции - помните об этом.

    А почему функция данных группы отделена от других? Почему бы просто не создать эту коллекцию автоматически из ключей групп? Это потому что часто нужно включать дополнительные свойства в данные группы для использования их в шаблоне заголовка, или в масштабированном виде (с помощью функции семантического масштабирования). Воспринимайте функцию данных группы как о том, что обеспечивает общую информацию для каждой группы. (Текст заголовка - это наиболее часто используемый элемент подобной информации). Так как эта функция вызывается лишь раз для каждой группы, вместо того, чтобы вызываться для каждого элемента, её вызов - это подходящее время для вычисления или получения из других источников сводных данных общего уровня. Например, для того, чтобы показать количество элементов в заголовке группы, нам лишь нужно включить данное свойство в объект, который возвращает функция данных группы, затем осуществить привязку данных к элементу в заголовке, который должен быть связан с данным свойством.

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

    function getGroupData(dataItem) {
    var key = getGroupKey(dataItem);
    	
    //Получает отфильтрованную проекцию для нашего списка, проверяет на совпадение ключей
    var filteredList = myList.createFiltered(function (item) {
    return key == getGroupKey(item);
    });	
    return {
    title: key,
    count: filteredList.length
    };	
    	}

    Что касается свойства count в коллекции, мы можем использовать его в шаблоне заголовка:

    <div id="headerTemplate" data-win-control="WinJS.Binding.Template" style="display: none">
    <div class="simpleHeaderItem">	
    <h1 data-win-bind="innerText: groupTitle"></h1>	
    <h6><span data-win-bind="innerText: count"></span> items</h6>	
    </div>	
    </div>

    После незначительных манипуляций в css/scenario1.css - изменения высоты класса simpleHeaderItem до 65 пикселей для того, чтобы получить немного дополнительного места - список будет выглядеть так, как показано ниже:

    И, наконец, вернемся к Это действие полностью отделено от создания отсортированной проекции отдельных элементов, для которого вы использовали бы WinJS.Binding.List.createSorted.. Эта функция принимает два ключа группы и возвращает ноль, если они равны, отрицательное число, если первый ключ при сортировке расположен перед вторым, и положительное число, если второй ключ расположен перед первым. Функция compareGroups в примере выполняет сортировку по алфавиту, которую я обновил в модифицированной версии для того, чтобы она соответствовала особенностям приложений, рассчитанных на глобальный рынок:

    function compareGroups(left, right) {
    return groupCompareGlobalized(left, right);
    }
    
    function groupCompareGlobalized(left, right) {
    var charLeft = cg.lookup(left);
    var charRight = cg.lookup(right);
    // Если оба имеют одинаковый символ группы, воспринимает их как одинаковые
    if (charLeft.localeCompare(charRight) == 0) {	
    return 0;	
    }	
    
    // В разных группах, мы должны полагаться на сортировку с учетом языкового стандарта так как
    // имена групп не сортируются так же, как сами группы, для некоторых языков.	
    return left.localeCompare(right);	
     }

    Для двухуровневой сортировки, сначала - по убыванию номера группы, потом по первому символу, мы можем написать следующее (это модифицированный пример, на этот код надо сослаться в myListcreateGrouped для того, чтобы увидеть его в действии).

    function compareGroups2(left, right) {
    var leftLen = filteredLengthFromKey(left);
    var rightLen = filteredLengthFromKey(right);
    
    if (leftLen != rightLen) {
    return rightLen - leftLen;
    }
    return groupCompareGlobalized(left, right);
    }
    function filteredLengthFromKey(key) {	
    var filteredList = myList.createFiltered(function (item) {
    return key == getGroupKey(item);	
    });	
    return filteredList.length;
    }

    Отладка функций группировки

    Если кажется, что различные функции группировки работают неправильно, вы можете устанавливать точки останова и пошагово исполнять код несколько раз, но это может превратиться в утомительную задачу, так как функции вызываются очень много раз даже для коллекций умеренного размера. Вместо этого, попробуйте воспользоваться console.log для того, чтобы узнать о том, какие параметры передаются данным функциям и/или какие значения возвращаются, что позволит вам получить итоговый результат гораздо быстрее. Для того, чтобы увидеть, что происходит в функции сортировки группы, например, попробуйте такой код:

    console.log("Comparing left = " + left + " to right = " + right);

    Элемент управления ListView в шаблоне Приложение таблицы

    Теперь, когда мы рассмотрели подробности об элементе управления ListView и об источниках данных, расположенных в памяти, мы можем полностью понять устройство шаблона Приложение таблицы (Grid App) в Visual Studio и Blend. Как было упомянуто в подразделе "Процесс и стили навигации" раздела "Элементы управления страниц и навигация" лекции 3, этот шаблон проекта предоставляет структуру приложения, построенную вокруг навигации по страницам: домашняя страница (pages/groupedItems) отображает коллекцию примеров данных (js/data.js) в элементе управления ListView, где каждое представление элемента описано с помощью WinJS.Binding.Template, так же, как и заголовки групп. Рис. 5.4 показывает макет домашней страницы и идентифицирует соответствующие элементы ListView. Как мы уже обсуждали, жест прикосновения к элементу вызывает перемещение на страницу pages/groupDetail, и сейчас мы сможем увидеть, как всё это работает с ListView.

    Элемент управления ListView на Рис. 5.4 занимает нижнюю часть области содержимого приложения. Так как он поддерживает горизонтальную прокрутку, он, на самом деле, выходит за края. Использованы различные CSS-поля для выравнивания первого элемента и элементов макета, которые позволяют ему заходить за левый край при прокрутке ListView.

    (рис 5.4) Элементы в ListView на домашней странице шаблона Приложение таблицы (Все цветные элементы - это добавленные для разъяснений метки и линии)

    Заголовки групп (Group headers)

    Элементы, выведенные из шаблона (Items rendered from template)

    Элемент управления ListView (Полностью примыкает к краям) (ListView Control (full bleed to sides))

    С ListView в этом проекте происходит не так уж и много всего, поэтому рассмотрим происходящее пошагово. Для начинающих, разметка элемента управления в pages/groupedItems/groupedItems.html весьма стандартна, среди параметров - лишь тот, который указывает на то, что элементы никак не реагируют на выделение:

    <div class="groupeditemslist win-selectionstylefilled"
    aria-label="List of groups" data-win-control="WinJS.UI.ListView" 
    data-win-options="{ selectionMode: 'none' }">
    </div>

    Переходя к pages/groupedItems/groupedItems.js, мы можем сказать, что метод ready обрабатывает инициализацию:

    ready: function (element, options) {	
    var listView = element.querySelector(".groupeditemslist").winControl;	
    listView.groupHeaderTemplate = element.querySelector(".headerTemplate");
    listView.itemTemplate = element.querySelector(".itemtemplate");	
    listView.oniteminvoked = this._itemInvoked.bind(this);	
    // (Инициализация обработчика событий клавиатуры опущена)...
    
    this.initializeLayout(listView, appView.value);
    listView.element.focus();
    },

    Здесь вы можете видеть, что шаблон элемента управления может быть задан в коде так же просто, как это делается в разметке, и в этом случае мы используем класс для того, чтобы определить местоположение элемента шаблона, вместо использования id. Почему это работает? Потому, что мы, на самом деле, ссылаемся на элемент всё время: хост-процесс приложения автоматически создаёт переменную для элемента, которая имеет то же имя, что и id. Это - одно и то же. В коде вы, кроме того, можете предоставить функцию вместо декларативно описанного шаблона, которая позволит вам динамически выводить каждый элемент по отдельности. Больше об этом позже.

    Вы можете, кроме того, видеть, как эта страница назначает обработчик для событий itemInvoked (выше ready), вызывая WinJS.Navigation.navigate для того, чтобы перейти к страница groupDetail или itemDetail, как мы видели в лекции 3:

    _itemInvoked: function (args) {
    if (appView.value === appViewState.snapped) {
    // Если страница в прикрепленном режиме, пользователь открывает группу. 
    var group = Data.groups.getAt(args.detail.itemIndex); this.navigateToGroup(group.key);
    } else {
    // Если страница не в прикрепленном режиме, пользователь открывает элемент. 
    var item = Data.items.getAt(args.detail.itemIndex); nav.navigate("/pages/itemDetail/itemDetail.html", {
    item: Data.getItemReference(item) });
    }
    }
    
    navigateToGroup: function (key) {
    nav.navigate("/pages/groupDetail/groupDetail.html", { groupKey: key });
    },

    В таком случае, мы получаем данные элемента из коллекции (методы getAt) вместо того, чтобы использовать данные самого элемента. Это так, потому что необходимая информация о группе, необходимая для первого случая, не является, напрямую, частью элемента. Кроме того, мы видим здесь, что страницы по-разному интерпретируют активацию, в зависимости от состояния просмотра. Это так, потому что переход в новый режим просмотра меняет и макет, и источник данных. Это обрабатывается во внешнем методе страницы _initializeLayout, вызываемом и при старте приложения, и из функции страницы updateLayout:

    initializeLayout: function (listView, viewState) {
    if (viewState === appViewState.snapped) { listView.itemDataSource = Data.groups.dataSource; 
    listView.groupDataSource = null;
    listView.layout = new ui.ListLayout();
    } else {
    listView.itemDataSource = Data.items.dataSource;
    listView.groupDataSource = Data.groups.dataSource;
    listView.layout = new ui.GridLayout({ groupHeaderPosition: "top" });
    }
    },

    Макет элемента управления ListView может быть изменен в любое время путём установки его свойства property. Когда программа находится в прикрепленном режиме просмотра, он установлен в WinJS.UI.ListLayout, в иных случаях - в WinJS.UI.GridLayout (свойство которого groupHeaderPosition может быть "top" или "left"). Кроме того, вы можете увидеть, что вы можете "на лету" поменять источник данных для ListView: в прикрепленном режиме просмотра это - список групп, в противном случае - список элементов.

    Надеюсь, теперь вы понимаете, почему я как следует разъяснил особенности навигации по страницам, прежде чем мы добрались до ListView, так как этот проект, при ближайшем рассмотрении, выглядит довольно сложно. В любом случае, посмотрим сейчас на шаблоны для этой страницы (pages/groupedItems/groupedItems.html):

    <div class="headertemplate" data-win-control="WinJS.Binding.Template">	
    <button class="group-header win-type-x-large win-type-interactive"	
    data-win-bind="groupKey: key" role="link" tabindex="-1" type="button"	
    onclick="Application.navigator.pageControl.navigateToGroup(event.srcElement.groupKey)" >	
    <span class="group-title win-type-ellipsis" data-win-bind="textContent: title"></span>
    <span class="group-chevron"></span>	
    </button>	
    </div>	
    <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
    <div class="item">
    <img class="item-image" src="#" data-win-bind="src: backgroundImage; alt: title" />
    <div class="item-overlay">
    <h4 class="item-title" data-win-bind="textContent: title"></h4>
    <h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6>
    </div>
    </div>
    </div>

    Опять же, мы так же используем WinJS.Binding.Template и различные части синтаксиса привязки данных, разбросанные по разметке, не говоря уже об обработчике click, присвоенном тексту заголовка, который, как и элемент в прикрепленном режиме просмотра, ведет нас к странице подробной информации о группе.

    Как и в случае с данными (которые вы, вероятнее всего, замените), это задаётся в js/data.js, как массив, расположенный в памяти, который используется для WinJS.Binding.List. В массиве sampleItems каждый элемент заполнен либо внутренними данными, либо значениями других переменных. Каждый элемент, кроме того, имеет свойство group, которое берется из массива sampleGroups. К несчастью, этот последний массив имеет почти такие же свойства, как и массив элементов, что может показаться запутанным. Для того, чтобы помочь в чётком различении этих массивов, вот полная структура свойств элемента:

    {
    group : { 
    key, 
    title, 
    subtitle,
    backgroundImage,
    description
    }, title, 
    subtitle, 
    description, 
    content,
    backgroundImage
    }

    Как мы видели в примере группировки ListView ранее, проект на основе шаблона Приложение таблицы использует createGrouped для задания источника данных. Интересно увидеть здесь, что он задаёт изначально пустой список, создаёт сгруппированную проекцию (опуская необязательную функцию сортировки) и затем добавляет элементы, используя метод списка push:

    var list = new WinJS.Binding.List();	
    var groupedItems = list.createGrouped(	
    function groupKeySelector(item) { return item.group.key; },
    function groupDataSelector(item) { return item.group; }	
    );	
    generateSampleData().forEach(function (item) {
    list.push(item);
    });

    Это чётко указывает на динамическую природу списков и ListView: вы можете добавлять и удалять элементы из источника данных, а односторонняя привязка данных позволит сохранять уверенность в том, что ListView соответствующим образом обновится. В подобном случае вам не нужно обновлять макет ListView - это произойдёт автоматически. Я говорю это, так как обычно имеется некоторое непонимание в вопросе использования метода ListView forceLayout, который вам нужно вызывать лишь тогда, как сказано в документации: "когда вы делаете ListView снова видимым после того, как его свойство style.display будет установлено в 'none'". Вы обнаружите, однако, что код из шаблона Приложение таблицы вовсе не использует этот метод.

    В js/data.js имеются и другие полезные функции, такие, как getItemsFromGroup, которая использует WinJS.Binding.List.createFiltered, как мы делали ранее. Другие функции предназначены для организации взаимосвязи между группами и элементами, которая нужна для перемещения между списком элементов, подробностями о группах (подобная страница отображает лишь элементы в определенной группе), и подробностями об элементах. Все эти функции заключены в пространство имен, которое называется Data, в верхней части js/data.js, в итоге, ссылка на всё, что описано в этом файле, должна иметь префикс Data.

    И, имея эти знания, я думаю, вы сможете понять всё, что происходит в приложении, построенном по шаблону Приложение таблицы, для того, чтобы адаптировать его под свои нужды. Просто помните о том, что все данные-образцы, вроде логотипа по умолчанию и изображения экрана-заставки, нужно полностью заменить реальными данными, полученными из других ресурсов, наподобие файла или WinJS.xhr, и их вы можете заключить в WinJS.Binding.List. Некоторые дальнейшие руководства вы можете найти в материале "Создание программы для чтения блогов" (http://msdn.microsoft.com/library/windows/apps/Hh974582.aspx) в Центре разработчиков Windows, и хотя в руководстве используется шаблон проекта Приложение с разделением (Split App), здесь много общего с шаблоном Приложение таблицы, и этот рассказ, на самом деле, применим и к тому и к другому шаблонам.

    Элемент управления Semantic Zoom (семантическое масштабирование)

    С тех пор, как мы загрузили пример "Группировка HTML ListView и семантическое масштабирование" (http://code.msdn.microsoft.com/windowsapps/ListView-grouping-and-6d032cc1), и завершили первый разговор об элементах управления для коллекций, сейчас подходящее время для того, чтобы рассмотреть еще один очень интересный элемент управления WinJS: SemanticZoom (http://msdn.microsoft.com/library/windows/apps/br229690.aspx).

    Семантическое масштабирование позволяет пользователям легко переключаться между двумя режимами просмотра одних и тех же данных, это режим детализированного представления (zoomed-in), который отображает детали, и режим общего представления (zoomed-out), который предоставляет более общую информацию. Основная область использования семантического масштабирования - это длинные списки элементов (особенно - несгруппированных), где пользователю, скорее всего, надоест постоянно прокручивать контент из конца в конец, не взирая даже на то, что довольно забавно водить пальцем по сенсорному экрану. С помощью семантического зуммирования вы можете уменьшить масштаб для того, чтобы увидеть заголовки, категории, или другие формы краткого представления общей информации о данных, и затем, кликнув по одному из отображаемых элементов, вернуться в его раздел или группу. Руководство по дизайну рекомендует поддерживать режим общего представления данных, в котором они занимают от одного до трёх экранов максимум, что делает очень простым просмотр всего набора данных и понимание их особенностей.

    Попробуем в деле семантическое масштабирование из Сценария 2 примера, посвященного группировке HTML ListView и семантическому масштабированию. Для переключения между режимами просмотра, используйте жесты сведения и разведения пальцев (pinch-zoom), сочетания клавиш Ctrl+/Ctrl-, нажатие клавиши Ctrl, сопровождающееся прокруткой колеса мыши, либо маленькие кнопки масштабирования, которые автоматически появляются в правом нижнем углу элемента управления, как показано на Рис. 5.5. Когда вы переходите к режиму общего представления (zoom-out), вы видите отображение заголовков групп, что так же показано на рисунке.

    (рис 5.5) Семантическое масштабирование между двумя режимами просмотра в примере о группировке ListView и применении семантического масштабирования

    Режим детализированного представления (Zoomed in view)

    Переход в режим общего представления (Zoom out)

    Элемент управления масштабированием (наложение)(Zoom control (overlay))

    Режим общего представления (Zoomed out view)

    Прикоснитесь к элементу или выполните команду увеличения масштаба, когда фокус установлен на данном элементе (Tap an item or zoom in with focus on that item)

    Элемент управления довольно просто и понятно использовать. Объявите в разметке элемент управления WinJS, используя конструктор WinJS.UI.SemanticZoom. Внутри элемента вы, затем, объявите два (и только два) дочерних элемента: первый определяет режим детализированного представления, второй - режим общего представления - всегда в таком порядке. Здесь показан пример работы с двумя элементами управления ListView (плюс - шаблон, используемый для режима общего представления; Я показал код в измененном примере, который включен в дополнительные материалы к курсу):

    <div id="semanticZoomTemplate" data-win-control="WinJS.Binding.Template" >	
    <div class="semanticZoomItem">	
    <h2 class="semanticZoomItem-Text" data-win-bind="innerText: groupTitle"></h2>
    </div>	
    </div>	
    
    <div id="semanticZoomDiv" data-win-control="WinJS.UI.SemanticZoom">
    <div id="zoomedInListView" data-win-control="WinJS.UI.ListView"
    data-win-options="{ itemDataSource: myGroupedList.dataSource, itemTemplate: mediumListIconTextTemplate,
    groupDataSource: myGroupedList.groups.dataSource, groupHeaderTemplate: headerTemplate,
    selectionMode: 'none', tapBehavior: 'none', swipeBehavior: 'none' }">
    </div>
    
    <div id="zoomedOutListView" data-win-control="WinJS.UI.ListView"
    data-win-options="{ itemDataSource: myGroupedList.groups.dataSource, itemTemplate: semanticZoomTemplate,
    selectionMode: 'none', tapBehavior: 'invoke', swipeBehavior: 'none' }" >
    </div>
    </div>

    Первый дочерний элемент zoomedInListView, очень похож на ListView из Сценария 1, с заголовками групп и элеметами. Второй элемент zoomedOutListView, использует группы в роли элементов и выводит их с использованием другого шаблона. Элемент управления семантического масштабирования просто перключается между двумя режимами отображения в ответ на соответствующие жесты. Когда масштаб отображения меняется, элемент управления SemanticZoom вызывает событие zoomchanged, у которого значение args.detail равняется true в случае общего представления, и false при детализированном представлении. Вы можете использовать это событие для того, чтобы сделать, для различных режимов отображения, доступными определенными команды панели приложения, например - в режиме общего представления активировать команды для изменения сортировки или фильтрации, результат работы который затем подействует на режим детализированного представления. Панели приложения (app bar) мы рассмотрим в лекции 1 курса "Пользовательский интерфейс приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript".

    Элемент управления имеет некоторые другие свойства, такие, как enableButtonl (логическое значение, управляющее видимостью накладываемых кнопок; значение по умолчанию - true), locked (логическое значение, отключающее изменение масштаба в обоих направлениях, может быть установлено динамически - для блогировки текущего режима отображения; по умолчанию - false), и zoomedOut (логическое, показывает, находится ли элемент в режиме общего представления, таким образом, вы можете инициализировать его подобным способом; по умолчанию - false). У него есть метод forceLayout, который используется в том же случае, что и метод forceLayout элемента управления ListView: а именно, когда вы отключаете стиль display: none.

    Свойство zoomFactor весьма интересно, оно определяет, какая анимация используется при переходе между двумя режимами просмотра. Анимация - это комбинация масштабирования и затухания, которые делают переход в режим общего представления выглядящим как опускание вглубь плоскости экрана или подъем оттуда, в завиимости направления переключения, в то время как переход в режим детального представления может выглядеть, как "утопание" или "поднятие". Если быть более точным, то в режим детализированного просмотра масштабирование производится между 1/zoomFactor и 1, в то время, как значение прозрачности лежит между 0 и 1. Значение zoomFactor по умолчанию - 0.65, что создаёт эффект умеренной силы. Более низкие значения (минимум - 0.2) усиливают эффект, более высокие (максимум - 0.8) ослабляют его.

    Когда особенности стилизации понятны, вы можете сделать большинство из того, что вам нужно, прямо с дочерними элементами SemanticZoom. Тем не менее, стилизуя элемент управления SemanticZoom вы можете переопределить стили в win-semanticzoom (для всего элемента) и win-semanticzoomactive (для активного режима просмотра). Стиль win-semanticzoombutton позволяет вам, если нужно, стилизовать кнопки для управления масштабом.

    Важно понимать, что SemanticZoom предназначен для переключения между двумя режимами отображения одних и тех же данных, а не для переключения между полностью различными наборами данных (смотрите "Руководство по контекстному масштабированию" (http://msdn.microsoft.com/ru-ru/library/windows/apps/hh465319.aspx)). Кроме того, элемент управления не поддерживает вложенность (то есть - многократное изменение масштаба для разных уровней данных). Однако, это не означает, что вы вынуждены использовать элементы управления одного вида и для одного, и для другого режимов просмотра: режим детального просмотра может быть списком, а режим общего просмотра - диаграммой, календарём, или чем угодно другим. Режим общего просмотра, другими словами, это отличное место для показа сводных данных, которые сложно получить в режиме детального просмотра. Например, используя те же модификации кода, которые мы использовали для включения количества элементов в данные групп для Сценария 1 (смотрите "Быстрый старт №2b, выше), нам достаточно лишь внести некоторые дополнения в шаблон элемента для режима общего просмотра (как сделано в модифицированном примере к этой лекции в дополнительных материалах к курсу):

    Кроме того, вам нужно знать, что элемент управления ZemanticZoom не работает с любыми дочерними элементами. Об этом вам может сообщить исключение, вызванное, если элемент не имеет свойства zoomableView. Каждый дочерний элемент должен обеспечивать реализацию интерфейса WinJS.UI.IZoomableView (http://msdn.microsoft.com/library/windows/apps/br229794.aspx) посредством свойства zoomableView. Среди всех встроенных элементов управления HTML и WinJS этому требованию удовлетворяет лишь ListView, и именно поэтому его обычно используют с ZoomView. Однако, вы можете обеспечить данный интерфейс в пользовательском элементе управления, когда объект, возвращаемый конструктором, содержит свойство zoomableView, которое является объектом, содержащим методы интерфейса. Среди этих методов задачи beginZoom (начало масштабирования) и endZoom (завершение масштабирования) вполне ясны, методы getCurrentItem (получить текущий элемент) и setCurrentItem (установить текущий элемент) позволяют элементу управления выполнять масштабирование для правильной группы, когда пользователь коснётся её в режиме общего просмотра.

    Подробности вы можете найти в примере "HTML SemanticZoom для пользовательских элементов управления" (http://code.msdn.microsoft.com/windowsapps/SemanticZoom-for-custom-4749edab), который, кроме того, предоставляет другие образцы использования пользовательских элементов управления.

    Особенности FlipView и его стилизация

    Нам не стоит забывать о скромном элементе управления FlipView, рассматривая ListView - один из самых сложных и богатых возможностями элементов управления WinJS. Поэтому, прежде чем мы продолжим углубляться в его особенности, посвятим несколько страниц описанию FlipView, через рассмотрение сценариев его использования, показанных в примере "Элемент управления FlipView" (http://code.msdn.microsoft.com/windowsapps/FlipView-control-sample-18e434b4). Так же важно отметить, что хотя этот пример показыват возможности элемента управления в сравнительно малой области, FlipView может иметь любой физический размер, даже занимать большую часть экрана. Обычное использование этого элемента управления, на самом деле, позволяет пользователю просматривать полноразмерные изображения в фотогалерее, переходя между ними путём пролистывания. Конечно, FlipView можно использовать везде, где он может понадобиться, в большом или малом размере. Смотрите материал "Руководство по элементу управления FlipView" (http://msdn.microsoft.com/library/windows/apps/hh850405) для того, чтобы узнать, как им лучше пользоваться.

    Во всяком случае, Сценарий 2 в примере ("Ориентация и расстояние между элементами") демонстрирует свойство orientation этого элемента управления. Оно определяет местоположение элементов управления со стрелкой: слева и справа (horizontal), или сверху и снизу (vertical), как показано ниже. Кроме того, оно определяет начальные и конечные анимации элемента, и то, использует ли элемент управления клавиши вправо/влево или вверх/вниз для навигации с помощью клавиатуры. Данный сценарий так же позволит вам установить свойство itemspacing, которое задаёт размер свободного места между элементами, когда вы перематываете их, используя сенсорные жесты (ниже справа) Эти эффекты невидны при использовании клавиатуры или мыши для тех же целей. Для того, чтобы их увидеть, вам понадобится использовать эмуляцию сенсорного дисплея в имитаторе Visual Studio для перемещения между элементами.

    Сценарий 3 ("Использование интерактивного содержимого") показывает использование функции шаблона (template function) вместо декларативно описанного шаблона. Больше об этом мы поговорим в разделе "Как, на самом деле, работают шаблоны" ниже в этой лекции, но, говоря простым языком, функция шаблона или визуализатор (renderer) создаёт элементы и устанавливает их свойства программно, делая то, что обычно выполняет WinJS.Binding.Template на основе разметки шаблона, которую вы ему предоставляете. Это позволяет вам выводить элементы по-разному (то есть, создавать разные элементы классов настройки стилей) в зависимости от их реальных данных. В Сценарии 3, источник данных содержит "оглавление" элементов в начале, для которого визуализатор (функция, которая называется myTemplate в js/interactiveContent.js) создаёт полностью различные элементы:

    Сценарий так же задаёт прослушиватель события click в записях "оглавления", обработчик которого переходит на соответствующий элемент, уставливая свойство currentPage элемента управления FlipView. Элемент-изображение имеет обратную ссылку на "оглавление". Посмотрите функцию clickHandler в коде для того, чтобы увидеть оба этих действия.

    Сценарий 4 ("Создание контекстного элемента управления") показывает добавление навигационных элементов управления путём их наложения на каждый из элементов:

    Элементы выводятся с использованием декларативного шаблона, который в данном случае содержит элемент-заполнитель для навигационых элементов управления (html/contextControl.html), div, который называется ContextContainer.

    <div>	
    <div id="contextControl_FlipView" class="flipView" data-win-control="WinJS.UI.FlipView"
    data-win-options="{ itemDataSource: DefaultData.bindingList.dataSource,	
    itemTemplate: contextControl_ItemTemplate }">	
    </div>	
    <div id="ContextContainer"></div>	
    </div>

    Когда элемент управления инициализируется в методе processed в js/contextControl.js, в примере вызывается асинхронный метод FlipView count. Обработчик завершения, countRetrieved, затем создаёт навигационные элементы управления, используя строку стилизованных переключателей. Обработчик onpropertychange для каждого переключателя, затем, устанавливает свойство currentPage FlipView.

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

    Сценарий 5 ("Стилизация навигационных кнопок") показывает особенности стилизации FlipView, включающую в себя различные win-* стили и псевдо-классы, как показано здесь:

    Используется для границ, отбивок и так далее (Use for margins, padding, etc.)

    Для фона, если расстояние между элементами больше 0, задаёт стиль конкретного элемента управления (For background with item spacing > 0, style the specific control).

    Если вы, в шаблоне, используете собственные навигационные кнопки (подключённые к методам next и previous), скройте кнопки по умолчанию, добавив display.none к правилу стиля <control selector> .win-navbutton.

    И, наконец, есть еще несколько методов и событий элемента управления FlipView, которые не используются в примере:

  • pageCompleted - это событие, которое вызывается, когда переход к новому элементу полностью завершен (то есть, новый элемент визуализирован). В противоположность этому, вышеупомянутое событие pageselected вызывается, когда анимируется элемент-заполнитель (placeholder) (не полностью визуализированный). Смотрите "Функции шаблонов (Часть 2)" в конце этой лекции.
  • datasourcecountchanged - это событие, вызываемое по вполне понятной причине (изменение количества элементов в источнике данных), что-то похожее Сценарий 4 использует для обновления элементов управления навигации, если элементы добавляют в источник данных или убирают из него.
  • next и previous - это методы для перемещения между элементами (наподобие currentPage), которые полезны, если вы реализуете собственные навигационные кнопки.
  • forceLayout - это метод, который вызывается, когда вы делаете элемент управления FlipView, убирая стиль display: none. (В примере этот метод вызывается, когда вы переключаетесь между сценариями, однако, в этом нет необходимости, так как здесь никогда не меняется стиль).
  • setCustomAnimations позволяет вам контролировать анимации, используемые при пролистывании вперед, назад, и при переходе на произвольный элемент.
  • Для того, чтобы узнать об этом подробности, обратитесь к документации по WinJS.UI.FlipView (http://msdn.microsoft.com/library/windows/apps/br211711.aspx).

    Источники данных

    Во всех случаях, которые мы рассмотрели, мы использовали источники данных, построенные на основе WinJS.Binding.List и находящиеся в памяти. Очевидно, существуют и другие типы источников данных, и их не обязательно сразу же загружать в память. Как же работать с такими источниками?

    WinJS предоставляет некоторую помощь в этой области. Первое средство - это объект WinJS.UI.StorageDataSource, который работает с файлами в файловой системе, как в следующем разделе, в демонстрации работы FlipView с библиотекой изображений. Следующее средство - это WinJS.UI.VirtualizedDataSource, которое подразумевает использование базового класса для пользовательских источников данных, это расширенный сценарий, которого мы коснёмся лишь кратко.

    FlipView и библиотека изображений

    Всё, что мы видели в примере, касающемся FlipView, ведет нас к совершенно очевидной возможности: перемещаться по файлам изображений в папке. Используя то, что мы узнали, как нам реализовать это? У нас уже есть шаблон элемента, который содержит тег img, таким образом, возможно, нам лишь нужны некоторые URI для таких файлов. Возможно, нам следует создать их массив, используя API наподобие Windows.Storage.KnownFolders.picturesLibrary.getFilesAsync (конечно, объявив возможность Библиотека изображений (Pictures Library) в манифесте). Это даст нам множество объектов StrorageFile, для которых мы можем вызвать URL.createObjectURL. Найденные URI мы можем сохранить в массиве и затем поместить их в WinJS.Binding.List:

    var myFlipView = document.getElementById("pictures_FlipView").winControl;
    
    Windows.Storage.KnownFolders.picturesLibrary.getFilesAsync()
    .done(function (files) {
    var pixURLs = [];
    
    files.forEach(function (item) {
    var url = URL.createObjectURL(item, {oneTimeOnly: true });
    pixURLs.push({type: "item", title: item.name, picture: url });
    });
    
    var pixList = new WinJS.Binding.List(pixURLs);
    myFlipView.itemDataSource = pixList.dataSource;
    });

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

    Лучший подход заключается в использовании WinJS.UI.StorageDataSource (http://msdn.microsoft.com/library/windows/apps/br212650.aspx), который работает напрямую с файловой системой, вместо того, чтобы работать с массивом, расположенным в памяти. Я реализовал это в виде Сценария 8 в измененном примере использования FlipView в дополнительных материалах к этой лекции (Другие материалы можно найти в примере "Использование StorageDataSource и GetVirtualizedFilesVector" (http://code.msdn.microsoft.com/windowsapps/Data-source-adapter-sample-3d32e535)). Здесь мы можем использовать короткое имя для того, чтобы получить источник данных из библиотеки изображений:

    myFlipView.itemDataSource = new WinJS.UI.StorageDataSource("Pictures");

    "Pictures" это короткое имя, так как первый аргумент StorageSource, это файловый запрос, который исходит из API Windows.Storage.Search, подробнее об этом мы поговорим в лекции 2 курса "Пользовательский интерфейс приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript". Эти запросы, которые попадают в мощную функцию Windows.Storage.StorageFolder.createFileQueryWithOptions (http://msdn.microsoft.com/library/windows/apps/br211591.aspx) - это способы для перебора файлов в папках вместе с метаданными - такими, как обложки альбомов, подробности о записях, и эскизы, которые кадрированы так, чтобы сохранить соотношение сторон. Короткие имена, наподобие"Pictures" (а так же "Music", "Documents", "Videos" которые нуждаются в соответствующих возможностях, объявленных в манифесте) лишь создают типичные запросы для подобных библиотек документов.

    Нужно отметить, что StorageDataSource не поддерживает напрямую одностороннюю привязку данных, поэтому вы получите исключение, если вы попытаетесь сослаться на элемент напрямую в шаблоне. Чтобы это обойти, нужно явно использовать функцию-инициализатор WinJS.Binding.oneTime для каждого свойства:

    <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>

    В случае со свойством img.src, файловый запрос возвращает нам элементы типа Windows.Storage.BulkAccess.FileInformation (http://msdn.microsoft.com/library/windows/apps/windows.storage.bulkaccess.fileinformation.aspx) (переменная source в коде ниже), которая содержит эскизы изображений, а не URI. Для того чтобы конвертировать эти данные об изображениях в URI, нам нужно использовать свой собственный инициализатор привязки:

    WinJS.Namespace.define("InitFunctions", {	
    thumbURL: WinJS.Binding.initializer(function (source, sourceProp, dest, destProp) {
    if (source.thumbnail) {	
    dest.src = URL.createObjectURL(source.thumbnail, { oneTimeOnly: true });	
    }	
    })	
    });

    В этом инициализаторе часть data-win-bind src : thumbnail, на самом деле, игнорируется, так как мы устанавливаем свойство изображения src напрямую в значение source.thumbnail. Это - лишь форма односторонней привязки данных.

    Обратите внимание на то, что эскизы не всегда сразу доступны в объекте FileInformation, именно поэтому мы проверяем, действительно ли у нас имеется эскиз, прежде чем создаём URI для него. Это означает, что быстрое перелистывание изображений может показать пользователю пустые места для изображений. Для того, чтобы справиться с этим, мы можем прослушивать событие FileInformation.onthumbnailupdated и обновлять элементы в это время. Лучший способ добиться этого заключается в использовании вспомогательного метода StorageDataSource.loadThumbnail (лекции 3).

    Вы можете использовать этот метод внутри инициализатора привязки, как показано в Сценарии 1 вышеупомянутого примера "Использование StorageDataSource и GetVirtualizedFilesVector" (http://code.msdn.microsoft.com/windowsapps/Data-source-adapter-sample-3d32e535), или внутри функции рендеринга, которая имеется в декларативном шаблоне. Мы сделаем это для нашего примера использования FlipView позже, в разделе "Как, на самом деле, работают шаблоны", что, кроме того, позволит нам избежать трюков с односторонней привязкой данных.

    В качестве последнего замечания, Сценарий 6 в примере работы с FlipView, содержит некоторые образцы использования различных источников данных, особенно те, которые работают со службой поиска Bing. Поэтому, взглянем на пользовательские источники данных.

    Пользовательские источники данных

    Теперь, когда мы видели элемент управления для коллекций, наподобие FlipView, который работал с двумя разными источниками данных, вы, вероятно, начинаете догадываться о том, что все источники данных имеют некоторые общие характеристики и общий программный интерфейс. Это снова продемонстрировано в Сценарии 6 примера использования FlipView, так же, как и в примере "Работа HTML ListView с источниками данных" (http://code.msdn.microsoft.com/windowsapps/ListView-custom-data-4dcfb128), показанного на Рис. 5.6, который мы рассмотрим в этом разделе.

    (рис 5.6) Пример работы HTML ListView с источниками данных

    Сценарии 2 и 3 этого примера работают на основе источника данных WinJS.Binding.List, как мы уже видели, и предоставляют кнопки для управления этим источником данных. Эти изменения отражаются на выходных данных. Разница между двумя сценариями заключается в том, что Сценарий 2 манипулирует данными посредством методов WinJS.Binding.List наподобие move, в то время как Сценарий 3 манипулирует источником данных, на котором он основан, посредством более общего API ListDataSource (http://msdn.microsoft.com/library/windows/apps/br211786.aspx).

    Из-за привязки данных, изменения в данных отражаются на элементе управления ListView в любом случае, но есть три важных различия. Первое, интерфейс ListDataSource - обычный для всех источников данных, в итоге любой код, написанный с его использованием, будет работать для любого источника данных. Второе - его методы обычно асинхронны, так как источник данных может быть подключён к онлайновому сервису или другому подобному ресурсу. Третье - ListDataSource обеспечивает вызов beginEdits для пакетного изменения данных, что подавляет любые сообщения об изменениях, направленные внешним присоединённым объектам, до вызова endEdits. Это позволяет вам выполнять изменение больших объемов данных способом, который может улучшить производительность ListView.

    Сценарии 1 и 4 в примере показывают, как можно создать пользовательский источник данных. Сценарий 1 создаёт источник данных для поиска Bing. Сценарий 4 создаёт один источник в виде массива в памяти, который вы можете приспособить для работы с некоторыми полученными извне данными, которые поступают от некоего сервиса небольшими порциями. Важно здесь то, что при всех этих подходах реализуется то, что называется адаптером обработки данных (data adapter), который является объектом с методами интерфейса WinJS.UI.IListDataAdapter (http://msdn.microsoft.com/library/windows/apps/br212603.aspx). Это обеспечивает такие возможности, как кэширование, виртуализация, обнаружение изменений и так далее. К счастью, вы получаете большинство из этих методов, наследуя ваш класс от WinJS.UI.VirtualizedDataSource (http://msdn.microsoft.com/library/windows/apps/hh701413.aspx) и затем реализуя те методы, которые вам нужно настроить. В примере, например, bindingImageDataSource определен так, как показано ниже (смотрите js/BingImageSearchDataSource.js):

    bingImageSearchDataSource = WinJS.Class.derive(WinJS.UI.VirtualizedDataSource, function (devkey, query) {
    this._baseDataSourceConstructor(new bingImageSearchDataAdapter(devkey, query));
    });

    Где класс bingImageSearchDataAdapter реализует напрямую лишь методы getCount и itemsFromIndex.

    Для получения более глубоких знаний по данному вопросу, выходящих за пределы этого примера, я отсылаю вас к сессии конференции Build 2011 года: "APP210-T: Построение коллекций, управляемых данными и приложений со списками с использованием ListView в HTML5" (http://channel9.msdn.com/Events/BUILD/BUILD2011/APP-210T). Кое-что с тех пор изменилось (так, ArrayDataSource теперь WinJS.Binding.List), но в целом здесь хорошо разъяснены механизмы. Кроме того, полезно помнить, что вы так же можете использовать другие языки, наподобие C# или C++ для того, чтобы описать пользовательский источник данных. Подобные языки могут предложить более высокую производительность внутри источника данных и позволят получить доступ к более производительным API, нежели те, к которым даёт доступ JavaScript.

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

    Ранее, когда мы рассматривали шаблон Приложение таблицы, я упоминал, что вы можете использовать функцию вместо декларативного описания шаблона для свойств наподобие 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. Эта функция должна быть отмечена для обработки, как обсуждалось в лекции 4, так как она участвует в 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-объекты!" в конце этой лекции.

    Особенности и стилизация ListView

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

    Когда ListView - это неправильный выбор?

    ListView - это один из богатейших по возможностей элементов управления во всей Windows. Он очень мощный, гибкий, и, как мы уже знаем, весьма глубокий и сложный. Но по всем этим причинам, иногда, он является не лучшим выбором! В зависимости от дизайна, может быть проще использовать обычный HTML/CSS макет

    Концептуально ListView определяется путём задания взаимоотношений трех частей: источника данных, шаблонов и макета. То есть, объекты из источника данных, которые могут быть сгруппированы, отсортированы и отфильтрованы, выводятся с использованеим шаблонов и организуются с помощью макета (обычно - с помощью групп и заголовков групп). В подобном определении, ListView предназначен для того, чтобы помочь в визуализации коллекций похожих и/или взаимосвязанных объектов, где группировка тоже подразумевает взаимоотношения определенного рода.

    Имея это в виду, можно признать следующие факторы убедительно свидетельствующими о том, что ListView - это хороший выбор для вывода некоторой коллекции:

  • Коллекция может содержать различное чисто объектов для вывода, возможно, очень большое количество, которых больше, чем приложение, запущенное на большом экране, способно отобразить в одном окне.
  • Имеет значение организация и реорганизация элементов в различных группах.
  • Заголовки групп поомогают уточнить общие свойства элементов в группах, и их можно использовать для навигации к странице детальной информации о группе.
  • Имеет значение сортировка или/и фильтрация элементов в соответствии с различными условиями.
  • Различная группировка элементов и информации об этих группах предлагает способы работы, в которых опыт взаимодействия пользователя и программы способно улучшить семантическое масштабирование.
  • Группы имеют некоторые общие черты, в том смысле, что каждая из них связана с похожими объектами. Различные имена мест, например, сходны; лента новостей, список друзей и календарь праздников не схожи.
  • Элементы поддерживают индивидуальное или групповое выделение, таким образом, что команды на панели приложения позволяют выполнять с ними какие-то действия.
  • С другой стороны, противоположные факторы говорят о том, что ListView не является правильным выбором:

  • Коллекция содержит ограниченное или фиксированное количество элементов, или это совсем не коллекция взаимосвязанных элементов.
  • Неважна возможность реорганизации группировки объектов, фильтрация или сортировка элементов.
  • Вам не нужны заголовки групп.
  • Вы не нуждаетесь в применении семантического масштабирования.
  • Группы весьма различны - то есть, группам не имеет смысла находиться бок о бок при отсутствии заголовков.
  • Позвольте мне пояснить, что я не говорю о дизайне - ваш дизайнер может передать вам любой макет, который он хочет, и, как разработчик, именно вы должны решить, как реализовать его! Я говорю о том, как вы выбираете подход для подобной реализации, с использованием ли элементов управления наподобие ListView, или с применением обычного HTML/CSS макета.

    Я говорю об этом, так как в работе с разработчиками, которые создают самые первые приложения для Магазина Windows, мы часто видим, как они пытаются использовать ListView в ситуации, где он просто не нужен. Домашняя страница приложения, например, может содержать комбинацию из лент новостей, списков друзей и календаря. Страница детальной информации об элементе может включать в себя изображение, текстовое описание и медиа-библиотеку. В обоих случаях, страница содержит ограниченное количество разделов и разделы содержат очень разное содержимое, о котором можно сказать, что оно не имеет межгрупповых связей. Поэтому, использование ListView более сложно, чем простое использование одного div, поддерживающего прокрутку с CSS-сеткой, в которой вы можете разместить любые разделы, которые вам нужны.

    Внутри данных разделов, конечно, вы можете использовать элементы управления ListView для отображения коллекций элементов, но если говорить обо всей странице, то обычный макет, основанный на div - это всё, что вам нужно. Я показал подобный выбор на Рис. 5.7, используя изображение из материала "Проектирование навигации для приложений Магазина Windows" (http://msdn.microsoft.com/library/windows/apps/hh761500), так как вы, возможно, получите похожие изображения от вашего дизайнера. За исключением навигационных стрелок, хаб приложения и страницы детальной информации обычно используют div в качестве корневого элемента, в то время как страница раздела обычно представляет собой ListView. Внутри корневого узла приложения и страниц детальной информации могут быть элементы управления ListView, но там, где нужно отобразить фиксированное содержимое (вроде отдельного элемента), лучше подойдёт div.

    (рис 5.7) Разбиение типичного дизайна Хаб-Раздел-Сведения (Hub-Section-Details), спроектированного с применением элементов div и элементов управления ListView

    Корневой раздел приложения (хаб): страница - это div; разделы в сетке - это либо div'ы, либо элементы управления ListView; если первый раздел содержит различные элементы, ей следует содержать ListView (Hub page: page is a div; sections in a grid are either divs or ListViews; if the first section has variable items, it could be a ListView).

    Элемент div с макетом (div with layout)

    Страница раздела: тело страницы (исключая заголовок) - это один ListView (Section page: the body of the page (excluding a page header) is a single ListView)

    Страница детальной информации (сведений): страница - это div; разделы в сетке - либо элементы div, либо - элементы управления ListView (Detail page: page is a div; sections in a grid are either divs or ListViews)

    Подсказывает о том, что вы идете по неверному пути, кстати, тот факт, что вы пытаетесь скомбинировать несколько коллекций несвязанных данных в единый источник данных, привязывая этот источник к ListView и реализуя функцию рендеринга для того, чтобы снова разделить эти данные, чтобы всё вывелось верно. Всей этой дополнительной работы можно избежать, просто используя макет HTML/CSS.

    Для того, чтобы узнать больше о проектировании ListView, обратитесь к материалу: "Руководство и контрольный список для элементов управления ListView" (http://msdn.microsoft.com/library/windows/apps/hh465465.aspx).

    Параметры, выделения и методы отдельного элемента

    В предыдущем разделе мы уже видели некоторые параметры, которые вы можете использовать при создании ListView, параметры, которые связаны со свойствами элемента управления и доступны, кроме того, из JavaScript. Посмотрим на полный список свойств, методов и событий, которые я организовал в несколько групп, в конце концов, эти свойства и методы превратились во что-то вроде коллекции! Подробности о них вы можете найти на странице WinJS.UI.ListView (http://msdn.microsoft.com/library/windows/apps/br211837.aspx), самое важное сейчас - понять как взаимосвязаны члены данных групп:

  • Адресация элементов. Свойство currentItem позволяет получить или задать элемент, имеющий фокус, а методы elementFromIndex и indexOfElement позволяют установить взаимосвязь между элементами DOM и их индексами. Последний может быть полезен, если у вас есть другие элементы управления в шаблоне элемента, и вам нужно определить окружающий элемент в обработчике события.
  • Видимость элементов. Свойства indexOfFirstVisible и indexOfLastVisible позволяют узнать индексы видимых элементов, или они могут быть использованы для прокрутки ListView к заданному элементу. Метод ensureVisible приводит к отображению заданного элемента, если он загружен. Здесь имеется свойство scrollPosition, которое содержит дистанцию в пикселях между первым элементом в списке и текущей отображаемой областью. Последством этого свойства вы можете задать позицию прокрутки ListView, это работает только в том случае, если значение loadingState (смотрите группу "Состояние загрузки" ниже") установлено в ready, в противном случае ListView может еще не знать своих истинных размеров. Рекомендовано, вместо этого, использовать ensureVisible или indexOfFirstVisible для управления позицией прокрутки.
  • Активизация элементов. Событие itemInvoked, как мы уже видели, вызывается, когда пользователь касается элемента, если только свойство tapBehavior не установлено в none - в подобном случае активации не происходит. Другое значение tapBehavior из перечисления WinJS.UI.tapBehavior (http://msdn.microsoft.com/library/windows/apps/hh701303.aspx) всегда приводит к вызову данного события, но определяет, как касание влияет на выделение. Заметьте, что вы можете переопределить поведение при выделении для каждого элемента в отдельности, используя событие selectionchanging и подавляя анимацию, если это нужно. Смотрите врезку "Поведение при щелчке и прикосновении" ниже.
  • Выделение элементов. Свойство selectionMode содержит значение из перечисления WinJS.UI.selectionMode (http://msdn.microsoft.com/library/windows/apps/br229687.aspx), показывающее режим выделения - одиночный, множественный или запрещающее выделение. Во всех случаях свойство selection содержит объект ListViewItems (http://msdn.microsoft.com/library/windows/apps/br211809.aspx), методы которого позволяют вам получать список выделенных элементов и управлять ими (например, настраивать выделенные элементы, используя их метод set). Изменения в выделении вызывают события selectionchanging и selectionchanged. В случае с selectionchanging, его свойство args.detail.newSelection содержит вновь выделенные элементы. Больше об этом можно узнать в примере "Настройка интерактивного поведения HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-selection-detail-95e06ade).
  • Прокрутка. Свойство swipeBehavior, связанное с выделением элемента, содержит значение из перечисления WinJS.UI.SwipeBehavior (http://msdn.microsoft.com/library/windows/apps/hh701287.aspx). "Прокрутка" (swiping) или "скольжение по диагонали" (cross-slide) это сенсорный жест на элементе для его выделения, когда жест производится перпендикулярно направлению прокрутки списка. Если данное свойство установлено в none, прокрутка не воздействует на элемент и жест маршрутизируется к родительским элементам, позволяя осуществлять прокрутку вертикально ориентированного ListView или страницы, на которой он расположен. Если это свойство установлено в select, жест обрабатывается элеменом и приводит к его выделению.
  • Источники данных и шаблоны. Мы уже видели свойства groupDataSource, groupHeaderTemplate, itemDataSource, и itemTemplate. Два связанных свойства - это resetGroupHeader и resetItem, которые содержат функции, которые ListView может вызывать при повторном использовании элементов. Это разъясняется в разделе "Функции шаблонов (Часть 2)"
  • Макет. Как мы уже видели, свойство layout (и объект) описывают организацию элементов в ListView, о чём больше сказано в разделе "Макеты и объединение ячеек" ниже. Мы, кроме того, рассматривали функцию forceLayout, которая используется, когда из ListView удаляется стиль display: none и элемент управления нуждается в повторной визуализации.
  • Поведение при загрузке. Как будет обяснено в разделе "Оптимизация производительности ListView" ниже, эта группа определяет, как ListView загружает страницы элементов (то, почему ensureVisible не всегда работает, если страница не загружена). Когда свойство loadingBehavior установлено в "randomaccess" (по умолчанию), полоса прокрутки ListView отражает общее число элементов только на пяти полных страницах элементов (максимум - 1000), хранящихся в памяти в любое время, когда пользователь перемещается по содержимому. (Пять страниц - это текущая страница и по две буферизованных страницы позади текущей и перед ней). Другое значение, "incremental", подразумевает изначальную загрузку некоторого количества страниц и затем, загрузку дополнительных страниц, когда пользователь прокрутит список до конца (сохраняя, после этого, все страницы в памяти). Инкрементная загрузка работает со свойствами automaticallyLoadPages, pagesToLoad, и pagesToLoadThreshold, вместе с методом loadMorePages, как мы увидим далее.
  • Состояние загрузки. Свойство только для чтения loadingState содержит либо "itemsLoading" (список запрашивает элементы и заголовки из источника данных), либо "viewportLoaded" (все элементы и заголовки, которые видимы, были загружены), либо "itemsLoaded" (все оставшиеся невидимые буферизованные элементы загружены), или "complete" (все элементы загружены, содержимое в шаблонах выведено и анимация завершена). Когда это свойство изменяется, что обычно происходит, когда ListView нуждается в обновлении макета при прокрутке, вызывается событие loadingStateChange.
  • Разное. Стандартные методы DOM addEventListener, removeEventListener, и dispatchEvent служат для обработки и вызова событий. Они могут быть использованы с любыми событиями, которые поддерживает ListView, в том числе contentanimating, которое возникает при исполнении элементом управления анимации при появлении или смене элементов, что позволяет вам либо предотвращать, либо задерживать подобные анимации. Свойство zoomableView содержит реализацию IZoomableView, которая нужна для контекстного масштабирования (приложения никогда не манипулируют данным свойством).
  • Врезка: Поведение при щелчке и прикосновении

    Когда вы прикасаетесь к элементу в ListView или щёлкаете по нему мышью, при том, что свойство tapBehavior установлено во что-то кроме none, происходит небольшая анимация, путём примерно 97% масштабирования для подтверждения данного действия. Если в списке есть некоторые элементы, которые не могут быть активированы (такие, как некоторые группы или те, которые вы показываете как деактивированные, так как данные, лежащие в их основе, пока не доступны), они продолжают показывать эту анимацию, так как установка tapBehavior применяется к элементу управления в целом. Для того чтобы отключить анимацию для любого конкретного объекта, вы можете добавить класс win-interactive к его элементу внутри функции рендеринга, что является способом указания на то, что объект самостоятельно обрабатывает события щелчка или прикосновения, даже если не делает ничего, кроме их приёма. Если позже элемент можно будет активировать, вы можете, конечно, убрать этот класс.

    Если вам нужно предотвратить выделение элемента, добавьте обработчик для события ListView selectionchanging и вызовите его метод args.detail.preventtapBehavior. Это работает для всех методов выделения, включая сенсорный жест прокрутки, щелчок мыши и нажатие на клавишу клавиатуры Enter.

    Стилизация

    После того, как мы коснулись этого в лекции 4 и в последнем разделе, посвященном ListView, стилизацию лучше рассматривать на иллюстрациях, как на Рис. 5.8, где я применил некоторые яркие CSS-стили к некоторым из стилей win-* так, чтобы они выделялись. Я рекомендую вам взглянуть на материал "Стилизация ListView и его элементов" (http://msdn.microsoft.com/library/windows/apps/hh850406.aspx) в документации, где детализированы некоторые дополнительные стили, не показанные здесь

    (рис 5.8) Классы стиля, использованные элементом управления ListView

    Весь элемент управления (entire control)

    Зона, где не осуществляется прокрутка (non-scrolling area)

    Зона вокруг элемента, на самом деле, лишь для стилизации полей и прозрачности (the area around an item really only to style margin and transparency)

    Зона внутри элемента (the area inside an item)

    Зона, где осуществляется прокрутка (the scrollable area)

    Некоторые замечания о стилизации

  • Помните, что в этом деле Blend - ваш лучший друг!
  • Как и в случае со стилизацией FlipView, класс наподобие win-listview наиболее полезен для стилей полей и отбивок, в то время как свойства, вроде цвета фона, на самом деле, не отображаются (в отличие от win-viewport и win-surface).
  • Фон, не поддерживающий прокрутку, задаваемый win-viewport, используется редко, возможно, из-за неподвижного фонового изображения. Стиль win-surface позволяет настроить прокручиваемую фоновую область.
  • win-container существует, преимущественно, ради двух целей. Первая - это создавать свободное пространство между элементами, используя стили margin, и второй - переназначать фоновый цвет, назначенный по умолчанию, часто делая фон прозрачным. Таким образом, фон, задаваемый win-surface или win-viewport виден через него. Обратите внимание, что если вы устанавливаете стиль padding вместо margin, вы создаёте области, по которым пользователь будет перемещаться как по элементам, которые и активируются как элементы. Это не очень хорошо. Поэтому всегда используйте margin для создания свободного пространства между элементами.
  • Хотя win-item перечислен как стиль, его использовать не рекомендуется, он может быть удалён в будущем: просто напрямую стилизуйте шаблон элемента.
  • Документация указывает на то, что стили вроде win-container и win-surface могут быть использованы несколькими элементами управления WinJS. (FlipView использует несколько таких). Если вы хотите переопределить стили для ListView, убедитесь в том, что их область видимости соответствует другим классам, наподобие .win-listview или конкретным id или классам элемента управления.
  • Высота ListView по умолчанию равна 400 пикселям, и элемент управления не подгоняет свой размер автоматически под размер содержимого. Вы практически всегда будете нуждаться в переопределении данного стиля в CSS или устновки его в JavaScript, если вы знаете, какое пространство должен занимать ListView, как показано в лекции 6, "Макет".
  • Стили, не показанные на рисунке, но описанные в материале "Стилизация ListView и его элементов" (http://msdn.microsoft.com/library/windows/apps/hh850406.aspx), включают win-focusedoutline, win-selection, win-selected, win-selectionborder, win-selectionbackground, и win-selectionhint. Кроме того, имеется класс win-selectionstylefilled, который вы можете добавить к элементу для использования заполненного (filled) стиля выделения вместо стиля с границей (bordered), который применяется по умолчанию, как показано здесь:
  • Заставки

    Есть еще один визуальный элемент ListView, который похож на стилизацию, но стилизация на него не воздействует. Его называют заставкой (backdrop), это эффект, который включен по умолчанию при использовании gridLayout. На аппаратном обеспечении невысокой производительности, особенно на мобильных устройствах, быстрая прокрутка содержимого ListView может легко опередить возможности элемента управления по загрузке и выводу элементов. Для того, чтобы пользователь видел результат своих действий, gridLayout показывает обычные заставки для элементов, основанные на размерах элементов по умолчанию и прокручивает их до момента вывода элементов. Как мы увидим в следующем разделе, вы можете выключить эту возможность с помощью свойства gridLayout disableBackgdrop и переопределить его серый цвет, применяемый по умолчанию, с помощью свойства backdropColor.

    Макеты и объединение ячеек

    Свойство layout элемента управления ListView, которое вы можете задать в любое время, содержит обект, который используется для организации элементов списка. WinJS предоставляет два предустановленных макета: WinJS.UI.GridLayout и WinJS.UI.ListLayout. Первый, уже описанный ранее, обеспечивает горизонтальную прокрутку двумерного макета, который располагает элементы в столбцах (сверху вниз) и затем в строках (слева направо). Второй - это одномерный макет, располагающий элементы сверху вниз, подходит для вертикальных списков (как в прикрепленном режиме просмотра). И тот и другой следуют рекомендованным для представления коллекций подходам к дизайну.

    Говоря техническим языком, свойство layout - это объект, содержащий некоторое количество других параметров вместе со свойством type. Обычно вы видите синтаксис layout: { type: <layout> } в строке data-win-options ListView, где <layout> - это WinJS.UI.GridLayout или WinJS.UI.ListLayout (технически - имя функции-конструктора). При декларативном использовании, layout может так же содержать особенные параметры, зависящие от его типа. Например, следующий код конфигурирует gridLayout, содержащий заголовки слева и четыре строки:

    layout: { type: WinJS.UI.GridLayout, groupHeaderPosition: 'left', maxRows: 4 }

    Если вы создаете объект макета в JavaScript, используя new для вызова конструктора напрямую (и присваивая её свойству layout), вы можете задать дополнительные параметры в конструкторе. Это исполняется в приложении, построенном по шаблону Приложение таблицы в методе initializeLayout в файле pages/groupedItems/groupedItems.js:

    listView.layout = new ui.GridLayout({ groupHeaderPosition: "top" });

    Кроме того, вы можете задать свойства объекта layout элемента управления ListView в JavaScript, при его создании, если хотите воспользоваться подобным подходом. Обычно изменение свойств обновляет макет.

    В любом случае, каждый макет имеет собственные уникальные параметры. Для gridLayout это следующие:

  • groupHeaderPosition управляет расположением заголовков по отношению к их группам; может быть установлен в значения "left" или "top".
  • maxRows управляет количеством элементов, которые макет расположит вертикально, прежде чем начнёт располагать их в следующем столбце.
  • backdropColor предназначен для настройки цвета заставки по умолчанию (смотрите "Заставки" в предыдущем разделе), и disableBackdrops полностью отключает данный эффект.
  • groupInfo определяет функцию, которая возвращает объект, свойства которого показывают, следует ли использовать объединение ячеек и размер ячеек (смотрите ниже). Данный вызов осуществляется лишь однажды при обработке макета.
  • itemInfo определяет функцию для использования вместе с объединением ячеек, которая возвращает объект, свойства которого описывают точный размер для каждого элемента, и то, следует ли разместить элемент в новой колонке (смотрите ниже).
  • gridLayout так же имеет свойство только для чтения, которое называется horizontal и всегда имеет значение true. Для ListLayout свойство horizontal всегда false и не имеет других настраиваемых параметров.

    Теперь, так как свойство layout ListView - это лишь объект (или имя конструктора для подобного объекта), можете ли вы создать собственную функцию, определяющую макет? Да, вы можете: создайте класс, который обеспечивет те же самые открытые методы, что и встроенные макеты, как описано в WinJS.UI.Layout (http://msdn.microsoft.com/library/windows/apps/br211781.aspx) (данная тема пока недостаточно документирована). Здесь объект макета может предоставлять любые другие параметры (свойства и методы), которые к нему применимы.

    Сейчас, прежде чем вы начали размышлять над тем, нужен ли вам макет собственной разработки, хочу отметить, что gridLayout предоставляет кое-что, называемое объединением ячеек (cell spanning), что позволяет вам создавать элементы различных размеров (для ListLayout это неприменимо). Именно для этого существуют свойства groupInfo и itemInfo, как показано в Сценариях 4 и 5 примера "Шаблоны элемента для HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-item-templates-7d74826f), как показано на Рис. 5.9.

    (рис 5.9) Пример шаблонов элемента ListView показывает использование элементов различных размеров с использованием объединения ячеек

    Основная идея объединения ячеек - это задать ячейку для gridLayout, основываясь на размере наименьшего элемента (включая стили отбивки (padding) и полей(margin)). Для лучшей производительности делайте ячейки настолько большими, насколько это возможно, чтобы размер любого другого элемента в ListView был кратен размеру этого элемента.

    Включается возможность объединения ячеек посредством свойства groupInfo gridLayout. Это функция, которая возвращает объект с треямя свойствами: enableCellsSpanning, которое следует установить в true, cellWidth и cellHeight, которые содержат размеры минимальной ячейки в пикселях (что, кстати, использует возможность gridLayout для вывода заставки в подобной ситуации). В примере (смотрите js/data.js), эта функция названа groupInfo, так же как свойство макета. Здесь, для ясности, я дал ей другое имя:

    function cellSpanningInfo() {
    return {	
    enableCellSpanning: true,
    cellWidth: 310,	
    cellHeight: 80	
    };	
    }

    Затем эту функцию задают как часть свойства layout в data-win-options:

    layout: { type: WinJS.UI.GridLayout, groupInfo: cellSpanningInfo }

    Или вы можете задать layout.groupInfo из JavaScript. В любом слуае, как только вы объявили об использовании объединения ячеек, шаблон элемента должен установить свойства каждого элемента style.width и style.height, и - подходящие значения отбивки, для увеличения ваших cellWidth и cellHeight в соответствии со следующими формулами (которые являются разными представлениями одной и той же формулы):

    templateSize = ((cellSize + margin) x multiplier) - margin (размерШаблона = ((размерЯчейки + поле) х множитель) - поле)
    
    cellSize = ((templateSize + margin) / multiplier) - margin
    (размерЯчейки = ((размерШаблона+поле)/множитель) - поле)

    В примере, эти стили установлены путём задания каждому из элементов одного из трёх имён классов: smallListIconTextItem, mediumListIconTextItem, и largeListIconTextItem, CSS-код которых приведен ниже: (из css/scenario4.css иcss/scenario5.css):

    .smallListIconTextItem {
    width: 300px;	
    height: 70px;	
    padding: 5px;	
    }
    .mediumListIconTextItem {
    width: 300px;	
    height: 160px;	
    padding: 5px;	
    }
    .largeListIconTextItem {
    width: 300px;	
    height: 250px;	
    padding: 5px;	
    }

    Так как каждый из этих классов имеет отбивку, их реальные размеры из CSS равняются 310х80, 310х170 и 310х260. Поля, которые используются в формуле, исходят из стиля win-container в таблице стилей WinJS, где они равны 5 пикселей (px). Таким образом:

    ((80 + 10) * 1) - 10 = 80; минус 5px отбивка сверху и снизу =  высота 70px в CSS 
    ((80 + 10) * 2) - 10 = 170; минус 5px отбивка = высота 160px
    ((80 + 10) * 3) - 10 = 260; минус 5px отбивка = высота 250px

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

    Что касается функции itemInfo, это способ оптимизации производительности listView при использовании объединения ячеек. Без присваивания функции этому свойству, gridLayout "вручную" определяет ширину и высоту каждого элемента при выводе и это может замедлить прокрутку, если вы быстро прокручиваете большое число элементов. Так как вам, возможно, уже известны размеры элементов, вы можете предоставить эту информацию посредством функции itemInfo. Эта функция принимает индекс элемента и возвращает объект, содержащий свойства элемента width и height. (Скоро мы увидим работающий пример).

    function itemInfo(itemIndex) {
    //определяет значения itemWidth и itemHeight по заданному itemIndex
    return {	
    newColumn: false,	
    itemWidth: itemWidth,	
    itemHeight: itemHeight	
    };	
    }

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

    Возможно, вы так же заметили свойство newColumn в возвращаемом значении. Как вы можете догадаться, установка его в true принуждает gridLayout начать новый столбец с этой ячейки, позволяя вам управлять данным поведением. Вы даже можете использовать newColumn самостоятельно, если хотите, с самыми маленькими списками.

    Сейчас вы можете задаться вопросом: что произойдёт, если я задам различные размеры в шаблоне элемента, но не объявлю об объединении ячеек? Всё закончится перекрывающими (и весьма странно выглядящими) ячейками. Это происходит потому, что gridLayout воспринимает первый элемент в группе в качестве базового, задающего размеры всех остальных элементов (и, так же, сетки из заставок). Он не пытается автоматически изменить размер каждого элемента в зависимости от его содержимого. Испытайте это в Сценариях 4 и 5: удалите свойство layout.group из data-win-options ListView в html/scenario4.html или html/scenario5.html и перезапустите приложение. Вы увидите, как элементы среднего и большого размера заходят друг на друга, как показано ниже:

    Затем, пройдите в js/data.js и установите стиль первого элемента в массиве myCellSpanningData на largeListIconTextItem, и перезапустите приложение. ListView теперь будет иметь макет с данным размером, установленным в качестве базового размера элемента:

    Использование размеров первого элемента, как здесь, подчёркивает тот факт, что ListView с объединением ячеек требует больше времени на вывод, так как он должен измерить каждый из элементов в том виде, в котором он размещается, либо с помощью функции itemInfo, либо без неё. По этой причине, возможно, лучше избегать объединения ячеек при работе с большими списками.

    Гораздо интереснее это всё становится, если подумать о том, как gridLayout поступает с элементами разной ширины. Да и в примере этого нет. В базовом алгоритме он всё еще располагает элементы сверху вниз и слева направо, но сейчас он заполняет пустые пространства небольшими элементами, когда большие элементы создают подобные пустоты. Для того, чтобы это показать, изменим пример таким образом, чтобы наименьший элемент имел размеры 155х80 (половина оригинального размера), средний был размером 310х80, и большой - 310х160. Вот какие изменения позволят нам это реализовать:

  • Отменим любые изменения из предыдущих испытаний: в html/scenario4.html, вернем назад groupInfo в data-win-options, и в js/data.js, изменим класс первого элемента myCellSpanningData обратно к значению smallListIconTextItem.
  • В js/data.js, изменим cellWidth в groupInfo на 155 (половина от 310), и оставим cellHeight в значении 80. Для ясности, кроме того, добавим значение приращения к началу текста каждого элемента в массиве myCellSpanningData.
  • В css/scenario4.css:
  • Изменим ширину (width) smallListIconTextItem до 145px. Применим формулу, ((145+10)*1)-10=145. Высоту (height) изменим на 70px.
  • Изменим ширину mediumListIconTextItem на 310px, высоту - на 70px.
  • Изменим ширину largeListIconTextItem на 310px, выс оту на 160px. Для высоты применим формулу: ((80+10)*2)-10=170px.
  • Установим стиль width в правиле #listview в значение 800px, height - в 600px (для того, чтобы было больше места, в котором можно видеть макет)
  • Рекомендую выполнять эти изменения в Blend, где ваши правки отражаются на результате гораздо быстрее, чем когда вы, для проверки, запускаете приложение из Visual Studio. В любом случае, результаты, которые показаны на Рис. 5.10, где числа показывают нам порядок, в котором расположены элементы (извиняюсь за обрезанный текст… чем-то приходится жертвовать). Копию этого примера вы можете найти в дополнительных материалах к курсу.

    (рис 5.10) Измененный пример работы с шаблонами элементов ListView, более полно показывающий объединение ячеек

    В измененный пример я включил функцию itemInfo в js/data.js, как вы уже могли заметить. Она возвращает размеры элементов в соответствии с типами, заданными для этих элементов:

    function itemInfo(index) {	
    //getItem(index).data получает массив элементов из WinJS.Binding.List
    var item = myCellSpanningData.getItem(index).data;	
    var width, height;	
    switch (item.type) {	
    case "smallListIconTextItem":
    width = 145;	
    height = 70;	
    break;	
    case "mediumListIconTextItem":
    width = 310;	
    height = 70;	
    break;	
    case "largeListIconTextItem":
    width = 310;	
    height = 160;	
    break;	
    }	
    return {	
    newColumn: false,	
    itemWidth: width,	
    itemHeight: height
    };	
    }

    Вы можете установить в этой функции точку останова и убедиться в том, что она вызывается для каждого элемента. Кроме того, вы можете увидеть, что это приводит к одному и тому же результату. Теперь измените возвращаемое значение newColumn так, как показано ниже, для того, чтобы принудительно начать вывод с новой колонки перед элементом №7 и №15 на Рис. 5.10, так как они странно располагаются по столбцам.

    newColumn: (index == 6 || index == 14),  //Разрыв на элементах 7 и 15 (индексы 6 и 14)

    Результат этого изменения показан на Рис. 5.11.

    (рис 5.11) Вывод с новой колонки при объединении ячеек на элементах 7 и 15

    Последнее, что я отметил, экспериментируя с данным примером, это то, что если размеры элементов в правиле стиля наподобие smallListIconTextItem меньше, чем размер дочернего элемента, такого, как .regularlistIconTextItem (который включает поля и отбивки) больший размер выигрывает в макете. В ходе собственных экспериментов вы можете захотеть удалить поля по умолчанию в 5px, которые установлены для win-container. Это то, что создаёт пустые пространства между элементами на Рис. 5.10, но это должно быть добавлено в уравнение. Следующее правило устанавливает это поле в 0px:

    #listView > .win-horizontal .win-container {
    margin: 0px;
    }

    Оптимизация производительности ListView

    Я часто говорю людям, что с ListView можно столько всего делать, о нём можно столько всего узнать, что он достоен отдельной книги! На самом деле, Microsoft могла просто создать базовый элемент управления, который позволял бы вам создавать элементы, настроенные по шаблону и на этом остановиться. Однако, зная, что ListView будет в центре огромного количества приложений (возможно, в составе большинства приложений вне игровых приложений), ожидая, что ListView будет вызываться для управления тысячами, или даже десятками тысяч элементов, высокопрофессиональная и увлеченная своим делом группа инженеров сделала всё возможное для обеспечения множества уровней усовершенствований, которые позволят вашим приложениям наилучшим образом выполнять своё предназначение.

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

    Мне хотелось бы отметить, что материал "Использование элемента управления ListView" (http://msdn.microsoft.com/library/windows/apps/Hh781224.aspx) содержит даже больше советов, чем я способен привести здесь. (Мне нужно писать и другие главы!). Я надеюсь, что вы изучите данный материал, и кто знает, может быть вы станете тем, кто напишет всеобъемлющую книгу по ListView! Более того, дополнительное руководство по производительности приложения в целом можно найти в материале "Рекомендации по повышению производительности приложений Магазина Windows" (http://msdn.microsoft.com/library/windows/apps/hh465194.aspx), который содержит и подраздел об использовании ListView.

    Произвольный доступ

    Если вы похожи на меня и на других членов моей семьи, возможно, у вас есть постоянно увеличивающийся запас цифровых фотографий, который заставляет вас радоваться тому, что жёсткие диски объёмом больше гигабайта падают в цене. Другими словами, для многих пользователей вполне нормально работать с коллекциями, содержащими десятки тысяч элементов, которые они могут захотеть просмотреть в ListView. Но только представьте себе нагрузку от попытки загрузить эскизы для каждого из этих элементов в память для отображения их в списке. На низкопроизводительном аппаратном обеспечении, ограниченном в потреблении электроэнергии, это, возможно, приведет к необходимости срочного завершения работы нескольких приостановленных приложений, и результат может быть вовсе не "быстрым и гибким". Пользователь может столкнуться с необходимостью ждать по-настоящему долго до того момента, пока элемент управления приобретет интерактивность и может по-настоящему устать, глядя на индикатор выполнения.

    Именно поэтому свойство loadingBehavior установлено по умолчанию в значение "randomaccess". В этом режиме полоса прокрутки ListView отражает общий размер списка, в итоге, пользователь сможет оценить его размер, но ListView в любой момент полностью хранит в памяти лишь пять полных экранов элементов (с общим лимитом в 1000 элементов). Для большинства страниц это означает видимую страницу (в области просмотра) и две буферных страницы впереди и позади неё. (Если вы просматриваете первую страницу, то буфер простирается на четыре страницы вперед; если вы на последней странице, то буфер простирается на четыре страницы позади неё - вы поняли идею).

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

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

    Инкрементная загрузка

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

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

    Работа инкрементной загрузки показана в Сценариях 2 и 3 примера "Режимы работы загрузки данных ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-loading-behaviors-718a4673) (Сценарий 1 посвящен произвольному доступу, но там нет ничего нового). Режим инкрементной загрузки активирует следующие характеристики:

  • Свойство ListView pagesToLoad показывает, как много страниц или полных экранов загружаются одновременно. Значение по умолчанию - пять.
  • Свойство automaticallyLoadPages показывает, следует ли ListView загружать новые страницы автоматически, когда вы перемещаетесь по списку. Если оно установлено в true (по умолчанию), как показано в Сценарии 2, когда вы прокручиваете список до конца, вы увидите изменение полосы прокрутки при загрузке новых страниц. Если в false, что показано в Сценарии 3, новые страницы не загружаются до тех пор, пока вы не вызовете метод loadMorePages.
  • Если свойство automaticallyLoadPages установлено в true, свойство pagesToLoadThreshold показывает, как близко пользователь должен подойти к текущему концу списка, прежде чем будет запущена загрузка новых страниц. Значение по умолчанию - два.
  • Когда новые страницы начинают загружаться (либо автоматически, либо в ответ на loadMorePages), ListView начинает обновлять свойство loadingState, вызывая события loadingstatechanged, как было уже описано.
  • Функции шаблонов (Часть 2): Promise-объекты!

    Как мы только что обсудили, параметры поведения ListView при загрузке данных имеют отношение к инкрементной загрузке страниц. Будет полезным скомбинировать их с инкрементной загрузкой элементов. Для этого нам нужно взглянуть на то, что называется конвейером визуализации (rendering pipeline), в том виде, в котором это реализовано в функциях шаблонов.

    Когда мы ранее впервые смотрели на функции шаблонов (посмотрите "Как, на самом деле, работают шаблоны"), я отмечал, что они дают нам возможность контролировать и то, как конструируется элемент, и то, когда это происходит, и то, что подобные функции называют визуализаторами (renderers). Это - средство, с помощью которого вы можете реализовать пять прогрессивных уровней оптимизации для ListView (и для FlipView, хотя это распространено меньше). Сам факт использования визуализатора, с чем мы уже сталкивались, - это Уровень 1. Теперь мы готовы увидеть оставшиеся четыре уровня. Это захватывающая тема, так как она показывает усовершенствования, которые реализованы для нас в ListView!

    В этом рассказе мы можем опираться на пример "Оптимизация производительности HTML ListView" (http://code.msdn.microsoft.com/windowsapps/ListView-performance-39fb71f0), который демонстрирует все эти уровни и позволяет вам видеть их эффект. Вот обзор этих возможностей:

  • Простой (simple) или базовый визуализатор позволяет управлять выводом на поэлементной основе.
  • Визуализатор элементов-заполнителей (placeholder) разделяет создание элементов на два этапа. На первом этапе возвращаются лишь те объекты, которые определяют форму элементов. Это позволяет ListView быстро выполнить вывод шаблона, до того, как будут получены все подробные сведения, особенно когда данные поступают из потенциально медленного источника. Когда данные элемента доступны, начинается вторая фаза, запускается копирование этих данных в элементы и создаются дополнительные элементы, которые не влияют на форму элементов.
  • Визуализатор с повторным использованием элементов-заполнителей (recycling placeholder) добавляет возможность повторного использования существующего содержимого DOM для элементов, что быстрее, чем создание их с нуля. Для этих целей, ListView, зная, что его будут быстро пролистывать, хранит некоторое количество элементов, когда они оказываются не видны. В визуализаторе, вы добавляете ветвь кода для очистки повторно используемых элементов, если они к вам попадают, и для возвращения их в виде элементов-заполнителей. Затем вы заполняете их реальными значениями на второй стадии визуализации.
  • Многошаговый (multistage) визуализатор расширяет визуализатор с повторным использованием элементов-заполнителей отложенной загрузкой изображений и других данных тогда, когда элемент полностью готов в ListView. Кроме того, он задерживает любые действия, связанные с визуальным оформлением списка, такие, как анимации, до тех пор, пока элемент не появится на экране.
  • И, наконец, многошаговый пакетный (multistage batch) визуализатор добавляет возможность пакетного добавления изображений и других ресурсов, тем самым, отрисовывая их и получая возможность анимировать их появление в ListView в виде группы, в итоге ресурсы видеокарты (GPU) могут быть использованы более эффективно.
  • Используя любой из этих визуализаторов, вы должны стремиться к тому, чтобы сделать их как можно более быстрыми. В особенности это касается минимизации использования вызовов DOM API, которые включают в себя установку индивидуальных свойств. Используйте строку innerHTML, там, где это возможно, для создания элементов, вместо того, чтобы выполнять отдельные вызовы, и сведите к минимуму использование getElementById, querySelector и других вызовов, предусматривающих обход DOM, кэшируя элементы, к которым вы обращаетесь чаще всего. Это значительно улучшит производительность. Для того, чтобы показать эффект от этих улучшений, следующий рисунок показывает пример того, как происходит вывод данных в неоптимизированном ListView:

    Жёлтые столбцы показывают исполнение JavaScript-кода приложения - то есть - время, потраченное внутри визуализатора. Бежевые столбцы показывают время, потраченное в DOM-макете, и столбцы цвета морской волны показывают вывод данных на экран. Как вы можете видеть, когда элементы добавляются по одному, есть перерывы в исполнении кода, и сложность здесь в том, что большинство дисплеев обновляются лишь каждые 10-20 миллисекунд (50-100 Гц). В результате мы имеем множество разрывов в процессе визуализации.

    После улучшений, график может выглядеть так, как показано ниже, когда работа приложения скомбинирована в одном блоке, таким образом, значительно уменьшилась нагрузка, связанная с DOM-макетом (бежевые столбцы):

    Как и другое изображение, это получено от инструмента для анализа производительности, который называется XPerf и является частью Windows SDK (смотрите врезку). Без изучения деталей, самое важное, что мы здесь разобрали - это шаги, которые нужно предпринять для достижения подобного результата. А именно, различные формы визуализиторов, которые вы можете применять, как показано в примере.

    Врезка: XPerf и msWriteProfilerMark

    Средство XPerf в Windows SDK, документацию по которому можно найти на странице "Инструменты анализа производительности Windows" (http://msdn.microsoft.com/en-US/performance/cc825801.aspx), могут хорошо помочь вам понять реальное поведение вашего приложения в конкретной системе. Помимо прочего, он записывает в журнал вызовы, которые вы выполняете к msWriteProfilerMark (http://msdn.microsoft.com/library/windows/apps/dd433074.aspx), вы можете это заметить, просматривая исходный код WinJS. Для того чтобы отобразить это с помощью xperf, однако, вам нужно запустить протоколирование такой командой:

    xperf -start user -on PerfTrack+Microsoft-IE:0x1300

    и завершить протоколирование нижеприведенной командой, где <trace_filename> - это любой путь и имя файла по вашему выбору:

    xperf -stop user -d <trace_filename>.etl

    Открытие .etl-файла, который вы сохранили, приведет к запуску Windows Perfomance Analyzer (Анализатора производительности Windows) и отобразит график событий. Щелчкните правой кнопкой мыши по графику, затем щёлкните Summary Table (Сводная таблица). В этой таблице разверните Microsoft-IE и затем разверните узел Mshtml_DOM_CustomSiteEvents. Столбец Field3 должна содержать текст, который вы передаете msWriteProfilerMark, а столбец Time(s) поможет вам определить, сколько времени заняло действие.

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

    function simpleRenderer(itemPromise) {
    return itemPromise.then(function (item) {
    var element = document.createElement("div");
    element.className = "itemTempl";
    element.innerHTML = "<img src='" + item.data.thumbnail +
    "' alt='Databound image' /><div class='content'>" + item.data.title + "</div>";
    return element;
    });
    }

    Эта структура ожидает доступности данных элемента и возвращает promise-объект для элемента, данные которого будут получены.

    Визуализатор с повторным использованием элементов-заполнителей создаёт элемент в два этапа. Возвращаемое значение - это объект, который содержит минимальный элемент-заполнитель в свойстве element, и promise-объект renderComplete, который выполняет, если необходимо, остальную работу:

    function placeholderRenderer(itemPromise) {	
    // создает базовый шаблон для элемента, не зависящий от данных
    var element = document.createElement("div");	
    element.className = "itemTempl";	
    element.innerHTML = "<div class='content'>...</div>";	
    // Возвращает элемент в виде заполнителя, и обратный вызов для его обновления, когда данные будут 
    //доступны
    return {
    element: element,
    
    // задаёт promise-объект, который завершит работу, когда завершится визуализация
    // itemPromise завершит работу, когда будут доступны данные
    renderComplete: itemPromise.then(function (item) {
    // изменяет элемент для включения данных
    element.querySelector(".content").innerText = item.data.title;
    element.insertAdjacentHTML("afterBegin", "<img src='" +
    item.data.thumbnail + "' alt='Databound image' />");
    })
    };
    }

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

    Визуализатор с повторным использованием элементов-заполнителей теперь знает о существования второго параметра, который называется recycled, и который ListView (но не FlipView) может предоставить вашей функции визуализации, когда параметр ListView loadingBehavior установлен в "randomaccess". Если задан recycled, вы можете просто очистить элементы, вернув их в качестве элементов-заполнителей, и затем заполнить данными внутри promise-объекта renderComplete, как ранее.

    Если он не предоставлен (когда ListView только что создан, или если loadingBehavior установлено в значение "incremental"), вы создаёте новый элемент:

    function recyclingPlaceholderRenderer(itemPromise, recycled) {
    var element, img, label;
    if (!recycled) {
    // создает базовый шаблон для элемента, не зависящий от данных 
    element = document.createElement("div");
    element.className = "itemTempl";
    element.innerHTML = "<img alt='Databound image' style='visibility:hidden;'/>" + "<div class='content'>...</div>";
    }
    else {
    // очищает элемент, после чего мы можем повторно использовать его 
    element = recycled;
    label = element.querySelector(".content");
    label.innerHTML = "...";
    img = element.querySelector("img");
    img.style.visibility = "hidden";
    }
    return {
    element: element,
    renderComplete: itemPromise.then(function (item) {
    // изменяет элемент для включения данных 
    if (!label) {
    label = element.querySelector(".content");
    img = element.querySelector("img");
    }
    label.innerText = item.data.title; img.src = item.data.thumbnail; img.style.visibility = "visible";
    })
    };
    }

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

    Если вы хотите очистить элементы, которые используются повторно, вы можете предоставить функцию для свойства ListView resetItem, которая будет содержать тот же код, как показано выше для данного случая. То же самое справедливо для свойства resetGroupHeader, так как вы можете использовать функции шаблона для заголовков групп, так же, как и для элементов. Мы не говорим много об этом, так как заголовков групп обычно немного, и они обычно не играют в производительности такой же роли. Несмотря на это, подобная возможность присутствует.

    Далее, у нас есть многошаговый визуализатор, который добавляет к повторно используемым элементам-заполнителям отложенную загрузку изображений и других данных тогда, когда элемент полностью представлен в DOM, и, далее, задерживает эффекты, наподобие анимации, до тех пор, пока элемент не окажется видимым на экране.

    Ключевую роль здесь играют три метода: ready, loadImage и isOnScreen, которые прикрепляются к элементу, предоставленному itemPromise. Следующий код показывает, как этим пользоваться (здесь element.querySelector обходит лишь небольшую часть DOM, в итоге, это не проблема):

    renderComplete: itemPromise.then(function (item) {	
    // преобразует элемент для обновления его названия (title)	
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;	
    // использует promise-объект item.ready для того, чтобы отложить более
    // ресурсоёмкие процедуры
     return item.ready;
    // использует возможность объединения promise-объектов в цеочку
    // для получения возможности отмены задания
    }).then(function (item) {
    //использует загрузчик изображений для того, чтобы
    //поставить в очередь загрузку изображения
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
    //как только загружено, проверяет видимость элемента 
    return item.isOnScreen();
    });
    }).then(function (onscreen) {
    if (!onscreen) {
    //если элемент не видим, его прозрачность не анимируется 
    img.style.opacity = 1;
    } else {
    //если элемент видим, анимировать прозрачность изображения
    WinJS.UI.Animation.fadeIn(img);
    }
    })

    Хочу предупредить, что в данной оптимизации производительности используется много promise-объектов! Но всё, что здесь имеется - это стандартная структура promise-объектов, объединенных в цепочку. Первая асинхронная операция в визуализаторе обновляет простые частки элемента, такие, как текст. Затем она возвращает promise-объект в item.ready. Когда данный вызов завершится, или, точнее, если он будет исполнен - вы можете использовать асинхронный метод элемента loadImage для загрузки изображения, возвращая promise-объект item.isOnScreen из его обработчика завершения. Когда и если отложенный результат isOnScreen будет получен, вы можете выполнить необходимые действия, которые нужны только для видимых элементов.

    Я выделял "если" в этих описаниях, так как весьме вероятно то, что пользователь будет прокручивать содержимое ListView, пока всё это происходит. Когда все эти promise-объекты объединены в цепочку, ListView может отменить асинхронные операции в любой момент, когда элемент выходит из области видимости или из области буферных страниц. Достаточно сказать, что элемент управления ListView прошёл через великое множество испытаний производительности!

    Теперь мы пришли к многошаговому пакетному визуализатору, который комбинирует вставку изображений в DOM для минимизации задач, связанных с макетом и перерисовкой. В нашем примере, для этого используется функция, которая называется createBatch, которая использует метод WinJS.Promise.timeout с 64-миллисекундным периодом для комбинации promise-вызовов, загружающих изображения в многошаговом визуализаторе. Честно говоря, вам придётся поверить мне на слово, так как вам надо стать настоящим экспертом в использовании promise-объектов для того, чтобы понять, как это работает!

    //При инициализации (за пределами визуализатора)
    thumbnailBatch = createBatch();
    
    //Внутри цепочки renderComplete 
    //...
    
    }).then(function () {
    return item.loadImage(item.data.thumbnail);
    }).then(thumbnailBatch()
    ).then(function (newimg) {
    img = newimg;
    element.insertBefore(img, element.firstElementChild);
    return item.isOnScreen();
    }).then(function (onscreen) {
    
    //...
    //Реализация createBatch
    
    function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];
    
    function completeBatch() {
    var callbacks = batchedItems;
    batchedItems = [];
    for (var i = 0; i < callbacks.length; i++) {
    callbacks[i]();	
    }	
    }	
    return function () {
    batchTimeout.cancel();
    batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);
    var delayedPromise = new WinJS.Promise(function (c) {
    batchedItems.push(c);	
    });	
    return function (v) { return delayedPromise.then(function () { return v; }); };
    };	
     }

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

    Что мы только что изучили

  • Коллекциями данных в памяти можно управлять с помощью WinJS.Binding.List, который отлично интегрируется с элементами управления для коллекций, наподобие FlipView и ListView. Данные коллекций, размещенных в памяти, могут поступать из WinJS.Xhr и из файлов.
  • Элемент управления WinJS.UI.FlipView отображает один элемент за раз; WinJS.UI.ListView отображает множество элементов в соответствии с конкретным макетом.
  • Центральная идея, общая для обоих элементов управления, заключается в том, что имеется источника данны, для вывода каждого элемента этого источника используется шаблон элемента. Шаблоны могут быть заданы как декларативно, так и программно.
  • ListView работает с добавленным описанием макета. WinJS предоставляет два встроенных макета. GridView - это двумерный, горизонтально прокручиваемый список; ListLayout предназначен для вывода одномерных вертикально прокручиваемых списков. Возможно реализовать и собственный макет.
  • ListView предоставляет возможность отображения элементов в группах; WinJS.BindingList предоставляет методы для создания сгруппированных, отсортированных и отлфильтрованных проекций элементов из источника данных.
  • Элемент управления для семантического масштабирования (WinJS.UI.SemanticZoom) предоставляет интерфейс, посредством которого вы можете переключаться между двумя различными представлениями источника данных, режимом детализированного представления (zoomed-in), который отображает детали, и режимом общего представления (zoomed-out), который предоставляет более общую информацию. Внешний вид этих двух режимов может очень сильно различаться, но они должны отображать связанные данные. Интерфейс IZoomableView нужен для каждого режима просмотра, таким образом, элемент управления SemanticZoom может переключаться между ними и перемещаться к верному элементу.
  • WinJS предоставляет StorageDataSource для создания коллекций на основе элементов StorageFile.
  • Можно реализовать пользовательский источник данных, как показано в примерах Windows SDK.
  • Программно заданные шаблоны реализованы в виде функций шаблонов, или визуализаторов. Эти функции могут реализовывать прогрессивные уровни оптимизации для отложенной загрузки изображений и пакетного добавления элементов в DOM.
  • И FlipView, и ListView предоставляют множество параметров и возможностей по стилизации. ListView, кроме того, позволяет организовывать выделение объектов и применять различные схемы поведения при выделении.
  • Элемент управления ListView обеспечивает встроенную поддержку оптимизации произвольного доступа к большим источникам данных, и, так же, инкрементный доступ к потенциально бесконечным источникам данных.
  • Элемент управления ListView поддерживает понятие объединения ячеек в gridLayout для поддержки отображения элементов разных размеров, каждый из которых должен быть кратен размеру базовой ячейки.
  • Вернуться к учебному плану