It’s a safe bet to say that wherever you are, right now, you’re probably surrounded by quite a number of collections. This book you’re reading is a collection of chapters, and chapters are a collection of pages. Those pages are collections of paragraphs, which are collections of words, which are collections of letters, which are (assuming you’re reading this electronically) collections of pixels. On and on….
Your body, too, has collections on many levels, which is very much what one studies in college-level anatomy courses. Looking around my office and my home, I see even more collections: a book shelf with books; scrapbooks with pages and pages with pictures; cabinets with cans, boxes, and bins of food; my son’s innumerable toys; the DVD case…even the forest outside is a collection of trees and bushes, which then have branches, which then have leaves. On and on….
We look at these things as collections because we’ve learned how to generalize specific instances of unique things—like leaves or pages or my son’s innumerable toys—into categories or groups. This gives us powerful means to organize and manage those things (except for the clothes in my closet, as my wife will attest). And just as the physical world around us is very much made of collections, the digital world that we use to represent the physical is naturally full of collections as well. Thus programming languages like JavaScript have constructs like arrays to organize and manage collection data, and environments like Windows 8 provide collection controls through which we can visualize and manipulate that data.
In this chapter we’ll turn our attention to the two collection controls provided by WinJS: the FlipView, which shows one item from a collection at a time, and the ListView, which shows many items in different arrangements. As you might expect, the ListView is the richer of the two. As it’s really the centerpiece of many app designs, we’ll be spending the bulk of this chapter exploring its depths, along with the concept and implementation of semantic zoom (another control, in fact).
As both collection controls can handle items of arbitrary complexity (both in terms of data and presentation, unlike the simple HTML listbox and combobox controls), as well as an arbitrary number of items, they naturally build on the foundations of data binding and template controls we just saw at the end of Chapter 4, “Controls, Control Styling, and Data Binding.” They also have a close relationship to collection data sources, which we’ll specifically examine as well, and their own styling and behavioral considerations.
But let’s not exhaust our minds here at the outset of this chapter with theory or architectural intricacies! Instead, let’s just jump into some code to explore the core aspects of both controls.
To seek the basics of the collection controls, we’ll first look at the FlipView which will introduce us to item templates and data sources. We’ll then see how these also apply to the ListView control, then look at grouping items within a ListView.
As shown in Figure 5-1, the FlipView control sample is both a great piece of reference code for this control and a great visual tool through which to explore the control itself. (I’m also extremely grateful that I’ve not had to write such samples for this book!) For the purposes of this Quickstart, let’s just look at the first scenario of populating the control from a simple data source and using a template for rendering the items, as these mechanisms are shared with the ListView. We’ll come back to the other FlipView scenarios later in the chapter.
FIGURE 5-1 The FlipView control sample; the FlipView is the control displaying the picture.
As FlipView is a WinJS control, whose constructor is WinJS.UI.FlipView
, we declare it in markup with data-win-control
and data-win-options
attributes (see 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>
And of course, WinJS.UI.processAll
is called in the page-loading process to instantiate the control. In the FlipView’s options we can immediately see the two critical pieces to make the control work: a data source that provides the goods for each item and a template to render them.
If you were paying attention at the end of Chapter 4, you’ve probably guessed that the template is an instance of WinJS.Binding.Template
. And you’re right! That piece of markup, in fact, comes just before the control declaration in 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>
Note that a template must always be declared in markup before any controls that reference them: WinJS.UI.processAll
must instantiate the template first because the collection control will be asking the template to render its contents for each item in the data source. Also remember from Chapter 4 that instantiating a template removes its contents from the DOM so that it cannot be altered at run time. You can see this when running the sample: expand the nodes in Visual Studio’s DOM Explorer or Blend’s Live DOM pane, and you’ll see the root div
of the template but none of its children.
In the sample, the prosaically named ItemTemplate
is made of an img
element and another div
containing an h2
. The overlay
class on that latter div
, if you look at Figure 5-1 carefully, is clearly styled with a partially transparent background color (see css/default.css for the .overlaidItemTemplate .overlay
selector). This shows that you can use any elements you want in a template, including other WinJS controls. In the latter case, these are picked up when WinJS.UI.process/ processAll
is invoked on the template.31
You can also see that the template uses WinJS data-binding attributes, where the img.src
, img.alt
, and h2.innerText
properties are bound to data properties called picture
and title
. This shows how properties of two target elements can be bound to the same source property. (Remember that if you’re binding to properties of the WinJS control itself, rather than its child elements, those properties must begin with winControl
.)
For the data source, the FlipView’s itemDataSource
option is assigned the value of DefaultData.bindingList.dataSource
that you can find in 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 });
We briefly met WinJS.Binding.List
at the end of Chapter 4; its purpose is to turn an in-memory array into an observable data source for one-way binding. The WinJS.Binding.List
wrapper is also necessary because the FlipView and ListView controls cannot work directly against a simple array, even for one-time binding. They expect their data sources to provide the methods of the WinJS.UI.-IListDataSource
interface. The dataSource
property of a WinJS.Binding.List
, as in bindingList.dataSource
, provides exactly this, and you’ll always use this property in conjunction with FlipView and ListView. (It exists for no other purpose, in fact.) If you forget and attempt to just bind to the WinJS.Binding.List
directly, you’ll see an exception that says, “Object doesn’t support property or method ‘createListBinding’.”
Suffice it to say that WinJS.Binding.List
will become your intimate friend for in-memory data sources. Of course, you won’t typically be using hard-coded data like the sample. You’ll instead load array data from a file or obtain it from a web service, at which point WinJS.Binding.List
makes it accessible to collection controls.
Do note that WinJS.Binding.List
fully supports dynamic data. If you look at its reference page in the documentation, you’ll see that it looks a whole lot like a JavaScript array, with a length
property and the whole set of array methods from concat
and indexOf
to push
, pop
, and unshift
. This is entirely intentional: no need to make you relearn the basics!
It’s also important to note with FlipView, as well as ListView, that setting the control’s itemDataSource
property automatically sets up one-way binding, so any changes to the list object or even the array on which it is built will trigger an automatic update in the bound control.
As I said before, the basic mechanisms for data sources and templates apply to the ListView control exactly as it does to FlipView, which we can now see in the HTML ListView essentials sample (shown in Figure 5-2), specifically its first two scenarios of creating the control and responding to item events.
Because ListView can display multiple items at the same time, it needs one more piece in addition to the data source and the template: something to describe how those items visually relate to one another. This is the ListView’s layout
property, which we see in the markup for Scenario 1 of this sample along with a few other behavioral options (html/scenario1.html):
<div id="listView" data-win-control="WinJS.UI.ListView" data-win-options="{ itemDataSource: myData.dataSource, itemTemplate: smallListIconTextTemplate, selectionMode: 'none', tapBehavior: 'none', swipeBehavior: 'none', layout: { type: WinJS.UI.GridLayout } }"> </div>
FIGURE 5-2 The HTML ListView essentials sample.
The ListView’s constructor, WinJS.UI.ListView
, is, of course, called by the ubiquitous WinJS.-UI.processAll
when the page control is loaded. The data source for this list is set to myData.dataSource
where myData
is again a WinJS.Binding.List
(defined at the top of js/data.js over a simple array) and its dataSource
property provides the needed interface.
The control’s item template is defined earlier in default.html with the id of smallListIconTextTemplate and is essentially the same sort of thing we saw with the FlipView (an img
and some text elements), so I won’t list it here.
In the control options we see three behavioral properties: selectionMode
, tapBehavior
, and swipeBehavior
. These are all set to ’none’
in this sample to disable selection and click behaviors entirely, making the ListView a passive display. It can still be panned, but the items don’t respond to input. (Also see the “Item Hover Styling” sidebar.)
As for the layout
property, this is an object of its own, whose type
property indicates which layout to use. WinJS.UI.GridLayout
, as we’re using here, is a two-dimensional top-to-bottom then left-to-right algorithm, suitable for horizontal panning. WinJS provides another layout type called WinJS.UI.-ListLayout
, a one-dimensional top-to-bottom organization that’s suitable for vertical panning, especially in snapped view. (We’ll see this with the Grid App project template shortly; the ListView essentials sample lacks a good snapped view.)
Now while the ListView control in Scenario 1 only displays items, we often want those items to respond to a click or tap. Scenario 2 shows this, where the tapBehavior
property is set to ’invoke’
(see html/scenario2.html). This is the same as using tapBehavior:
WinJS.UI.TapBehaviortoggleSelect
, as that’s just defined in the enumeration as “invoke”. This behavior will select or deselect and item, depending on its state, and then invoke it. Other variations are directSelect
, where an item is always selected and then invoked, and invokeOnly
where the item is invoked without changing the selection state. You can also set the behavior to none
so that clicks and taps are ignored.
When an item is invoked, the ListView control fires an itemInvoked
event. You can wire up a handler by using either addEventListener
or the ListView’s oniteminvoked
property. Here’s how Scenario 2 does it (slightly rearranged from js/scenario2.js):
var listView = element.querySelector('#listView').winControl; listView.addEventListener("iteminvoked", itemInvokedHandler, false); function itemInvokedHandler(eventObject) { eventObject.detail.itemPromise.done(function (invokedItem) { // Act on the item }); }
Note that we’re listening for the event on the WinJS control, but it also works to listen for the event on the containing element thanks to bubbling. This can be helpful if you need to add listeners to a control before it’s instantiated, since the containing element will already be there in the DOM.
In the code above, you could also assign a handler by using the listView.oniteminvoked
property directly, or you can specify the handler in the iteminvoked
property data-win-options
(in which case it must be marked safe for processing). The event object you then receive in the handler contains a promise for the invoked item, not the item itself, so you need to call its done
or then
method to obtain the actual item data. It’s also good to know that you should never change the ListView’s data source properties directly within an iteminvoked
handler, because you’ll probably cause an exception. If you have need to do that, wrap the change code inside a call to setImmediate
so that you can yield back to the UI thread first.
While disabling selection and tap behaviors on a ListView creates a passive control, hovering over items with the mouse (or suitable touch hardware) still highlights each item; refer back to Figure 5-2. You can control this using the .win-container:hover
pseudo-selector for the desired control. For example, the following style rule removes the hover effect entirely:
#myListView .win-container:hover { background-color: transparent; outline: 0px; }
Displaying a list of items is great, but more often than not, a collection really needs another level of organization—what we call grouping. This is readily apparently when I open the file drawer next to my desk, which contains a collection of various important and not so important papers. Right away, on the file folder tabs, I see my groups: Taxes, Financials, Community, Insurance, Cars, Writing Projects, and Miscellany (among others). Clearly, then, we need a grouping facility within a collection control and ListView is happy to oblige.
A core demonstration of grouping can be found in the HTML ListView grouping and Semantic Zoom sample (shown in Figure 5-3). As with the Essentials sample, the code in js/groupedData.js contains a lengthy in-memory array around which we create a WinJS.Binding.List
. Here’s a condensation to show the item structure (I’d show the whole array, but this is making me hungry for some dessert!):
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" }, ...
Here we have a bunch of items with title
, text
, and picture
properties. We can group them any way we like and even change the groupings on the fly. As Figure 5-3 shows, the sample groups these by the first letter of the title.
FIGURE 5-3 The HTML ListView grouping and Semantic Zoom sample.
If you take a peek at the ListView reference, you’ll see that the control works with two templates and two collections: that is, alongside its itemTemplate
and itemDataSource
properties are ones called groupHeaderTemplate
and groupDataSource
. These are used with the ListView’s GridLayout
(the default) to organize the groups and create the headers above the items.
The header template in html/scenario1.html is very simple (and the item template is like what we’ve already seen):
<div id="headerTemplate" data-win-control="WinJS.Binding.Template"> <div class="simpleHeaderItem"> <h1 data-win-bind="innerText: title"></h1> </div> </div>
This is referenced in the control declaration (other options omitted):
<div id="listView" data-win-control="WinJS.UI.ListView" data-win-options="{ groupDataSource: myGroupedList.groups.dataSource, groupHeaderTemplate: headerTemplate }"> </div>
For the data sources, you can see that we’re now using a variable called myGroupedList
with a property inside it called groups
. What’s all this about?
Well, let’s take a short conceptual detour. Although computers have no problem chewing on a bunch of raw data like the myList
array, human beings like to view data with a little more organization. The three primary ways of doing this are grouping, sorting, and filtering. Grouping organizes items into groups, as shown in Figure 5-3; sorting orders items according to various rules; and filtering provides a subset of items that match certain criteria. In all three cases, however, you don’t want such operations to actually change the underlying data: a user might want to group, sort, or filter the same data in different ways from moment to moment.
Grouping, sorting, and filtering, then, are thus referred to as projections of the data: they’re all connected to the same underlying data such that a change to an item in the projection will be propagated back to the source, just as changes in the source are reflected in the projection.
The WinJS.Binding.List
object provides methods to create these projections: createGrouped
, createSorted
, and createFiltered
. Each method produces a special form of a WinJS.Binding.List
: GroupedSortedListProjection
, SortedListProjection
, and FilteredListProjection
, respectively. That is, each projection is a bindable list in itself, with a few extra methods and properties that are specific to the projection. You can even create a projection from a projection. For instance, createGrouped(...).createFiltered(...)
will create a filtered projection on top of a grouped projection. (Note, however, that the list’s sort
method does not create a projection. It applies the sorting in-place, just like the JavaScript array’s sort
.)
Now that we know about projections, we can see how myGroupedList
is created:
var myGroupedList = myList.createGrouped(getGroupKey, getGroupData, compareGroups);
This method takes three functions. The first, the group key function, associates an item with a group: it receives an item and returns the appropriate group string, known as the key. The key—which must be a string—can be something that’s directly included in an item or it can be derived from item properties. In the sample, the getGroupKey
function returns the first character of the item’s title
property (in upper case). Note, however, that the original sample just uses charAt
to obtain the grouping character, but this won’t work for a large number of languages. Instead, use the Windows.Globalization.-Collation.CharacterGroupings
class and its lookup
method as shown below, which will normalize casing automatically so that calling toLocaleUpperCase
isn’t necessary:
var cg = Windows.Globalization.Collation.CharacterGroupings(); function getGroupKey(dataItem) { return cg.lookup(dataItem.title); }
This code, and other changes made below, can be found in the modified version of this sample included with this chapter’s companion content.
Be clear that this first function, referred to as the group key function, determines only the association between the item and a group, nothing more. It also gets called for every item in the collection when createGrouped
is called, so it should be a quick operation. For this reason the creation of CharacterGroupings
is done one outside of the function.
Tip If deriving the group key from an item at run time required an involved process, you’ll improve overall performance by storing a prederived key in the item instead and just returning that from the group key function.
The data for the groups themselves, which is the collection to which the header template is bound to, isn’t actually created until the group projection’s groups
method is invoked, as happens when our ListView’s groupedDataSource
option gets processed. At that point, the second function passed to createGrouped
—the group data function—gets called only once per group with a representative item for that group. In response, your function returns an object for that group containing whatever properties you need for data binding.
In the sample, the getGroupData
function (passed to createGrouped
) simply returns an object with a single groupTitle
property that’s the same as the group key, but of course you can make that value anything you want. This code is also modified from the original sample to be attentive to globalization concerns, which we do by reusing getGroupKey
:
function getGroupData(dataItem) { return { groupTitle: getGroupKey(dataItem) }; }
In the modified sample I changed name the title
property of this group data object to a more distinct groupTitle
to make it very clear that it has nothing whatsoever to do with the title
property of the items. This meant changing the header templates in html/scenario1.html and html/scenario2.html to refer to groupTitle
as well. This helps us be clear that the data contexts in the header and item templates are completely different. For the header template, it’s the collection generated by the return values of your group data function; for the item template, it’s the grouped projection from WinJS.-Binding.List.createGrouped
. Two different collections—remember that!
So why do we have the group data function separated out at all? Why not just create that collection automatically from the group keys? It’s because you often want to include additional properties within the group data for use in the header template or in a zoomed-out view (with semantic zoom). Think of your group data function as providing summary information for each group. (The header text is really only the most basic such summary.) Since this function is only called once per group, rather than once per item, it’s the proper time to calculate or otherwise retrieve summary-level data. For example, to show an item count in the group headers, we just need to include that property in the objects returned by the group data function, then data-bind an element in the header template to that property.
In the modified sample, I use WinJS.Binding.List.createFiltered
to obtain a projection of the list filtered by the current key.32 The length
property of this projection is then the number of items in the group:
function getGroupData(dataItem) { var key = getGroupKey(dataItem); //Obtain a filtered projection of our list, checking for matching keys var filteredList = myList.createFiltered(function (item) { return key == getGroupKey(item); }); return { title: key, count: filteredList.length }; }
With this count
property in the collection, we can use it in the header template:
<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>
After a small tweak in css/scenario1.css—changing the simpleHeaderItem
class height to 65px to make a little more room—the list will now appears as follows:
Finally, back to WinJS.Binding.List.createGrouped
, the third (and optional) function here is a group sorter function, which is called to sort the group data collection and therefore the order in which those groups appear in the ListView.33 This function receives two group keys and returns zero if they’re equal, a negative number if the first key sorts before the second, and a positive if the second sorts before the first. The compareGroups
function in the sample does an alphabetical sort, which I’ve updated in the modified version to again use world-ready sort ordering:
function compareGroups(left, right) { return groupCompareGlobalized(left, right); } function groupCompareGlobalized(left, right) { var charLeft = cg.lookup(left); var charRight = cg.lookup(right); // If both are under the same grouping character, treat as equal if (charLeft.localeCompare(charRight) == 0) { return 0; } // In different groups, we must rely on locale-sensitive sort order of items since the names // of the groups don't sort the same as the groups themselves for some locales. return left.localeCompare(right); }
For a two-level sort, first by the descending item count and then by the first character, we could write the following (this is in the modified sample; refer to this in the call to myList.createGrouped
to see it in action):
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; }
If your various grouping functions don’t seem to be working right, you can set breakpoints and step through the code a few times, but this becomes tedious as the functions are called many, many times for even modest collections. Instead, try using console.log
to emit the parameters sent to those functions and/or your return values, allowing you to review the overall results much more quickly. To see what’s coming into the group sorting function, for example, try this code:
console.log("Comparing left = " + left + " to right = " + right);
Now that we’ve covered the details of the ListView control and in-memory data sources, we can finally understand the rest of the Grid App project template in Visual Studio and Blend. As we covered in the “The Navigation Process and Navigation Styles” section of Chapter 3, “App Anatomy and Page Navigation,” this project template provides an app structure built around page navigation: the home page (pages/groupedItems) displays a collection of sample data (see js/data.js) in a ListView control, where each item’s presentation is described by a WinJS.Binding.Template
as are the group headings. Figure 5-4 shows the layout of the home page and identifies the relevant ListView elements. As we also discussed before, tapping an item navigates to the pages/itemDetail page and tapping a heading navigates to the pages/groupDetail page, and now we can see how that all works with the ListView control.
The ListView in Figure 5-4 occupies the lower portion of the app’s contents. Because it can pan horizontally, it actually extends all the way across; various CSS margins are used to align the first items with the layout silhouette while allowing them to bleed to the left when the ListView is panned.
FIGURE 5-4 ListView elements as shown in the Grid App template home page. (All colored items are added labels and lines.)
There’s quite a bit going on with the ListView in this project, so we’ll take one part at a time. For starters, the control’s markup in pages/groupedItems/groupedItems.html is very basic, where the only option is to indicate that the items have no selection behavior:
<div class="groupeditemslist win-selectionstylefilled" aria-label="List of groups" data-win-control="WinJS.UI.ListView" data-win-options="{ selectionMode: 'none' }"> </div>
Switching over to pages/groupedItems/groupedItems.js, the page’s ready
method handles initialization:
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); // (Keyboard handler initialization omitted)... this.initializeLayout(listView, appView.value); listView.element.focus(); },
Here you can see that the control’s templates can be set in code just as easily as from markup, and in this case we’re using a class to locate the template element instead of an id. Why does this work? It’s because we’ve actually been referring to elements the whole time: the app host automatically creates a variable for an element that’s named the same as its id. It’s the same thing. In code you can also provide a function instead of a declarative template, which allows you to dynamically render each item individually. More on this later.
You can also see how this page assigns a handler to the itemInvoked
events (above ready
), calling WinJS.Navigation.navigate
to go to the groupDetail or itemDetail pages as we saw in Chapter 3:
_itemInvoked: function (args) { if (appView.value === appViewState.snapped) { // If the page is snapped, the user invoked a group. var group = Data.groups.getAt(args.detail.itemIndex); this.navigateToGroup(group.key); } else { // If the page is not snapped, the user invoked an item. 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 }); },
In this case we retrieve item data from the underlying collection (the getAt
methods) rather than using the item data itself. This is because the group information needed for the first case isn’t part of an item directly. We also see here that the page interprets item invocations differently depending on the view state. This is because it actually switches both its layout and its data source when the view state changes. This is handled in the page’s internal _initializeLayout
method, called both on startup and from the page’s updateLayout
function:
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" }); } },
A ListView’s layout, in short, can be changed at any time by setting its layout
property. When the view state is snapped, this is set to WinJS.UI.ListLayout
, otherwise WinJS.UI.GridLayout
(whose groupHeaderPosition
property can be "top"
or "left"
). You can also see that you can change a ListView’s data source on the fly: in snapped state it’s a list of groups, otherwise it’s the list of items.
I hope you can now see why I introduced page navigation well before we got to ListView, because this project gets quite complicated down in its depths! In any case, let’s now look at the templates for this page (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>
Again, we have the same use of WinJS.Binding.Template
and various bits of data-binding syntax sprinkled around the markup, not to mention the click
handler assigned to the header text itself, which, like an item in snapped view, navigates to the group detail page.
As for the data itself (that you’ll likely replace), this is again defined in js/data.js as an in-memory array that feeds into WinJS.Binding.List
. In the sampleItems
array each item is populated with inline data or other variable values. Each item also has a group
property that comes from the sampleGroups
array. Unfortunately, this latter array has almost identical properties as the items array, which can get confusing. To help clarify that a bit, here’s the complete property structure of an item:
{ group : { key, title, subtitle, backgroundImage, description }, title, subtitle, description, content, backgroundImage }
As we saw with the ListView grouping sample earlier, the Grid App project template uses createGrouped
to set up the data source. What’s interesting to see here is that it sets up an initially empty list, creates the grouped projection (omitting the optional sorter function), and then adds the items by using the list’s push
method:
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); });
This clearly shows the dynamic nature of lists and ListView: you can add and remove items from the data source, and one-way binding will make sure the ListView is updated accordingly. In such cases you do not need to refresh the ListView’s layout—that happens automatically. I say this because there’s been some confusion with the ListView’s forceLayout
method, which you only need to call, as the documentation states, “when making the ListView visible again after its style.display
property had been set to ‘none’.” You’ll find, in fact, that the Grid App code doesn’t use this method at all.
In js/data.js there are also a number of other utility functions, such as getItemsFromGroup
, which uses WinJS.Binding.List.createFiltered
as we did earlier. Other functions provide for cross-referencing between groups and items, as is needed to navigate between the items list, group details (where that page shows only items in that group), and item details. All of these functions are wrapped up in a namespace called Data
at the bottom of js/data.js, so references to anything from this file are prefixed elsewhere with Data.
.
And with that, I think you’ll be able to understand everything that’s going on in the Grid App project template to adapt it to your own needs. Just remember that all the sample data, like the default logo and splash screen images, is intended to be wholly replaced with real data that you obtain from other sources, like a file or WinJS.xhr
, and that you can wrap with WinJS.Binding.List
. Some further guidance on this can be found in the Create a blog reader tutorial on the Windows Dev Center, and although the tutorial uses the Split App project template, there’s enough in common with the Grid App project template that the discussion is really applicable to both.
Since we’ve already loaded up the HTML ListView grouping and Semantic Zoom sample, and have completed our first look at the collection controls, now is a good time to check out another very interesting WinJS control: Semantic Zoom.
Semantic zoom lets users easily switch between two views of the same data: a zoomed-in view that provides details and a zoomed-out view that provides more summary-level information. The primary use case for semantic zoom is a long list of items (especially ungrouped items), where a user will likely get really bored of panning all the way from one end to the other, no matter how fun it is to swipe the screen with a finger. With semantic zoom, you can zoom out to see headers, categories, or some other condensation of the data, and then tap on one of those items to zoom back into its section or group. The design guidance recommends having the zoomed-out view fit on one to three screenfuls at most, making it very easy to see and comprehend the whole data set.
Go ahead and try semantic zoom through Scenario 2 of the ListView grouping and Semantic Zoom sample. To switch between the views, use pinch-zoom touch gestures, Ctrl+/Ctrl- keystrokes, Ctrl+mouse wheel, and/or a small zoom button that automatically appears in the lower-right corner of the control, as shown in Figure 5-5. When you zoom out, you’ll see a display of the group headers, as also shown in the figure.
FIGURE 5-5 Semantic zoom between the two views in the ListView grouping and Semantic Zoom sample.
The control itself is quite straightforward to use. In markup, declare a WinJS control using the WinJS.UI.SemanticZoom
constructor. Within that element you then declare two (and only two) child elements: the first defining the zoomed-in view, and the second defining the zoomed-out view—always in that order. Here’s how the sample does it with two ListView controls (plus the template used for the zoomed-out view; I’m showing the code in the modified sample included with this chapter’s copanion content):
<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>
The first child, zoomedInListView, is just like the ListView for Scenario 1 with group headers and items; the second, zoomedOutListView, uses the groups as items and renders them with a different template. The semantic zoom control simply switches between the two views on the appropriate input gestures. When the zoom changes, the semantic zoom control fires a zoomchanged
event where the args.detail
value in the handler is true
when zoomed out, false
when zoomed in. You might use this event to make certain app bar commands available for the different views, such as commands in the zoomed-out view to change sorting or filtering, which would then affect how the zoomed-in view is displayed. We’ll see the app bar in Chapter 7, “Commanding UI.”
The control has a few other properties, such as enableButton
(a Boolean to control the visibility of the overlay button; default is true
), locked
(a Boolean that disables zooming in either direction and can be set dynamically to lock the current zoom state; default is false
), and zoomedOut
(a Boolean indicating if the control is zoomed out, so you can initialize it this way; default is false
). There is also a forceLayout
method that’s used in the same case as the ListView’s forceLayout
: namely, when you remove a display: none
style.
The zoomFactor
property is an interesting one that determines how the control animates between the two views. The animation is a combination of scaling and cross-fading that makes the zoomed-out view appear to drop down from or rise above the plane of the control, depending on the direction of the switch, while the zoomed-in view appears to sink below or come up to that plane. To be specific, the zoomed-in view scales between 1 and zoomFactor
while transparency goes between 1 and 0, and the zoomed-out view scales between 1/zoomFactor
and 1 while transparency goes between 0 and 1. The default value for zoomFactor
is 0.65, which creates a moderate effect. Lower values (minimum is 0.2) emphasize the effect, and higher values (maximum is 0.8) minimize it.
Where styling is concerned, you do most of what you need directly to the Semantic Zoom’s children. However, to style the Semantic Zoom control itself you can override styles in win-semanticzoom
(for the whole control) and win-semanticzoomactive
(for the active view). The win-semanticzoombutton
stylealso lets you style the zoom control button if needed.
It’s important to understand that semantic zoom is intended to switch between two views of the same data and not to switch between completely different data sets (see Guidelines and checklist for the Semantic Zoom control). Also, the control does not support nesting (that is, zooming out multiple times to different levels). Yet this doesn’t mean you have to use the same kind of control for both views: the zoomed-in view might be a list, and the zoomed-out view could be a chart, a calendar, or any other visualization that makes sense. The zoomed-out view, in other words, is a great place to show summary data that would be otherwise difficult to derive from the zoomed-in view. For example, using the same changes we made to include the item count with the group data for Scenario 1 (see “Quickstart #2b” above), we can just add a little more to the zoomed-out item template (as done in the modified sample in this chapter’s companion content):
The other thing you need to know is that the semantic zoom control does not work with arbitrary child elements. An exception about a missing zoomableView
property will tell you this! Each child control must provide an implementation of the WinJS.UI.IZoomableView
interface through a property called zoomableView
. Of all built-in HTML and WinJS controls, only ListView does this, which is why you typically see semantic zoom in that context. However, you can certainly provide this interface on a custom control, where the object returned by the constructor should contain a zoomableView
property, which is an object containing the methods of the interface. Among these methods are beginZoom
and endZoom
for obvious purposes, and getCurrentItem
and setCurrentItem
that enable the semantic zoom control to zoom in to the right group when it’s tapped in the zoomed-out view.
For more details, check out the HTML SemanticZoom for custom controls sample, which also serves as another example of a custom control.
For all the glory that ListView merits as the richest and most sophisticated control in all of WinJS, we don’t want to forget the humble FlipView! Thus before we delve wholly into ListView, let’s spend a few pages covering FlipView and its features through the other scenarios in the FlipView control sample. It’s worth mentioning too that although this sample demonstrates the control’s capabilities in a relatively small area, a FlipView can be any size, even occupying most of the screen. A common use for the control, in fact, is to let users flip through full-sized images in a photo gallery. Of course, the control can be used anywhere it’s appropriate, large or small. See Guidelines for FlipView controls for more on how best to use the control.
Anyway, Scenario 2 in the sample (“Orientation and Item Spacing”) demonstrates the control’s orientation
property. This determines the placement of the arrow controls: left and right (horizontal
) or top and bottom (vertical
) as shown below. It also determines the enter and exit animations of the items and whether the control uses the left/right or up/down arrow keys for keyboard navigation. This scenario also let you set the itemSpacing
property, which determines the amount of space between items when you swipe items using touch (below right). Its effect is not visible when using the keyboard or mouse to flip; to see it, you may need to use touch emulation in the Visual Studio simulator to partly drag between items.
Scenario 3 (“Using interactive content”) shows the use of a template function instead of a declarative template. We’ll talk more of such functions in “How Templates Really Work” later in this chapter, but put simply, a template function or renderer creates elements and sets their properties procedurally, which is essentially what WinJS.Binding.Template
does from the markup you give it. This allows you to render an item differently (that is, create different elements or customize style classes) depending on its actual data. In Scenario 3, the data source contains a “table of contents” item at the beginning, for which the renderer (a function called mytemplate
in js/interactiveContent.js) creates a completely different item:
The scenario also sets up a listener for click
events on the TOC entries, the handler for which flips to the appropriate item by setting the FlipView’s currentPage
property. The picture items then have a back link to the TOC. See the clickHandler
function in the code for both of these actions.
Scenario 4 (“Creating a context control”) demonstrates adding a navigation control overlay to each item:
The items themselves are again rendered using a declarative template, which in this case just contains a placeholder div
called ContextContainer for the navigation control (html/context-Control.html):
<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>
When the control is initialized in the processed
method of js/contextControl.js, the sample calls the FlipView’s async count
method. The completed handler, countRetrieved
, then creates the navigation control using a row of styled radiobuttons. The onpropertychange
handler for each radiobutton then sets the FlipView’s currentPage
property.
Scenario 4 also sets up listeners for the FlipView’s pageselected
and pagevisibilitychanged
events. The first is used to update the navigation radiobuttons when the user flips between pages. The other is used to prevent clicks on the navigation control while a flip is happening. (The event occurs when an item changes visibility and is fired twice for each flip, once for the previous item, and again for the new one.)
Scenario 5 (“Styling Navigation Buttons”) demonstrates the styling features of the FlipView, which involves various win-*
styles and pseudo-classes as shown here:
If you were to provide your own navigation buttons in the template (wired to the next
and previous
methods), hide the default by adding display: none
to the <control selector> .win-navbutton
style rule.
Finally, there are a few other methods and events for the FlipView that aren’t used in the sample, so here’s a quick rundown of those:
• pageCompleted
is an event that is raised when flipping to a new item is fully completed (that is, the new item has been rendered). In contrast, the aforementioned pageselected
event will fire when a placeholder item (not fully rendered) has been animated in. See “Template Functions (Part 2)” at the end of this chapter.
• datasourcecountchanged
is an event raised for obvious purpose, which something like Scenario 4 would use to refresh the navigation control if items could be added or removed from the data source.
• next
and previous
are methods to flip between items (like currentPage
), which would be useful if you provided your own navigation buttons.
• forceLayout
is a method to call specifically when you make a FlipView visible by removing a display: none
style. (The FlipView sample actually calls this whenever you change scenarios, but it’s not necessary because it never changes the style.)
• setCustomAnimations
allows you to control the animations used when flipping forward, flipping backward, and jumping to a random item.
For details on all of these, refer to the WinJS.UI.FlipView
documentation.
In all the examples we’ve seen thus far, we’ve been using an in-memory data source built on WinJS.-Binding.List
. Clearly, there are other types of data sources and it certainly doesn’t make sense to load everything into memory first. How, then, do we work with such sources?
WinJS provides some help in this area. First is the WinJS.UI.StorageDataSource
object that works with files in the file system, as the next section demonstrates with a FlipView and the Pictures Library. The other is WinJS.UI.VirtualizedDataSource
, which is meant for you to use as a base class for a custom data source of your own, an advanced scenario that we’ll touch on only briefly.
For everything we’ve seen in the FlipView sample already, it really begs for the ability to do something completely obvious: flip through pictures files in a folder. Using what we’ve learned so far, how would we implement something like that? We already have an item template containing an img
tag, so perhaps we just need some URIs for those files. Perhaps we could make an array of these using an API like Windows.Storage.KnownFolders.picturesLibrary.getFilesAsync
(declaring the Pictures Library capability in the manifest, of course!). This would give us a bunch of StorageFile
objects for which we could call URL.createObjectURL
. We could store those URIs in an array and then wrap it up with 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; });
Although this approach works, it can consume quite a bit of memory with a larger number of high-resolution pictures because each picture has to be fully loaded to be displayed in the FlipView. This might be just fine for your scenario but in other cases would consume more resources than necessary. It also has the drawback that the images are just stretched or compressed to fit into the FlipView without any concern for aspect ratio, and this doesn’t produce the best results.
A better approach is to use the WinJS.UI.StorageDataSource
that again works directly with the file system instead of an in-memory array. I’ve implemented this as a Scenario 8 in the modified FlipView sample code in this chapter’s companion content. (Another example can be found in the StorageDataSource and GetVirtualizedFilesVector sample.) Here we can use a shortcut to get a data source for the Pictures library:
myFlipView.itemDataSource = new WinJS.UI.StorageDataSource("Pictures");
"Pictures"
is a shortcut because the first argument to StorageDataSource
is actually something called a file query that comes from the Windows.Storage.Search
API, a subject we’ll see in more detail in Chapter 8, “State, Settings, Files, and Documents.” These queries, which feed into the powerful Windows.Storage.StorageFolder.createFileQueryWithOptions
function, are ways to enumerate files in a folder along with metadata like album covers, track details, and thumbnails that are cropped to maintain the aspect ratio. Shortcuts like "Pictures"
(also "Music"
, "Documents"
, and "Videos"
that all require the associated capability in the manifest) just create typical queries for those document libraries.
The caveat with StorageDataSource
is that it’s doesn’t directly support one-way binding, so you’ll get an exception if you try to refer to item properties directly in a template. To work around this, you have to explicitly use WinJS.Binding.oneTime
as the initializer function for each property:
<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>
In the case of the img.src
property, the file query gives us items of type Windows.Storage.-BulkAccess.FileInformation
(the source
variable in the code below), which contains a thumbnail image, not a URI. To convert that image data into a URI, we need to use our own binding initializer:
WinJS.Namespace.define("InitFunctions", { thumbURL: WinJS.Binding.initializer(function (source, sourceProp, dest, destProp) { if (source.thumbnail) { dest.src = URL.createObjectURL(source.thumbnail, { oneTimeOnly: true }); } }) });
In this initializer, the src : thumbnail
part of data-win-bind
is actually ignored because we’re just setting the image’s src
property directly to source.thumbnail
. This is just a form of one-way binding.
Note that thumbnails aren’t always immediately available in the FileInformation
object, which is why we have to verify that we actually have one before creating a URI for it. This means that quickly flipping through the images might show some blanks. To solve this particular issue, we can listen for the FileInformation.onthumbnailupdated
event and update the item at that time. The best way to accomplish this is to use the StorageDataSource.loadThumbnail
helper, which makes sure to call removeEventListener
for this WinRT event. (See “WinRT Events and removeEventListener” in Chapter 3.) You can use this method within a binding initializer, as demonstrated in Scenario 1 of the aforementioned StorageDataSource and GetVirtualizedFilesVector sample, or within a rendering function that takes the place of the declarative template. We’ll do this for our FlipView sample later on, in “How Templates Really Work,” which also lets us avoid the one-time binding tricks.
As a final note, Scenario 6 of the FlipView sample contains another example of a different data source, specifically one working with Bing Search. For that, let’s look at custom data sources.
Now that we’ve seen a collection control like FlipView working against two different data sources, you’re probably starting to correctly guess that all data sources share some common characteristics and a common programmatic interface. This is demonstrated again in Scenario 6 of the FlipView sample as well as in the HTML ListView working with data sources sample shown in Figure 5-6, as we’ll see in this section.
FIGURE 5-6 The HTML ListView working with data sources sample.
Scenarios 2 and 3 of this sample both work against a WinJS.Binding.List
data source, as we’ve already seen, and provide buttons to manipulate that data source. Those changes are reflected in the output. The difference between the two scenarios is that Scenario 2 manipulates the data through WinJS.Binding.List
methods like move
, whereas Scenario 3 manipulates the underlying data source directly through a more generic ListDataSource
. API.
Because of data binding, changes to the data are reflected in the ListView control either way, but there are three important differences. First, the ListDataSource
interface is common to all data sources, so any code you write against it will work for any kind of data source. Second, its methods are generally asynchronous because a data source might be connected to an online service or other such resource. Third, ListDataSource
provides for batching changes together by calling beginEdits
, which will defer any change notifications to any external bound objects until endEdits
is called. This allows you to do bulk data editing in ways that can improve ListView performance.
Scenarios 1 and 4 of the sample demonstrate how to create custom data sources. Scenario 1 creates a data source for Bing searches; Scenario 4 creates one for an in-memory array that you could adapt to work against a data feed that’s only brought down from a service a little at a time. What’s important for all these is that they implement something called a data adapter, which is an object with the methods of the WinJS.UI.IListDataAdapter
interface. This provides for capabilities like caching, virtualization, change detection, and so forth. Fortunately, you get most of these methods by deriving your class from WinJS.UI.VirtualizedDataSource
and then implementing those methods you need to customize. In the sample, for instance, the bingImageSearchDataSource
is defined as follows (see js/BingImageSearchDataSource.js):
bingImageSearchDataSource = WinJS.Class.derive(WinJS.UI.VirtualizedDataSource, function (devkey, query) { this._baseDataSourceConstructor(new bingImageSearchDataAdapter(devkey, query)); });
where the bingImageSearchDataAdapter
class implements only the getCount
and itemsFromIndex
methods directly.
For a further deep-dive on this subject beyond the sample, I refer you to a session from the 2011 //Build conference entitled APP210-T: Build data-driven collection and list apps in HTML5. Some of the details have since changed (like the ArrayDataSource is now WinJS.Binding.List
), but on the whole it very much explains all the mechanisms. It’s also helpful to remember that you can use other languages like C# and C++ to write custom data sources as well. Such languages could offer much higher performance within the data source and have access to higher-performance APIs than JavaScript.
Earlier, when we looked at the Grid App project template, I mentioned that you can use a function instead of a declarative template for properties like itemTemplate
(FlipView and ListView) and groupHeaderTemplate
(ListView). This is an important capability because it allows you to dynamically render items in a collection individually, using its particular contents to customize its view. It also allows you to initialize item elements in ways that can’t be done in the declarative form, such as cell spanning, delay-loading images, adding event handlers for specific parts of an item, and optimizing performance.
We’ll return to some of these topics later on. For the time being, it’s helpful to understand exactly what’s going on with declarative templates and how that relates to custom template functions.
As I noted before, when you refer to a declarative template in the FlipView or ListView controls, what you’re actually referring to is an element, not an element id. The id works because the app host creates variables with those names for the elements they identify. However, we don’t actually recommend this approach, especially within page controls (which you’ll probably use often). The first concern is that only one element can have a particular id, which means you’ll get really strange behavior if you happen to render the page control twice in the same DOM.
The second concern is a timing issue. The element id variable that the app host provides isn’t created until the chunk of HTML containing the element is added to the DOM. With page controls, WinJS.UI.-processAll
is called before this time, which means that element id variables for templates in that page won’t yet be available. As a result, any controls that use an id for a template will either throw an exception or just show up blank. Both conditions are guaranteed to be terribly, terribly confusing.
To avoid this issue with a declarative template, place the template’s name in its class
attribute:
<div data-win-control="WinJS.Binding.Template" class="myItemTemplate" ...></div>
Then in your control declaration, use the syntax select("<selector>")
in the options record, where <selector>
is anything supported by element.querySelector
:
<div data-win-control="WinJS.UI.ListView" data-win-options="{ itemTemplate: select('.myItemTemplate') }"></div>
There’s more to this, actually, than just a querySelector
call. The select
function within the options searches from the root of its containing page control. If no match is found, it looks for another page control higher in the DOM, then looks in there, continuing the process until a match is found. This lets you safely use two page controls at once that both contain the same class name for different templates, and each page will use the template that’s most local.
You can also retrieve the template element using querySelector
directly in code and assign the result to the itemTemplate
property. This would typically be done in a page’s ready
function, as demonstrated in the Grid App project, and doing so avoids both concerns identified here because querySelector
will be scoped to the page contents and will happen after WinJS.UI.processAll
.
The next interesting question about templates is, what, exactly, do we get when instantiating a WinJS.-Binding.Template
? This is more or less another WinJS control that turns into an element when you call WinJS.UI.processAll
. But it’s different in that it removes all its child elements from the DOM, so it never shows up by itself. It doesn’t even set a winControl
property on its containing element.
What is does have, however, is this exceptionally useful function called render
. Given a data context (an object with properties) and an element, render
creates a full copy of the template inside the element, resolving any data-binding relationships in the template (in both data-win-bind
and data-win-options
attributes) using the data context. In short, think of a declarative template as a set of instructions that the render
method uses to do all the necessary createElement
calls along with setting properties and doing data binding.
As shown on the How to use templates to bind data topic, you can just instantiate and render a template anywhere you want:
var templateElement = document.getElementById("templateDiv"); var renderHere = document.getElementById("targetElement"); renderHere.innerHTML = ""; WinJS.UI.process(templateElement).then(function (templateControl) { templateControl.render(myDataItem, renderHere); });
It should be wholly obvious that this is exactly what FlipView and ListView controls do for each item in a given data source. In the case of FlipView, it calls its item template’s render
method each time you switch to a different item in the data source. ListView iterates over its itemDataSource
and calls the item template’s render
for each item, and does something similar for its groupDataSource
and the groupHeaderTemplate
.
Knowing now that a WinJS.Binding.Template
control is basically just a set of declarative instructions for its render
method, you can just create a custom function to do the same job directly. That is, in addition to an element, the FlipView/ListView itemTemplate
properties and the ListView groupHeaderTemplate
property can also accept a renderer function. The controls use typeof
at run time to determine what you’ve assigned to these properties, so if you provide a template element, the controls will call its render
method; if you provide a function, the controls will just call that function for each item that needs to be rendered. This provides a great deal of flexibility to customize the template based on individual item data.
Indeed, a renderer allows you to individually control not only how the elements for each item are constructed but also when. As such, renderers are the primary means through which you can implement five progressive levels of optimization, especially for ListView. Warning! There be promises ahead! Well, I’ll save most of that discussion for the end of the chapter, because we need to look at other ListView features first. But here let’s at least look at the core structure of a renderer that applies to both FlipView and ListView, which you can see in the HTML ListView item templates and the HTML ListView optimizing performance samples. We’ll be drawing code from the latter.
For starters, you can specify a renderer by name in data-win-options
for both the FlipView and ListView controls. That function must be marked for processing as discussed in Chapter 4 since it definitely participates in WinJS.UI.processAll
, so this is a great place to use WinJS.Utilities.-markSupportForProcessing
. Note that if you assign a function to an itemTemplate
or groupHeaderTemplate
property in JavaScript, it doesn’t need the mark.
In its basic form, a template function receives an item promise as its first argument and returns a promise whose completed handler creates the elements for that item. Huh? Yeah, that confuses me too! So let’s look at the basic structure in terms of two functions:
function basicRenderer(itemPromise) { return itemPromise.then(buildElement); }; function buildElement (item) { var result = document.createElement("div"); //Build up the item, typically using innerHTML return result; }
The renderer is the first function above. It simply says, “When itemPromise
is fulfilled, meaning the item is available, call the buildElement
function with that item.” By returning the promise from itemPromise.then
(not done
, mind you!), we allow the collection control that’s using this renderer to chain the item promise and the element-building promise together. This is especially helpful when the item data is coming from a service or other potentially slow feed, and it’s very helpful with incremental page loading because it allows the control to cancel the promise chain if the page is scrolled away before those operations complete. In short, it’s a good idea!
Just to show it, here’s how we’d make a renderer directly usable from markup, as in data-win-options = "{itemTemplate: Renderers.basic }"
:
WinJS.Namespace.define("Renderers", { basic: WinJS.Utilities.markSupportedForProcessing(function (itemPromise) { return itemPromise.then(buildElement); }) }
It’s also common to just place the contents of a function like buildElement
directly within the renderer itself, resulting in a more concise expression of the exact same structure:
function basicRenderer(itemPromise) { return itemPromise.then(function (item) { var result = document.createElement("div"); //Build up the item, typically using innerHTML return result; }) };
What you then do inside the element creation function (whether named or anonymous) defines the item’s layout and appearance. Returning to Scenario 8 that we’ve added to the FlipView sample, we can take the following declarative template, where we had to play some tricks to get data binding to work:
<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>
and turn it into the following renderer, keeping the two functions here separate for the sake of clarity:
//Earlier: assign the template in code myFlipView.itemTemplate = thumbFlipRenderer; //The renderer (see Template Functions (Part 2) later in the chapter for optimizations) function thumbFlipRenderer(itemPromise) { return itemPromise.then(buildElement); }; //A function that builds the element tree 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; //Set up a listener for thumbnailUpdated that will render to our img element var img = result.querySelector("img"); WinJS.UI.StorageDataSource.loadThumbnail(item, img).then(); return result; }
Because we have the individual item
in hand already, we don’t need to quibble over the details of declarative data binding and converters: we can just directly use the properties we need from item.data
. As before, remember that the thumbnail
property of the FileInformation
item might not be set yet. This is where we can use the StorageDataSource.loadThumbnail
method to listen for the FileInformation.onthumbnailupdated
event. This helper function will render the thumbnail into our img
element when the thumbnail becomes available (with a little animation to boot!).
Tip You might also notice that I’m building most of the element by using the root div.innerHTML
property instead of calling createElement
and appendChild
and setting individual properties explicitly. Except for very simple structures, setting innerHTML
on the root element is more efficient because we minimize the number of DOM API calls. This doesn’t matter so much for a FlipView control whose items are rendered one at a time, but it becomes very important for a ListView with potentially thousands of items. Indeed, when we start looking at performance optimizations, we’ll also want to render the item in various stages, such as delay-loading images. We’ll see all the details in the “Template Functions (Part 2): Promises, Promises!” section at the end of this chapter.
Having already covered data sources and templates along with a number of ListView examples, we can now explore the additional features of the ListView control, such as layouts, styling, and cell spanning for multisize items. Optimizing performance then follows in the last section of this chapter. First, however, let me answer a very important question.
ListView is the hands-down richest control in all of Windows. It’s very powerful, very flexible, and, as we’re already learning, very deep and intricate. But for all that, sometimes it’s also just the wrong choice! Depending on the design, it might be easier to just use basic HTML/CSS layout.
Conceptually, a ListView is defined by the relationship between three parts: a data source, templates, and layout. That is, items in a data source, which can be grouped, sorted, and filtered, are rendered using templates and organized with a layout (typically with groups and group headers). In such a definition, the ListView is intended to help visualize a collection of similar and/or related items, where their groupings also have a relationship of some kind.
With this in mind, the following factors strongly suggest that a ListView is a good choice to display a particular collection:
• The collection can contain a variable number of items to display, possibly a very large number, showing more when the app runs on a larger display.
• It makes sense to organize and reorganize the items in various groups.
• Group headers help to clarify the common properties of the items in those groups, and they can be used to navigate to a group-specific page.
• It makes sense to sort and/or filter the items according to different criteria.
• Different groupings of items and information about those groups suggest ways in which semantic zoom would be a valuable user experience.
• The groups themselves are all similar in some way, meaning that they each refer to a similar kind of thing. Different place names, for example, are similar; a news feed, a list of friends, and a calendar of holidays are not similar.
• Items might be selectable individually or in groups, such that app bar commands could act on them.
On the flip side, opposite factors suggest that a ListView is not the right choice:
• The collection contains a limited or fixed number of items, or it isn’t really a collection of related items at all.
• It doesn’t make sense to reorganize the groupings or to filter or sort the items.
• You don’t want group headers at all.
• You don’t see how semantic zoom would apply.
• The groups are highly dissimilar—that is, it wouldn’t make sense for the groups to sit side-by-side if the headers weren’t there.
Let me be clear that I’m not talking about design choices here—your designers can hand you any sort of layout they want and as a developer it’s your job to implement it! What I’m speaking to is how you choose to approach that implementation, whether with controls like ListView or just with HTML/CSS layout.
I say this because in working with the developers who created the very first apps for the Windows Store, we frequently saw them trying to use ListView in situations where it just wasn’t needed. An app’s hub page, for example, might combine a news feed, a list of friends, and a calendar. An item details page might display a picture, a textual description, and a media gallery. In both cases, the page contains a limited number of sections and the sections contain very different content, which is to say that there isn’t a similarity of items across the groups. Because of this, using a ListView is more complicated than just using a single pannable div
with a CSS grid in which you can lay out whatever sections you need.
Within those sections, of course, you might use ListView controls to display an item collection, but for the overall page, a simple div
is all you need. I’ve illustrated these choices in Figure 5-7 using an image from the Navigation design for Windows Store apps topic, since you’ll probably receive similar images from your designers. Ignoring the navigation arrows, the hub and details pages typically use a div
at the root, whereas a section page is often a ListView. Within the hub and details pages there might be some ListView controls, but where there is essentially fixed content (like a single item), the best choice is a div
.
FIGURE 5-7 Breaking down typical hub-section-detail page designs into div
elements and ListView controls.
A clue that you’re going down the wrong path, by the way, is if you find yourself trying to combine multiple collections of unrelated data into a single source, binding that source to a ListView, and implementing a renderer to tease all the data apart again so that everything renders properly! All that extra work could be avoided simply by using HTML/CSS layout.
For more on ListView design, see Guidelines and checklist for ListView controls.
In previous sections we’ve already seen some of the options you can use when creating a ListView, options that correspond to the control’s properties that are accessible also from JavaScript. Let’s look now at the complete set of properties, methods, and events, which I’ve organized into a few groups—after all, those properties and methods form quite a collection in themselves! Since the details for the individual properties are found on the WinJS.UI.ListView
reference page, what’s most useful here is to understand how the members of these groups relate:
• Addressing items The currentItem
property gets or sets the item with the focus, and the elementFromIndex
and indexOfElement
methods let you cross-reference between an item index and the DOM element for that item. The latter could be useful if you have other controls in your item template and need to determine the surrounding item in an event handler.
• Item visibility The indexOfFirstVisible
and indexOfLastVisible
properties let you know what indices are visible, or they can be used to scroll the ListView appropriate for a given item. The ensureVisible
method brings the specified item into view, if it’s been loaded. There is also the scrollPosition
property that contains the distance in pixels between the first item in the list and the current viewable area. Though you can set the scroll position of the ListView with this property, it’s reliable only if the control’s loadingState
(see “Loading state” group below) is ready
, otherwise the ListView may not yet know its actual dimensions. It’s thus recommended that you instead use ensureVisible
or indexOfFirstVisible
to control scroll position.
• Item invocation The itemInvoked
event, as we’ve seen, fires when an item is tapped, unless the tapBehavior
property is not set to none
, in which case no invocation happens. Other tapBehavior
values from the WinJS.UI.TapBehavior
enumeration will always fire this event but determine how the item selection is affected by the tap. Do note that you can override the selection behavior on a per-item basis using the selectionchanging
event and suppress the animation if needed. See the “Tap/Click Behaviors” sidebar after this list.
• Item selection The selectionMode
property contains a value from the WinJS.UI.-SelectionMode
, enumeration, indicating single-, multi-, or no selection. At all times the selection
property contains a ListViewItems
object whose methods let you enumerate and manipulate the selected items (such as setting selected items through its set
method). Changes to the selection fire the selectionchanging
and selectionchanged
events; with selectionchanging
, its args.detail.newSelection
property contains the newly selected items. For more on this, refer to the HTML ListView customizing interactivity sample.
• Swiping Related to item selection is the swipeBehavior
property that contains a value from the WinJS.UI.SwipeBehavior
enumeration. “Swiping” or “cross-slide” is the touch gesture on an item to select it where the gesture moves perpendicular to the panning direction of the list. If this is set to none
, swiping has no effect on the item and the gesture is bubbled up to the parent elements, allowing a vertically oriented ListView or its surround page to pan. If this is set to select
, the gesture is processed by the item to select it.
• Data sources and templates We’ve already seen the groupDataSource
, groupHeaderTemplate
, itemDataSource
, and itemTemplate
properties already. There are two related properties, resetGroupHeader
and resetItem
, that contain functions that the ListView will call when recycling elements. This is explained in “Template Functions (Part 2): Promises, Promises!” section.
• Layout As we’ve also seen, the layout
property (an object) describes how items are arranged in the ListView, which we’ll talk about more in “Layouts and Cell Spanning” below. We’ve also seen the forceLayout
function that’s specifically used when a display: none
style is removed from a ListView and it needs to re-render itself.
• Loading behavior As explained in the “Optimizing ListView Performance” section later on, this group determines how the ListView loads pages of items (which is why ensureVisible
doesn’t always work if a page hasn’t been loaded). When the loadingBehavior
property is set to "randomaccess"
(the default), the ListView’s scrollbar reflects the total number of items but only five total pages of items (to a maximum of 1000) are kept in memory at any given time as the user pans around. (The five pages are the current page, plus two buffer pages both ahead and behind.) The other value, "incremental"
, is meant for loading some number of pages initially and then loading additional pages when the user scrolls toward the end of the list (keeping all items in memory thereafter). Incremental loading works with the automaticallyLoadPages
, pagesToLoad
, and pagesToLoadThreshold
properties, along with the loadMorePages
method, as we’ll see.
• Loading state The read-only loadingState
property contains either "itemsLoading"
(the list is requesting items and headers from the data source), "viewportLoaded"
(all items and headers that are visible have been loaded), "itemsLoaded"
(all remaining nonvisible buffered items have been loaded), or "complete"
(all items are loaded, content in the templates is rendered, and animations have finished). Whenever this property changes, which is basically whenever the ListView needs to update its layout due to panning, the loadingStateChanged
event fires.
• Miscellany addEventListener, removeEventListener, and dispatchEvent are the standard DOM methods for handling and raising events. These can be used with any other event that the ListView supports, including contentanimating
that fires when the control is about to run an item entrance or transition animation, allowing you to either prevent or delay those animations. The zoomableView
property contains the IZoomableView
implementation as required by semantic zoom (apps will never manipulate this property).
When you tap or click an item in a ListView with the tapBehavior
property set to something other than none
, there’s a little ~97% scaling animation to acknowledge the tap. If you have some items in a list that can’t be invoked (like those in a particular group or ones that you show as disabled because backing data isn’t yet available), they’ll still show the animation because the tapBehavior
setting applies to the whole control. To remove the animation for any specific item, you can add the win-interactive
class to its element within a renderer function, which is a way of saying that the item internally handles tap/click events, even if it does nothing but eat them. If at some later time the item becomes invocable, you can, of course, remove that class.
If you need to suppress selection for an item, add a handler for the ListVIew’s selection-changing
event and call its args.detail.preventTapBehavior
method. This works for all selection methods, including swipe, mouse click, and the Enter key.
Following the precedent of Chapter 4 and the earlier section on ListView, styling is best understood visually as in Figure 5-8, where I’ve applied some garish CSS to some of the win-*
styles so that they stand out. I highly recommend you look at the Styling the ListView and its items topic in the documentation, which details some additional styles that are not shown here.
FIGURE 5-8 Style classes as utilized by the ListView control.
A few notes about styling:
• Remember that Blend is your best friend here!
• As with styling the FlipView, a class like win-listview
is most useful with styles like margins and padding, since a property like its background color won’t actually show through anywhere (unlike win-viewport
and win-surface
).
• win-viewport
styles the nonscrolling background of the ListView and is rarely used, perhaps for a nonscrolling background image. win-surface
styles the scrolling background area.
• win-container
primarily exists for two things. One is to create space between items using margin
styles, and the other is to override the default background color, often making its background transparent so that the win-surface
or win-viewport
background shows through. Note that if you set a padding
style here instead of margin
, you’ll create areas around what the user will perceive as the item that are still invoked as the item. Not good. So always use margin
to create space between items.
• Though win-item
is listed as a style, it’s deprecated and may be removed in the future: just style the item template directly.
• The documentation points out that styles like win-container
and win-surface
are used by multiple WinJS controls. (FlipView uses a few of them.) If you want to override styles for a ListView, be sure to scope your selectors them with other classes like .win-listview
or a particular control’s id or class.
• The default ListView height is 400px, and the control does not automatically adjust itself to its content. You’ll almost always want to override that style in CSS or set it from JavaScript when you know the space that the ListView should occupy, as we’ll cover in Chapter 6, “Layout.”
• Styles not shown in the figure but described on Styling the ListView and its items include win-focusedoutline
, win-selection
, win-selected
, win-selectionborder
, win-selectionbackground
, and win-selectionhint
. There is also the win-selectionstylefilled
class that you add to an item to use a filled selection style rather than the default bordered style, as shown here:
There is another ListView visual that is a bit like styling but not affected by styling. This is called the backdrop, an effect that’s turned on by default when you use the GridLayout. On low-end hardware, especially low-power mobile devices, panning around quickly in a ListView can very easily outpace the control’s ability to load and render items. To give the user a visual indication of what they’re doing, the GridLayout displays a simple backdrop of item outlines based on the default item size and pans that backdrop until such time as real items are rendered. As we’ll see in the next section, you can disable this feature with the GridLayout’s disableBackdrop
property and override its default gray color with the backdropColor
property.
The ListView’s layout
property, which you can set at any time, contains an object that’s used to organize the list’s items. WinJS provides two prebuilt layouts: WinJS.UI.GridLayout
and WinJS.UI.ListLayout
. The first, already described earlier in this chapter, provides a horizontally panning two-dimensional layout that places items in columns (top to bottom) and then rows (left to right). The second is a one-dimensional top-to-bottom layout, suitable for vertical lists (as in snapped view). These both follow the recommended design guidelines for presenting collections.
Technically speaking, the layout
property is an object in itself, containing some number of other properties along with a type
property. Typically, you see the syntax layout: { type: <layout> }
in a ListView’s data-win-options
string, where <layout>
is WinJS.UI.GridLayout
or WinJS.UI.ListLayout
(technically, the name of a constructor function). In the declarative usage, layout
can also contain options that are specific to the type. For example, the following configures a GridLayout with headers on the left and four rows:
layout: { type: WinJS.UI.GridLayout, groupHeaderPosition: 'left', maxRows: 4 }
If you create the layout object in JavaScript by using new
to call the constructor directly (and assigning it to the layout
property), you can specify additional options with the constructor. This is done in the Grid App project template’s initializeLayout
method in pages/groupedItems/groupedItems.js:
listView.layout = new ui.GridLayout({ groupHeaderPosition: "top" });
You can also set properties on the ListView’s layout
object in JavaScript once it’s been created, if you want to take that approach. Changing properties will generally update the layout.
In any case, each layout has its own unique options. For GridLayout, we have these:
• groupHeaderPosition
controls the placement of headers in relation to their groups; can be set to "left"
or "top".
• maxRows
controls the number of items the layout will place vertically before starting another column.
• backdropColor
provides for customizing the default backdrop color (see “Backdrops” in the previous section), and disableBackdrops
turns off the effect entirely.
• groupInfo
identifies a function that returns an object whose properties indicate whether cell spanning should be used and the size of the cell (see below). This is called only once within a layout process.
• itemInfo
identifies a function for use with cell spanning that returns an object of properties describing the exact size for each item and whether the item should be placed in a new column (see below).
The GridLayout also has a read-only property called horizontal
that’s always true
. As for the ListLayout, its horizontal
property is always false
and has no other configurable properties.
Now, because the ListView’s layout
property is just an object (or the name of a constructor for such an object), can you create a custom layout function of your own? Yes, you can: create a class that provides the same public methods as the built-in layouts, as described by the (currently under-documented) WinJS.UI.Layout
class. From there the layout object can provide whatever other options (properties and methods) are applicable to it.
Now before you start thinking that you might need a custom layout, the GridLayout provides for something called cell spanning that allows you to create variable-sized items (not an option for ListLayout). This is what its groupInfo
and itemInfo
properties are for, as demonstrated in Scenarios 4 and 5 of the HTML ListView item templates sample and shown in Figure 5-9.
FIGURE 5-9 The ListView item templates sample showing multisize items through cell spanning.
The basic idea of cell spanning is to define a grid for the GridLayout based on the size of the smallest item (including padding and margin styles). For best performance, make the grid as coarse as possible, where every other element in the ListView is a multiple of that size.
You turn on cell spanning through the GridLayout’s groupInfo
property. This is a function that returns an object with three properties: enableCellSpanning
, which should be set to true
, and cellWidth
and cellHeight
, which contain the pixel dimensions of your minimum cell (which, by the way, is what the GridLayout’s backdrop feature will use for its effects in this case). In the sample (see js/data.js), this function is named groupInfo like the layout’s property. I’ve given it a different name here for clarity:
function cellSpanningInfo() { return { enableCellSpanning: true, cellWidth: 310, cellHeight: 80 }; }
You then specify this function as part of the layout
property in data-win-options
:
layout: { type: WinJS.UI.GridLayout, groupInfo: cellSpanningInfo }
or you can set layout.groupInfo
from JavaScript. In any case, once you’ve announced your use of cell spanning, your item template should set each item’s style.width
and style.height
properties, plus applicable padding values, to multiples of your cellWidth
and cellHeight
according to the following formulae (which are two arrangements of the same formula):
templateSize = ((cellSize + margin) x multiplier) - margin
cellSize = ((templateSize + margin) / multiplier) - margin
In the sample, these styles are set by assigning each item one of three class names: smallListIconTextItem
, mediumListIconTextItem
, and largeListIconTextItem
, whose relevant CSS is as follows (from css/scenario4.css and css/scenario5.css):
.smallListIconTextItem { width: 300px; height: 70px; padding: 5px; } .mediumListIconTextItem { width: 300px; height: 160px; padding: 5px; } .largeListIconTextItem { width: 300px; height: 250px; padding: 5px; }
Because each of these classes has padding, their actual sizes from CSS are 310x80, 310x170, and 310x260. The margin to apply in the formula comes from the win-container
style in the WinJS stylesheet, which is 5px. Thus:
((80 + 10) * 1) – 10 = 80; minus 5px padding top and bottom = a height of 70px in CSS
((80 + 10) * 2) – 10 = 170; minus 5px padding = height of 160px
((80 + 10) * 3) – 10 = 260; minus 5px padding = height of 250px
The only difference between Scenario 4 and Scenario 5 is that the former assigns class names to the items through a template function. The latter does it through a declarative template and data-binds the class name to an item data field containing those values.
As for the itemInfo
function, this is a way to optimize the performance of a ListView when using cell spanning. Without assigning a function to this property, the GridLayout has to manually determine the width and height of each item as it’s rendered, and this can get slow if you pan around quickly with a large number of items. Since you probably already know item sizes yourself, you can return that information through the itemInfo
function. This function receives an item index and returns an object with the item’s width
and height
properties. (We’ll see a working example in a bit.)
function itemInfo(itemIndex) { //determine values for itemWidth and itemHeight given itemIndex return { newColumn: false, itemWidth: itemWidth, itemHeight: itemHeight }; }
Clearly, this function will be called for every item in the list but only if cell spanning has been turned on through the groupInfo
function. Again, unless your list is relatively small, you’ll very much improve performance by providing item dimensions through this function, but be sure to return as quickly as you can.
You probably also noticed that newColumn
property in the return value. As you might guess, setting this to true
instructs the GridLayout to start a new column with this item, allowing you to control that particular behavior. You can even use newColumn
by itself, if you like, with a smallish list.
Now you might be asking: what happens if I set different sizes in my item template but don’t actually announce cell spanning? Well, you’ll end up with overlapping (and rather confusing) items. This is because the GridLayout takes the first item in a group as the basic measure for the rest of the items (and the backdrop grid as well). It does not attempt to automatically size each item according to content. Try this with Scenarios 4 or 5: remove the layout.groupInfo
property from the ListView’s data-winoptions
in html/scenario4.html or html/scenario5.html and restart the app, and you’ll see the medium and large items bleeding into those that follow:
Then go into js/data.js, set the first item’s style in the myCellSpanningData
array to be largeListIconTextItem
, and restart; the ListView then does layout with that as the basic item size:
Using the first item’s dimension like this underscores the fact that a ListView with cell spanning will take more time to render because it must measure each item as it gets laid out, with or without the itemInfo
function. For this reason, cell spanning is probably best avoided for large lists.
Where all this gets a little more interesting, which the sample doesn’t show, is how the GridLayout deals with items that vary in width. Its basic algorithm is still to lay out columns from top to bottom and then left to right, but it will now infill empty spaces next to smaller items when larger ones create those gaps. To demonstrate this, let’s modify the sample so that the smallest item is 155x80 (half the original size), the medium item is 310x80, and the large item is 310x160. Here are the changes to make that happen:
1. Undo any changes from the previous tests: in html/scenario4.html, add groupInfo
back to data-win-options
, and in js/data.js, change the class in the first item of myCellSpanningData
back to smallListIconTextItem
.
2. In js/data.js, change the cellWidth
in groupInfo
to 155 (half of 310) and leave cellHeight
at 80. For clarity, also insert an incrementing number at the start of each item’s text in myCellSpanningData
array.
3. In css/scenario4.css:
a. Change the width of smallListIconTextItem
to 145px. Applying the formula, ((145+10) * 1) – 10 = 145. Height is 70px.
b. Change the width of mediumlListIconTextItem
to 310px and the height to 70px.
c. Change the width of largelListIconTextItem
to 310px and the height to 160px. Applying the formula to the height, ((80+10) *2 ) – 10 = 170px.
d. Set the width
style in the #listview
rule to 800px and the height
to 600px (to make more space in which to see the layout).
I recommend making these changes in Blend where your edits are reflected more immediately than running the app from Visual Studio. In any case, the results are shown in Figure 5-10 where the numbers show us the order in which the items are laid out (and apologies for clipping the text…experiments must make sacrifices at times!). A copy of the sample with these modifications is provided in the companion content for this chapter.
FIGURE 5-10 Modifying the ListView item templates sample to show cell spanning more completely.
In the modified sample I’ve also included an itemInfo
function in js/data.js, as you may have already noticed. It returns the item dimensions according to the type specified for the item:
function itemInfo(index) { //getItem(index).data retrieves the array item from a 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 }; }
You can set a breakpoint in this function and verify that it’s being called for every item; you can also see that it produces the same results. Now change the return value of newColumn
as follows, to force a column break before item #7 and #15 in Figure 5-10, because they oddly span columns:
newColumn: (index == 6 || index == 14), //Break on items 7 and 15 (index is 6 and 14)
The result of this change is shown in Figure 5-11.
FIGURE 5-11 Using new columns in cell spanning on items 7 and 15.
One last thing I noticed while playing with this sample is that if the item size in a style rule like smallListIconTextItem
ends up being smaller than the size of a child element, such as .regularListIconTextItem
(which includes margin and padding), the larger size wins in the layout. As you experiment, you might want to remove the default 5px margin that’s set for win-container
. This is what creates the space between the items in Figure 5-10 but has to be added into the equations. The following rule will set that margin to 0px:
#listView > .win-horizontal .win-container { margin: 0px; }
I’ve often told people that there’s so much you can do and learn about ListView that it could be a book in itself! Indeed, it would have been easy for Microsoft to have just created a basic control that let you create templated items and have left it at that. However, knowing that the ListView would be utterly central to a large number of apps (perhaps the majority outside the gaming category), and expecting that the ListView would be called upon to host thousands or even tens of thousands of items, a highly skilled and passionate group of engineers has gone to great extremes to provide many levels of sophistication that will help your apps perform their best.
One optimization is the ability to demand-load pages of items as determined by the ListView’s loadingBehavior
property, as described in the next two sections. The other optimization is to use template functions to delay-load different parts of each item, such as images, as well as to defer actions like animations until an item actually becomes visible, which is covered in the third section below. In all cases, the whole point of these optimizations is to help the ListView display the most important items or parts of items as quickly as possible, deferring the loading and rendering of other items or less important item elements until they’re really needed.
I did want to point out that the Using ListView topic in the documentation contains even more suggestions than I’m able to include here. (I do have other chapters to write!) I encourage you to study that topic as well, and who knows—maybe you’ll be the one to write the complete ListView book! Furthermore, additional guidance on appwide performance can be found on Performance best practices for Windows Store apps using JavaScript, which contains the Using ListView topic.
If you’re like myself and others in my family, you probably have an ever-increasing stockpile of digital photographs that make you glad that 1TB+ hard drives keep dropping in price. In other words, it’s not uncommon for many consumers to have ready access to collections of tens of thousands of items that they will at some point want to pan through in a ListView. But just imagine the overhead of trying to load thumbnails for every one of those items into memory to display in a list. On low-level and low-power hardware, you’d probably be causing every suspended app to be quickly terminated, and the result will probably be anything but “fast and fluid”! The user might end up waiting a really long time for the control to become interactive and will certainly get tired of watching a progress ring.
With this in mind, the default loadingBehavior
property for a ListView is set to "randomaccess"
. In this mode, the ListView’s scrollbar will reflect the total extent of the list so that the user has some idea of its size, but the ListView keeps a total of only five pages or screenfuls of items in memory at any given time (with an overall limit of 1000 items). For most pages, this means the visible page (in the viewport) plus two buffer pages ahead and behind. (If you’re viewing the first page, the buffer extends four pages ahead; if you’re on the last page, the buffer extends four pages behind—you get the idea.)
Whenever the user pans to a location in the list, any pages that fall out of the viewport or buffer zone are discarded (almost—we’ll come back to this in a moment), and loading of the new viewport page and its buffer pages begins. Thus the ListView’s loadingState
property will start again at itemsLoading
, then to viewportLoaded
when the visible items are rendered, then itemsLoaded
when the buffered pages are loaded, and then complete
when everything is done. Again, at any given time, only five pages of items are loaded into memory.
Now when I said that previously loaded items get discarded when they move out of the viewport/buffer range, what actually happens is that the items get recycled. One of the most expensive parts of rendering an item is creating its DOM elements, so the ListView actually takes those elements, moves them to a new place in the list, and fills them in with new content. This will become important when we look at optimization in template functions.
Apart from potentially very large but known collections, other collections are, for all intents and purposes, essentially unbounded, like a news feed that might have millions of items stretching back to the Cenozoic Era (at least by Internet reckoning!). With such collections, you probably won’t know just how many items there are at all; the best you can really do is just load another chunk when the user wants them.
This is what the loadingBehavior
of "incremental"
is for. In this mode, the ListView’s scrollbar will reflect only what’s loaded in the list, but if the user passes a particular threshold—for instance, they pan to the end of the list—the ListView will ask the data source for more pages of items and add them to the list, updating the scrollbar. In this case, all of the loaded items remain loaded, providing for very quick panning within the loaded list but with potentially more memory consumption than random access.
The incremental loading behavior is demonstrated in Scenarios 2 and 3 of the ListView loading behaviors sample. (Scenario 1 covers random access, but it’s nothing different than we’ve already seen.) Incremental loading activates the following characteristics:
• The ListView’s pagesToLoad
property indicates how many pages or screenfuls of items get loaded at a time. The default value is five.
• The automaticallyLoadPages
property indicates whether the ListView should load new pages automatically as you pan through the list. If true
(the default), as demonstrated in Scenario 2, as you pan toward the end of the list you’ll see the scrollbar change as new pages are loaded. If false
, as demonstrated in Scenario 3, new pages are not loaded until you call the loadMorePages
method directly.
• When automaticallyLoadPages
is true
, the pagesToLoadThreshold
property indicates how close the user can get to the current end of the list before new page loads are triggered. The default value is two.
• When new pages start to load (either automatically or in response to loadMorePages
), the ListView will start updating the loadingState
property firing loadingstatechanged
events as described already.
What we just discussed with the ListView’s loading behavior options pertains to the incremental loading of pages. It’s helpful now to combine this with incremental loading of items. For that, we need to look at what’s sometimes referred to as the rendering pipeline as implemented in template functions.
When we first looked at template functions earlier (see “How Templates Really Work”), I noted that they give us control over both how and when items are constructed and that such functions—again, called renderers—are the means through which you can implement five progressive levels of optimization for ListView (and FlipView, though this is less common). Just using a renderer, as we already saw, is level 1; now we’re ready to see the other four levels. This is a fascinating subject, because it shows the kind of sophistication that the ListView has implemented for us!
Our context for this discussion is the HTML ListView optimizing performance sample that demonstrates all these levels and allows you to see their respective effects. Here’s an overview:
• A simple or basic renderer allows control over the rendering on a per-item basis.
• A placeholder renderer separates creation of the item element into two stages. The first stage returns only those elements that define the shape of the item. This allows the ListView to quickly do its overall layout before all the details are filled in, especially when the data is coming from a potentially slow source. When item data is available, the second stage is then invoked to copy that data into the item elements and create additional elements that don’t contribute to the shape.
• A recycling placeholder renderer adds the ability to reuse an existing chunk of DOM for the item, which is much faster that having to create it all from scratch. For this purpose, the ListView, knowing that it will be frequently paged around, holds onto some number of item elements when they go offscreen. In your renderer, you add a code path to clean up a recycled element if one is given to you, and return that as your placeholder. You then populate it with values in the second stage of rendering.
• A multistage renderer extends the recycling renderer both to delay-load images and other media until the item element is fully built up in the ListView and also to delay any visibility-related actions, such as animations, until the item is actually on-screen.
• Finally, a multistage batching renderer adds the ability to add images and other media as a batch, thereby rendering and possibly animating their entrance into the ListView as a group such that the system’s GPU can be employed more efficiently.
With all of these renderers, you should strive to make them execute as fast as possible. Especially minimize the use of DOM API calls, which includes setting individual properties. Use an innerHTML
string where you can to create elements rather than discrete calls, and minimize your use of getElementById
, querySelector
, and other DOM-traversal calls by caching the elements you refer to most often. This will make a big difference.
To visualize the effect of these improvements, the following graphic shows an example of how unoptimized ListView rendering typically happens:
The yellow bars indicate execution of the app’s JavaScript—that is, time spent inside the renderer. The beige bars indicate the time spent in DOM layout, and aqua bars indicate actual rendering to the screen. As you can see, when elements are added one by one, there’s quite a bit of breakup in what code is executing when, and the kicker here is that most display hardware refreshes only every 10–20 milliseconds (50–100Hz). As a result, there’s lots of choppiness in the visual rendering.
After making improvements, the chart can look like the one below, where the app’s work is combined in one block, thereby significantly reducing the DOM layout process (the beige):
As for all the other little bits in these graphics, they come from the performance tool called XPerf that’s part of the Windows SDK (see sidebar). Without studying the details, what ultimately matters is that we understand the steps you can take to achieve these ends—namely, the different forms of renderers that you can employ as demonstrated in the sample.
The XPerf tool in the Windows SDK, which is documented on Windows Performance Analysis Tools, can very much help you understand how your app really behaves on a particular system. Among other things, it logs calls you make to msWriteProfilerMark
, as you’ll find sprinkled throughout the WinJS source code itself. For these to show up in xperf, however, you need to start logging with this command:
xperf –start user –on PerfTrack+Microsoft-IE:0x1300
and end logging with this one, where <trace_filename> is any path and filename of your choosing:
xperf –stop user –d <trace_filename>.etl
Launching the .etl file you save will run the Windows Performance Analyzer and show a graph of events. Right-click the graph, and then click “Summary Table”. In that table, expand Microsoft-IE and then look for and expand the Mshtml_DOM_CustomSiteEvent node. The Field3 column should have the text you passed to msWriteProfilerMark
, and the Time(s) column will help you determine how long actions took.
As a baseline for our discussion, here is a simple renderer:
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; }); }
Again, this structure waits for the item data to become available, and it returns a promise for the element that will be fulfilled at that time.
A placeholder renderer separates building the element into two stages. The return value is an object that contains a minimal placeholder in the element
property and a renderComplete
promise that does the rest of the work when necessary:
function placeholderRenderer(itemPromise) { // create a basic template for the item which doesn't depend on the data var element = document.createElement("div"); element.className = "itemTempl"; element.innerHTML = "<div class='content'>...</div>"; // return the element as the placeholder, and a callback to update it when data is available return { element: element, // specifies a promise that will be completed when rendering is complete // itemPromise will complete when the data is available renderComplete: itemPromise.then(function (item) { // mutate the element to include the data element.querySelector(".content").innerText = item.data.title; element.insertAdjacentHTML("afterBegin", "<img src='" + item.data.thumbnail + "' alt='Databound image' />"); }) }; }
The element
property, in short, defines the item’s shape and is returned immediately from the renderer. This lets the ListView do its layout, after which it will fulfill the renderComplete
promise. You can see that renderComplete
essentially contains the same sort of thing that a simple renderer returns, minus the already created placeholder elements. (For another example, the added Scenario 8 of the FlipView example in this chapter’s companion content has commented code that implements this approach.)
A recycling placeholder renderer now adds awareness of a second parameter called recycled
that the ListView (but not the FlipView) can provide to your rendering function when the ListView’s loadingBehavior
is set to "randomaccess"
. If recycled
is given, you can just clean out the element, return it as the placeholder, and then fill in the data values within the renderComplete
promise as before. If it’s not provided (as when the ListView is first created or when loadingBehavior
is "incremental"
), you’ll create the element anew:
function recyclingPlaceholderRenderer(itemPromise, recycled) { var element, img, label; if (!recycled) { // create a basic template for the item which doesn't depend on the data element = document.createElement("div"); element.className = "itemTempl"; element.innerHTML = "<img alt='Databound image' style='visibility:hidden;'/>" + "<div class='content'>...</div>"; } else { // clean up the recycled element so that we can re-use it element = recycled; label = element.querySelector(".content"); label.innerHTML = "..."; img = element.querySelector("img"); img.style.visibility = "hidden"; } return { element: element, renderComplete: itemPromise.then(function (item) { // mutate the element to include the data if (!label) { label = element.querySelector(".content"); img = element.querySelector("img"); } label.innerText = item.data.title; img.src = item.data.thumbnail; img.style.visibility = "visible"; }) }; }
In renderComplete
, be sure to check for the existence of elements that you don’t create for a new placeholder, such as label
, and create them here if needed.
If you’d like to clean out recycled items, you can also provide a function to the ListView’s resetItem
property that would contain the same code as shown above for that case. The same is true for the resetGroupHeader
property, because you can use template functions for group headers as well as items. We haven’t spoken of these as much because group headers are far fewer and don’t typically have the same performance implications. Nevertheless, the capability is there.
Next we have the multistage renderer, which extends the recycling placeholder renderer to delay-load images and other media until the rest of the item is wholly present in the DOM, and to further delay effects like animations until the item is truly on-screen.
The hooks for this are three methods called ready
, loadImage
, and isOnScreen
that are attached to the item provided by the itemPromise
. The following code shows how these are used (where element.querySelector
traverses only a small bit of the DOM, so it’s not a concern):
renderComplete: itemPromise.then(function (item) { // mutate the element to update only the title if (!label) { label = element.querySelector(".content"); } label.innerText = item.data.title; // use the item.ready promise to delay the more expensive work return item.ready; // use the ability to chain promises, to enable work to be cancelled }).then(function (item) { //use the image loader to queue the loading of the image if (!img) { img = element.querySelector("img"); } return item.loadImage(item.data.thumbnail, img).then(function () { //once loaded check if the item is visible return item.isOnScreen(); }); }).then(function (onscreen) { if (!onscreen) { //if the item is not visible, then don't animate its opacity img.style.opacity = 1; } else { //if the item is visible then animate the opacity of the image WinJS.UI.Animation.fadeIn(img); } })
I warned you that there would be promises aplenty in these performance optimizations! But all we have here is the basic structure of chained promises. The first async operation in the renderer updates simple parts of the item element, such as text. It then returns the promise in item.ready
. When that promise is fulfilled—or, more accurately, if that promise is fulfilled—you can use the item’s async loadImage
method to kick off an image download, returning the item.isOnScreen
promise from that completed handler. When and if that isOnScreen
promise is fulfilled, you can perform those operations that are relevant only to a visible item.
I emphasize the if part of all this because it’s very likely that the user will be panning around within the ListView while all this is happening. Having all these promises chained together makes it possible for the ListView to cancel the async operations any time these items are scrolled out of view and/or off any buffered pages. Suffice it to say that the ListView control has gone through a lot of performance testing!
Which brings us to the final multistage batching renderer, which combines the insertion of images in the DOM to minimize layout and repaint work. In the sample, this uses a function called createBatch
that utilizes WinJS.Promise.timeout
method with a 64-millisecond period to combine the image-loading promises of the multistage renderer. Honestly, you’ll have to trust me on this one, because you really have to be an expert in promises to understand how it works!
//During initialization (outside the renderer) thumbnailBatch = createBatch(); //Within the renderComplete chain //... }).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) { //... //The implementation of 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; }); }; }; }
Did I warn you about there being promises in your future? Well, fortunately, we’ve now exhausted the subject of template functions, but it’s time well spent because optimizing ListView performance, as I said earlier, will greatly improve consumer perception of apps that use this control.
• In-memory collection data is managed through WinJS.Binding.List
, which integrates nicely with collection controls like FlipView and ListView. In-memory collections can come from sources like WinJS.xhr
and data loaded from files.
• The WinJS.UI.FlipView
control displays one item at a time; WinJS.UI.ListView
displays multiple items according to a specific layout.
• Central to both controls is the idea that there is a data source and an item template used to render each item in that source. Templates can be either declarative or procedural.
• ListView works with the added notion of layout. WinJS provides two built-in layouts. GridLayout is a two-dimensional, horizontally panning list; ListLayout is for a one-dimensional vertically panning list. It is also possible to implement custom layouts.
• ListView provides the capability to display items in groups; WinJS.BindingList
provides methods to created grouped, sorted, and filtered projections of items from a data source.
• The Semantic Zoom control (WinJS.UI.SemanticZoom
) provides an interface through which you can switch between two different views of a data source, a zoomed-in (details) view and a zoomed-out (summary) view. The two views can be very different in presentation but should display related data. The IZoomableView
interface is required on each of the views so that the Semantic Zoom control can switch between them and scroll to the correct item.
• WinJS provides a StorageDataSource
to create a collection over StorageFile
items.
• It is possible to implement custom data sources, as shown by samples in the Windows SDK.
• Procedural templates are implemented as template function, or renderers. These functions can implement progressive levels of optimization for delay-loading images and adding items to the DOM in batches.
• Both FlipView and ListView controls provide a number of options and styling capabilities. ListView also provides for item selection and different selection behaviors.
• The ListView control provides built-in support for optimizing random access of large data sources, as well as incremental access of effectively unbounded data sources.
• The ListView control supports the notion of cell spanning in its GridLayout to support items of variable size, which should all be multiples of a basic cell size.