WinJS List view binding with observable objects

I have been looking over posts and questions where there are asks on how to do a binding in list view. The questions are more concentrated on how to capture user interactions on an item and change it’s UI to make it look as selected/unselected and also store the user interactions. In this series of post I will be covering some scenarios that I have come across.

The key in such scenarios is to use WinJS binding objects. The WinJS Binding objects are observable objects that track changes in the property values and raises an event. More or less it is same as INotifyPropertyChanged in WPF (if you are familiar).

This is start of the series. In this post we will look at how to bind a list view , allow the user to click and select/unselect the item. The appearance of the item will change on the state (selected/unselected).

Here is the overall scenario:
1. We have a list that is bound to some data (dummy products data) and show the products.
2. By default (unselected state) the details of the items will displayed, the background will be Light gray and fonts will be black.
3. When the user clicks on an item, it will be in selected state, the change on UI will be that the background will be dark gray and the fonts will be in white.
4. If the user clicks a selected item, it will be unselected and revert back to default setting and UI.

Simple enough… Lets start then 🙂

Let us first create the project. In visual studio create a new grid application (File ==> New Project ==> Other Languages ==> JavaScript ==> Windows store ==> Grid App). This will create a new project. I use the Grid application because some default code are added such as navigation.js etc.

Under the pages create a new folder called “ListViewBinding”. In this new folder lets add three files “listViewBinding.css”, “listViewBinding.js”, “listViewBinding.html”.

In prefer the light theme so in default I will change the css to point to light theme, so I change the CSS to light as:

 <link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
 

In the default.html lets navigate to the listViewBinding.html that we created:

 <div id="contenthost" data-win-control="Application.PageControlNavigator" data-win-options="{home: 'pages/ListViewBinding/listViewBinding.html'}"></div>
 

Now since we are done and we will navigate to our page when the app load, lets design our listViewBinding.html.

Our item data will have the following properties :
1. ProductName : The product name
2. Weight : The product weight
3. Color : The product Color
4. SalesPrice : The price for the Product
5. IsSelected : Flag to determine if item is selected or not.

First we will create the body section and add the list view.

 <section aria-label="Main content" role="main">
            <div class="mainList win-selectionstylefilled" data-win-control="WinJS.UI.ListView" data-win-options="{ selectionMode: 'none' }"></div>
        </section>
 

Second we will create a template for the list view items. My items is very simple , It shows the Product name and some product details such as Weight, Color and Sales price. So I declare the template as below. I have used data-win-bind to bind the properties to show the values (such as productname, weight etc..). I have also used style binding to background and font color. In the style binding I have used a converter which accepts the IsSelectedFlag. I will get to the converter later.

 <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item" data-win-bind="style.background : IsSelected TemplateConverters.SelectedBackground;title: ProductName">
            <div class="productName win-type-ellipsis" data-win-bind="title: ProductName">
                <h3 class="nameLabel" data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;textContent: ProductName"></h3>
            </div>

            <div class="details win-type-ellipsis" data-win-bind="title: LevelName">
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> <u> Details</u> </b> </span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Weight : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Weight"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Color : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Color"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Sales Price : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: SalesPrice"></span>
            </div>
        </div>
    </div>

Now since most of the design that we need to do on listViewBinding.html is completed , lets create the converter. Under data.js we will add a new file called “converters.js” and declare the two converters there.

We are using two converters :
1. To give the background color based on the IsSelected Flag state.

        SelectedBackground: WinJS.Binding.converter(function (value) {
            return !value ? "WhiteSmoke" : "DarkGray";
        })

2. To give the font color based on IsSelected Flag state.

        SelectedFontColor: WinJS.Binding.converter(function (value) {
            return !value ? "Black" : "White";
        })

Now to style . In “listViewBinding.css” we will create a style for template. I am going for a simple styling with two rows. One row to display the Product name and another to display the details.

