Using WinJS.Binding.List to update the list view

Published December 3, 2013 1:06 pm

WinJS.UI.ListView is the most used control in WinJS library. At the beginning, you typically start with static List views. In such List Views, data does not change after it is bound. You may have to sort or group data which can be achieved using createSorted or createGrouped methods on List. You can order the items, group the items, and order the groups using these methods. If you follow some of the msdn listview samples, it is sort of clear. (Note: not all samples needs to be followed to get hang of list view. Some of them like custom data source, incremental loading are advanced scenarios).

When you working with dynamic data, list view updates as user interacts with the list view. Examples:

  1. User can right click a item, and delete it using contextual app bar command.
  2. User clicks/taps an item. The item is either removed or updated.

List view elements sort order and grouping needs to be maintained after the item change or list changes. In this post, let’s look into (2). I will try to cover (1) in another post. To go over this, let’s take a simple problem of displaying list of todo tasks.

  1. The tasks should be displayed in two  groups – ‘todo’ and ‘done’ groups.
  2. When a task is clicked or tapped in the todo group – it is moved to ‘done’ group.
  3. ‘done’ group should maintain only the latest 5 done tasks. oldest task gets off the list when list grows beyond 5 tasks.

my-tasks-1With this, Let’s look at the HTML

<div id="todoListView" data-win-control="WinJS.UI.ListView"
    data-win-bind="winControl.itemDataSource: items.dataSource; winControl.groupDataSource: items.groups.dataSource"
    data-win-options="{
      itemTemplate: select('#todoItemTemplate'),
      groupHeaderTemplate: select('#groupHeaderTemplate'),
      selectionMode: 'none', swipeBehavior: 'none', tapBehavior: 'invokeOnly' }">
</div>
  1. list should have tapBehavior set to invokeOnly to enable click/tap.
  2. itemTemplate and groupHeaderTemplate are set which is kind of obvious.
  3. this snippet binds itemDataSource and groupDataSource in html. If you don’t have view model (MVVM) separate in your code, you may set it in the .js file.

Let’s look at the sample data.

var data = [
    { status: 'todo', title: 'wake up' },
    { status: 'todo', title: 'brush your teeth' },
    { status: 'todo', title: 'take bath' },
    { status: 'todo', title: 'do yoga' },
    { status: 'todo', title: 'take breakfast' },
    { status: 'todo', title: 'check email' },
    { status: 'todo', title: 'check facebook' },
    { status: 'todo', title: 'check stackoverflow forum' },
];

We will sort the list using the sort key <status, finishTime>. finishTime will be added whenever user taps the item. todo status is sorted before done status. finishTime is sorted in descending order. This way – all done items are order towards end of the list. Further, If any item is to be dropped, it is at the end of the list.

 

    function compareStatus(t1, t2)
    {
        if (t1 == t2)
            return 0;
        else if (t1 == 'todo')
            return -1;
        else
            return 1;
    }

    function compareFinishTime(d1, d2)
    {
        if (d1 == undefined && d2 == undefined)
            return 0;
        // treat unfinished task ahead in sort order
        else if (d1 == undefined)
            return -1;
        else if (d2 == undefined)
            return 1;
        else
        {
            var t1 = d1.getTime(), t2 = d2.getTime();
            // treat recently finished task ahead in sort order
            if (t1 > t2)
                return -1;
            else
                return 1;
        }
    }

    var sortedList = list.createSorted(function compare(i1, i2)
    {
        var c1 = compareStatus(i1.status, i2.status);
        if (c1 != 0)
            return c1;
        var c2 = compareFinishTime(i1.finishTime, i2.finishTime);
        return c2;
    });

Further, we use createGrouped to group the sorted items and order the groups.

    this.items = sortedList.createGrouped(function groupKey(item)
    {
        return item.status;
    },
    function groupData(item)
    {
        return { title: item.status };
    }, 
    function groupSorter(g1, g2)
    {
        // keep todo items group b4 'done' items group
        if (g1 == g2)
            return 0;
        else if (g1 == 'todo')
            return -1;
        else 
            return 1;
    });

Now, to move the item from ‘todo’ group to ‘done’ group – we can change the item in the iteminvoked handler and notify the list for item mutation using notifyMutated method. Once notified, list sorted and grouped projection will update themselves. It will reflect in the bound list view.

 
registerItemInvokedHandler: function registerItemInvokedHandler()
{
    function oniteminvoked(event)
    {
        var index = event.detail.itemIndex;
        var item = this.viewModel.items.getAt(index);
        // click on 'done' tasks should be noop. 
        if (item.status == 'done')
            return;
        item.status = 'done';
        item.finishTime = new Date();
        this.viewModel.items.notifyMutated(index);
        // schedule removal for later using setImmediate
        WinJS.Promise.timeout().then(removeIfReqd.bind(this));
    }

    function removeIfReqd()
    {
        var count = 0;
        var items = this.viewModel.items;
        for (var i = items.length; i > 0; i--)
        {
            var item = items.getAt(i - 1);
            if (item.status == 'todo')
                break;
            count++;
        }

        // remove only if more than 5 'done' items
        if (count > 5)
            items.splice(items.length - 1, 1);
    }

    todoListView.addEventListener('iteminvoked', oniteminvoked.bind(this));
},

my-tasks-3* Note the code above references items from the viewModel. That needs to be modified to reference items as per your code.

This way – we saw in this post – how createSorted, createGrouped along with notifyMutated – can be used to build a dynamic list view.