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

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

Быстрый старт №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.

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

увеличить изображение
Рис. 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. Две разные коллекции - помните об этом.

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

В измененном примере, я использовал WinJS.Binding.List.createFiltered для того, чтобы получить проекцию списка, отфильтрованного по текущему ключу2Создание отфильтрованных проекций это, кроме того, полезно для того, чтобы намеренно ограничить количество элементов, которые вы хотите отображать в элементе управления, когда вы можете быть уверенны в том, что под условия отбора подпадает лишь фиксированное количество элементов.. Свойство 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.createGrouped, третья (и необязательная) функция, представленная здесь - это функция сортировки групп, которая вызывается для сортировки коллекции данных групп и, таким образом, для изменения порядка, в котором группы отображаются в ListView3Это действие полностью отделено от создания отсортированной проекции отдельных элементов, для которого вы использовали бы 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. Как было упомянуто в подразделе "Процесс и стили навигации" раздела "Элементы управления страниц и навигация" "Анатомия приложения и навигация по страницам" , этот шаблон проекта предоставляет структуру приложения, построенную вокруг навигации по страницам: домашняя страница (pages/groupedItems) отображает коллекцию примеров данных (js/data.js) в элементе управления ListView, где каждое представление элемента описано с помощью WinJS.Binding.Template, так же, как и заголовки групп. Рис. 5.4 показывает макет домашней страницы и идентифицирует соответствующие элементы ListView. Как мы уже обсуждали, жест прикосновения к элементу вызывает перемещение на страницу pages/groupDetail, и сейчас мы сможем увидеть, как всё это работает с ListView.

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

Элементы в 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, как мы видели в "Анатомия приложения и навигация по страницам" :

_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), здесь много общего с шаблоном Приложение таблицы, и этот рассказ, на самом деле, применим и к тому и к другому шаблонам.

Владимир Мороз
Владимир Мороз
Украина, Киев, Киевская государственная академия водного транспорта имени Гетмана Петра Конашевича-Сагайдачного, 2012
Сергей Ширяев
Сергей Ширяев
Россия, г. Москва