Ioannis Panagopoulos blog

Tutorials on HTML5, Javascript, WinRT and .NET

Data binding in Windows 8.1 Apps with Knockout

by Ioannis Panagopoulos

During the implementation of a Windows 8 application and especially in the process of implementing the functionality of a single Page Control, one is faced with the question on whether to use the WinJS data-binding engine for his ViewModel or resort to a "web" solution such as Knockoutjs. This post is about data-binding using Knockoutjs (see the one for WinJS).

The page control (our view) will be as simple as possible to illustrate the most important aspects of each approach. The corresponding ViewModel to support the view in its "clean" form (without any structures added to support the binding requirements) is like that:

function VMClean() {
        this.listOfValues = [{ text: '-' }, ...];
        this.textValue = "-";
        this.inputValue = "-";
        this.buttonInvoked = function () {
            for (var i = 0; i < this.listOfValues.length; i++)
                this.listOfValues[i].text = (Math.random() * 100).toFixed(2);

            this.textValue = (Math.random() * 100).toFixed(2);
        };
        this.itemInvoked = function (item) {
            var md = new Windows.UI.Popups.MessageDialog(item.text);
            md.showAsync();
        }
    };

Below is the image of the view along with pointers of the properties/methods that are bound to specific elements:

Requirement: When I add/remove items from the list I need the change to be reflected in the UI.
Solution: (WinJS) Wrap the list in a ko.observableArray method

// this.listOfValues = [{ text: '-' }, ...];
this.listOfValues = ko.observableArray([{ text: '-' }, ...]);

Binding is: <div class="listView" data-bind="foreach:listOfValues">

Requirement: I need to be able to change the properties of the elements in the list and have the change reflected in the UI.
Solution: Wrap the JSON elements in ko.mapping.fromJS

// this.listOfValues = [{ text: '-' }, ...];
// this.listOfValues = ko.observableArray([{ text: '-' }, ...]);
this.listOfValues = ko.observableArray([ko.mapping.fromJS({ text: '-' }),...]);

Binding is in the listView item template as: <span data-bind="text:text">

Requirement: I need to bind the buttonInvoked method to the click event of the button.
Solution: No changes in the initial ViewModel (see the binding):

this.buttonInvoked = function () {
	// Your code here
};

Binding is: <button data-bind="click:buttonInvoked">

Requirement: Whenever the value of the input element changes we want the data bound value to change (TwoWay binding support)
Solution: No changes in the initial ViewModel (supported out of the box)

Requirement: When a value is displayed we need to add some extra characters in the end (for example a € symbol in prices)
Solution: Expressions are supported in the binding with no extra effort.

Binding is: <div data-bind="html:(textValue()*100).toFixed(2)+' €'">

Requirement: When you change a value in code you need the change to be reflected in the UI in an ordinary object and not in a JSON object through the use of ko.mapping.
Solution: Declare the property as ko.observable

function VM() {
    ...
    //this.textValue="-";
    this._textValue =ko.observable("-");
    ...
};

Requirement: Apply the ViewModel to a given element and its children
Solution: Execute the ko.applyBindings().

WinJS.UI.Pages.define("/pages/MVVMWinJS/MVVMWinJS.html", {
    ready: function (element, options) {
        ko.applyBindings(new VM(), element.querySelector(".dataBinding"));
    },
    ...

The final code of the ViewModel is:

function VM() {
    this.listOfValues = ko.observableArray([ko.mapping.fromJS({ text: '-' }), ...]);
    
    this.textValue = ko.observable("-");
    this.inputValue = ko.observable("-");
    this.buttonInvoked = function () {
        for (var i = 0; i < this.listOfValues().length; i++)
            this.listOfValues()[i].text(Math.random());

        this.textValue(Math.random());
    };
    this.itemInvoked = function (item) {
        var md = new Windows.UI.Popups.MessageDialog(item.text());
        md.showAsync();
    }
};


WinJS.UI.Pages.define("/pages/MVVMWinJS/MVVMWinJS.html", {
    ready: function (element, options) {
        ko.applyBindings(new VM(), element.querySelector(".dataBinding"));
    },
    ...
});

Wow. From 14 lines of code we went to 16 lines. But have we lost something?

ListView considerations

The new listView in knockoutjs is created with the foreach binding and although usable does not reflect the UI characteristics of the WinJS.UI.ListView. Therefore we need to find a way to support the ListView and the binding in its items with knockoutjs. To achieve that we take advantage of the fact that we can provide the item template to the listview from a function. The trick is to apply the ko.applyBindings command prior to delivering the template.

The change is as follows: Initially the template is defined in HTML as follows: <div class="listView" data-win-control="WinJS.UI.ListView" data-win-options="{ itemTemplate: select('.template')...>. This will be done in code through the function:

WinJS.UI.Pages.define("/pages/MVVMWinJS/MVVMWinJS.html", {
    ready: function (element, options) {
    	element.querySelector(".listView").winControl.itemTemplate = function (itemPromise) {
		    return itemPromise.then(function (item) {
		        var itemTemplate = document.querySelector('.template');
		        var container = document.createElement("div");
		        itemTemplate.winControl.render(item.data, container).then(function () {
		            ko.applyBindings(item.data, container); // THIS LINE DOES THE MAGIC
		        });;
		        return container;
		    });
		};

        ko.applyBindings(new VM(), element.querySelector(".dataBinding"));
    },
    ...

In the case that you want to provide declarative databinding for the ListView's itemDataSource you need to create a new binding attribute as follows and then use is as you do with the other attributes (text:,html:...) - that is itemDataSource:ListOfValues.dataSource :

ko.bindingHandlers.itemDataSource = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {        
    },
    update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        element.winControl.itemDataSource = valueAccessor();
    }
};

If you want to see the exact same example with pure WinJS binding click here

blog comments powered by Disqus
hire me