.samplepage .mainList .item {
        -ms-grid-columns: 1fr;
        -ms-grid-rows: 0.5fr 1fr;
        display: -ms-grid;
        height: 120px;
        width: 300px;
        background-color:whitesmoke;
        border:1px solid black;
    }

        .samplepage .mainList .item .productName {
            -ms-grid-row:1;
            margin-left:20px;
        }
        .samplepage .mainList .item .details {
            -ms-grid-row:2;
            margin-left:20px;
        }
        

Now to the code for this page.

First we will define the class for the item


var product =  WinJS.Binding.define({
        ProductName : "",
        Weight : 0,
        Color : "",
        SalesPrice: 0.0,
        IsSelected : false
    });

In the page definition we will declare a variable for the list view binding list.


 ui.Pages.define("/pages/ListViewBinding/listViewBinding.html", {

        listData: null,
.........

In the ready event we will first get the data, initialize the list ( set’s it template, datasource and layout). In the below code you will observable that while adding items I add then as “WinJS.Binding.as(…)”. This makes my object observable and will react to any change that is done on the property.

ready: function (element, options) {
            this.listData = new WinJS.Binding.List();
            this.createDummyData();
            var listview = element.querySelector(".mainList").winControl;
            listview.itemDataSource = this.listData.dataSource;
            listview.itemTemplate = element.querySelector(".itemtemplate");
            listview.oniteminvoked = this.itemInvoked.bind(this);
            this.initializeLayout(listview, appView.value);
        },

        initializeLayout: function (listView, viewState) {
            if (viewState === appViewState.snapped) {
                listView.layout = new ui.ListLayout();
            } else {
                listView.layout = new ui.GridLayout();
            }
        },
createDummyData: function () {
            // Creating dummy data .
            //Data format : Name, Quantity, SalesPrice, IsSelected 

            // TODO : Replace your data creation logic and push into the main list

            this.listData.push(WinJS.Binding.as(new product({ ProductName: "Front Brakes", Weight: 317, Color: "Silver", SalesPrice: 47.286, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "Front Derailleur", Weight: 88, Color: "Silver", SalesPrice: 40.6216, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Bottom Bracket", Weight: 170, Color: "NA", SalesPrice: 53.9416, IsSelected: false })));

///............. More data to be added
        }

Now in the item invoked method for the list we will switch the “IsSelected” flag. This will set the flag to true if current state is false and vice-versa.

itemInvoked: function (args) {
            var dataItem = this.listData.getAt(args.detail.itemIndex);
            dataItem.IsSelected = !dataItem.IsSelected;
        }

Now if you run the code you can select and unselect the items and the background and font color should change as the flag changes.

You can store/remove the selected items in another list (in the iteminvoked method) if you want to add additional functionality on the items that are selected. Also you could just move over the list and filter out the items where IsSelected = true to get a list of selected items.

The full source code is given below. Has additional styles and script for snapped view.

listviewbinding.html

<!--THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.-->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>ListView TwoWay Binding Sample</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <!-- ListViewTwoWayBinding references -->
    <link href="/pages/ListViewBinding/listViewBinding.css" rel="stylesheet" />
    <script src="/pages/ListViewBinding/listViewBinding.js"></script>
    <script src="/js/converters.js"></script>
</head>
<body>
    <!-- These templates are used to display each item in the ListView declared below. -->

    <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item" data-win-bind="style.background : IsSelected TemplateConverters.SelectedBackground;title: ProductName">
            <div class="productName win-type-ellipsis" data-win-bind="title: ProductName">
                <h3 class="nameLabel" data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;textContent: ProductName"></h3>
            </div>

            <div class="details win-type-ellipsis" data-win-bind="title: LevelName">
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> <u> Details</u> </b> </span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Weight : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Weight"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Color : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Color"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Sales Price : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: SalesPrice"></span>
            </div>
        </div>
    </div>

    <!-- The content that will be loaded and displayed. -->
    <div class="samplepage fragment">
        <header aria-label="Header content" role="banner">
           <button class="win-backbutton" aria-label="Back" disabled type="button"></button>
             <h1 class="titlearea win-type-ellipsis">
                 <span class="pagetitle">List view Binding Sample</span>
             </h1>
        </header>
        <section aria-label="Main content" role="main">
            <div class="mainList win-selectionstylefilled" data-win-control="WinJS.UI.ListView" data-win-options="{ selectionMode: 'none' }"></div>
        </section>
    </div>
</body>
</html>

listviewbinding.css

/*THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.*/

.samplepage section[role=main] {
    -ms-grid-row: 1;
    -ms-grid-row-span: 2;
}

.samplepage .mainList {
    height: 100%;
    position: relative;
    width: 100%;
    z-index: 0;
}

    /* This selector is used to prevent ui-dark/light.css from overwriting changes
       to .win-surface. */
    .samplepage .mainList .win-horizontal.win-viewport .win-surface {
        margin-bottom: 60px;
        margin-left: 45px;
        margin-right: 115px;
        margin-top: 128px;
    }

    .samplepage .mainList .win-container {
        outline:none;
        background-color:transparent;
    }

    .samplepage .mainList .win-container:hover {
        outline:none;
        background-color:transparent;
    }

    .samplepage .mainList .item {
        -ms-grid-columns: 1fr;
        -ms-grid-rows: 0.5fr 1fr;
        display: -ms-grid;
        height: 120px;
        width: 300px;
        background-color:whitesmoke;
        border:1px solid black;
    }

        .samplepage .mainList .item:hover {
            border:2px solid black;
        }

        .samplepage .mainList .item .productName {
            -ms-grid-row:1;
            margin-left:20px;
        }
        .samplepage .mainList .item .details {
            -ms-grid-row:2;
            margin-left:20px;
        }

@media screen and (-ms-view-state: snapped) {
    .samplepage section[role=main] {
        -ms-grid-row: 2;
        -ms-grid-row-span: 1;
    }

    .samplepage .mainList .win-vertical.win-viewport .win-surface {
        margin-bottom: 30px;
        margin-top: 0;
    }

    .samplepage .mainList .win-container {
        margin-bottom: 15px;
        margin-left: 10px;
        margin-right: 20px;
        padding: 7px;
    }

    .samplepage .mainList .item {
        height: 120px;
        width: 250px;
    }
}

listviewbinding.js

//// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
//// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
//// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//// PARTICULAR PURPOSE.
(function () {
    "use strict";

    var appView = Windows.UI.ViewManagement.ApplicationView;

    var appViewState = Windows.UI.ViewManagement.ApplicationViewState;

    var nav = WinJS.Navigation;

    var ui = WinJS.UI;

    var product =  WinJS.Binding.define({
        ProductName : "",
        Weight : 0,
        Color : "",
        SalesPrice: 0.0,
        IsSelected : false
    });

    ui.Pages.define("/pages/ListViewBinding/listViewBinding.html", {

        listData: null,

        ready: function (element, options) {
            this.listData = new WinJS.Binding.List();
            this.createDummyData();
            var listview = element.querySelector(".mainList").winControl;
            listview.itemDataSource = this.listData.dataSource;
            listview.itemTemplate = element.querySelector(".itemtemplate");
            listview.oniteminvoked = this.itemInvoked.bind(this);
            this.initializeLayout(listview, appView.value);
        },

        initializeLayout: function (listView, viewState) {
            if (viewState === appViewState.snapped) {
                listView.layout = new ui.ListLayout();
            } else {
                listView.layout = new ui.GridLayout();
            }
        },

        updateLayout: function (element, viewState, lastViewState) {
            var listview = element.querySelector(".mainList").winControl;
            if (lastViewState !== viewState) {
                if (lastViewState === appViewState.snapped || viewState === appViewState.snapped) {
                    var handler = function (e) {
                        listview.removeEventListener("contentanimating", handler, false);
                        e.preventDefault();
                    }
                    listview.removeEventListener("contentanimating", handler, false);
                    this.initializeLayout(listview, viewState);
                }
            }
        },

        itemInvoked: function (args) {
            var dataItem = this.listData.getAt(args.detail.itemIndex);
            dataItem.IsSelected = !dataItem.IsSelected;
        },

        createDummyData: function () {
            // Creating dummy data .
            //Data format : Name, Quantity, SalesPrice, IsSelected 

            // TODO : Replace your data creation logic and push into the main list

            this.listData.push(WinJS.Binding.as(new product({ ProductName: "Front Brakes", Weight: 317, Color: "Silver", SalesPrice: 47.286, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "Front Derailleur", Weight: 88, Color: "Silver", SalesPrice: 40.6216, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Bottom Bracket", Weight: 170, Color: "NA", SalesPrice: 53.9416, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Crankset", Weight: 575, Color: "Black", SalesPrice: 179.8156, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Mountain Frame - Black, 38", Weight: 2.68, Color: "Black", SalesPrice: 617.0281, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Mountain Frame - Silver, 42", Weight: 2.72, Color: "Silver", SalesPrice: 623.8403, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Mountain Pedal", Weight: 185, Color: "Silver/Black", SalesPrice: 35.9596, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Frame - Black, 62", Weight: 2.3, Color: "Black", SalesPrice: 722.2568, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Frame - Red, 44", Weight: 2.12, Color: "Red", SalesPrice: 747.9682, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Front Wheel", Weight: 650, Color: "Black", SalesPrice: 146.5466, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Pedal", Weight: 149, Color: "Silver/Black", SalesPrice: 35.9596, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Rear Wheel", Weight: 890, Color: "Black", SalesPrice: 158.5346, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Touring Frame - Blue, 50", Weight: 3, Color: "Blue", SalesPrice: 601.7437, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Touring Frame - Yellow, 50", Weight: 3, Color: "Yellow", SalesPrice: 601.7437, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Bottom Bracket", Weight: 223, Color: "NA", SalesPrice: 23.9716, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Crankset", Weight: 600, Color: "Black", SalesPrice: 77.9176, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Mountain Frame - Black, 40", Weight: 2.88, Color: "Black", SalesPrice: 136.785, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Mountain Frame - Silver, 52", Weight: 3.04, Color: "Silver", SalesPrice: 144.5938, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Mountain Pedal", Weight: 218, Color: "Silver/Black", SalesPrice: 17.9776, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Road Frame - Black, 44", Weight: 2.32, Color: "Black", SalesPrice: 176.1997, IsSelected: false })));
                   }
    });
})();

converters.js

//// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
//// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
//// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//// PARTICULAR PURPOSE.
(function () {

    WinJS.Namespace.define("TemplateConverters", {

        // Return the background color based on selection
        SelectedBackground: WinJS.Binding.converter(function (value) {
            return !value ? "WhiteSmoke" : "DarkGray";
        }),
        // Return the font color based on selection
        SelectedFontColor: WinJS.Binding.converter(function (value) {
            return !value ? "Black" : "White";
        })
    });
})();

I hope this is useful.. Let me know if you have any question..

Here are some links

List view
Converters
Win JS Binding class

Cheers …

 

Disclaimer

Published by

Girija Shankar Beuria

A software developer by profession with 13+ years of experience in the following technologies : Data Warehousing, Business Intelligence applications using SQL Server BI Stack, Product Frameworks and Test automation framework, MOSS , C# .Net, .NET, POWERSHELL, AMO, HTML 5, JavaScript, Reporting Service Web service, Dynamics AX, Dynamics AX 2012 BI Cubes, Dynamics AX 2012 SSRS Reports, SQL Azure, Windows Azure Web Services, ASP .NET MVC 4 Web API, WCF, Entity Framework, WPF, Excel Object Model, Windows 8 Apps, Windows Phone Apps

2 thoughts on “WinJS List view binding with observable objects”

Leave a comment