Data binding in Windows 8.1 Apps with WinJS
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 WinJS (see the one for knockout).
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
.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 WinJS.Binding.List object
// this.listOfValues = [{ text: '-' }, ...];
this.listOfValues = new WinJS.Binding.List([{ text: '-' }, ...]);
Binding is: <div class="listView" data-win-control="..." data-win-bind="winControl.itemDataSource:listOfValues.dataSource">
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 WinJS.Binding.as
// this.listOfValues = [{ text: '-' }, ...];
// this.listOfValues = new WinJS.Binding.List([{ text: '-' }, ...]);
this.listOfValues = new WinJS.Binding.List([WinJS.Binding.as({ text: '-' }),...]);
Binding is in the listView item template as: <span data-win-bind="textContent:text">
Requirement: I need to bind the buttonInvoked method to the click event of the button.
Solution: Wrap the function in WinJS.Utilities.markSupportedForProcessing and change the context to this (the same applies to any function that you need to bind such as the itemInvoked method to the winControl.oniteminvoked event of the listview):
//this.buttonInvoked = function () {
// Your code here
//};
this.buttonInvoked = WinJS.Utilities.markSupportedForProcessing((function () {
// Your code here
}).bind(this));
Binding is: <button data-win-bind="onclick:buttonInvoked">
Requirement: Whenever the value of the input element changes we want the data bound value to change (TwoWay binding support)
Solution: Create a generic binding initializer and use it.
WinJS.Namespace.define("Binding.Mode", {
TwoWay: WinJS.Binding.initializer(function (source, sourceProps, dest, destProps) {
WinJS.Binding.defaultBind(source, sourceProps, dest, destProps);
dest.onchange = function () {
var d = dest[destProps[0]];
var s = source[sourceProps[0]];
if (s !== d) source[sourceProps[0]] = d;
}
})
});
Binding is: <input type="text" data-win-bind="value:inputValue Binding.Mode.TwoWay">
Requirement: When a value is displayed we need to add some extra characters in the end (for example a € symbol in prices)
Solution: You cannot use expression directly in data-win-bind and therefore you have to declare a binding converter and use it
WinJS.Namespace.define("Converters", {
demoConverter: WinJS.Binding.converter(function (value) {
return value + " €";
})
});
Binding is: <div data-win-bind="innerHTML:textValue Converters.demoConverter">
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 WinJS.Binding.as
Solution: You need change your field in the object to property first (say you want to do this for the textValue field)
function VM() {
...
//this.textValue="-";
this._textValue = "-";
...
};
Then define property that has getters/setters and is enumerable and use the notify method in the setter. The notify method is provided by a mixin - see below
Object.defineProperty(VM.prototype, "textValue", {
get: function () {
return this._textValue;
},
set: function (value) {
this._textValue = value;
this.notify("textValue", value);
}, enumerable: true
});
You will not use the VM class to create your object but the VMObservable class which will be returned from the following mixin.
var VMObservable = WinJS.Class.mix(VM, WinJS.Binding.mixin);
Requirement: Apply the ViewModel to a given element and its children
Solution: Execute the WinJS.Binding.processAll() after the mixin.
WinJS.UI.Pages.define("/pages/MVVMWinJS/MVVMWinJS.html", {
ready: function (element, options) {
var VMObservable = WinJS.Class.mix(VM, WinJS.Binding.mixin);
WinJS.Binding.processAll(element.querySelector(".dataBindingRoot"), new VMObservable());
},
...
The final code of the ViewModel is:
WinJS.Namespace.define("Converters", {
demoConverter: WinJS.Binding.converter(function (value) {
return value + " €";
})
}); // Converter required to manipulate the binding value
WinJS.Namespace.define("Binding.Mode", {
TwoWay: WinJS.Binding.initializer(function (source, sourceProps, dest, destProps) {
WinJS.Binding.defaultBind(source, sourceProps, dest, destProps);
dest.onchange = function () {
var d = dest[destProps[0]];
var s = source[sourceProps[0]];
if (s !== d) source[sourceProps[0]] = d;
}
})
}); // Required to enable two way binding
function VM() {
this.listOfValues = new WinJS.Binding.List([WinJS.Binding.as({ text: '-' }),...)]);
this._textValue = "-";
this._inputValue = "-";
this.buttonInvoked = WinJS.Utilities.markSupportedForProcessing((function () {
for (var i = 0; i < this.listOfValues.length;i++)
this.listOfValues.getAt(i).text = (Math.random()*100).toFixed(2);
this.textValue = (Math.random() * 100).toFixed(2);
}).bind(this)); // markSupportedForProcessing required for "safe" binding
this.itemInvoked = WinJS.Utilities.markSupportedForProcessing((function (ev) {
var item =this.listOfValues.getAt(ev.detail.itemIndex);
var md = new Windows.UI.Popups.MessageDialog(item.text);
md.showAsync();
}).bind(this)); // markSupportedForProcessing required for "safe" binding
};
Object.defineProperty(VM.prototype, "textValue", {
get: function () {
return this._textValue;
},
set: function (value) {
this._textValue = value;
this.notify("textValue", value);
}, enumerable: true
});
Object.defineProperty(VM.prototype, "inputValue", {
get: function () {
return this._inputValue;
},
set: function (value) {
this._inputValue = value;
this.notify("inputValue", value);
}, enumerable: true
});
WinJS.UI.Pages.define("/pages/MVVMWinJS/MVVMWinJS.html", {
ready: function (element, options) {
var BindableVM = WinJS.Class.mix(VM, WinJS.Binding.mixin);
WinJS.Binding.processAll(element.querySelector(".dataBinding"), new BindableVM());
},
...
});
Wow. From 14 lines of code we went to 52 lines. If you want to see how knockout does it go to this post