We are building some Cordova apps for iOS, Android and Windows Phone. And we had everything figured out except for the UI part. We had already chosen Knockout in many of our other projects to facilitate data binding between UI and ViewModel. So when it was time to look into some UI solutions we came across WinJS. I am a huge fan of the Windows Phone UI (whatever it is called nowadays, Metro or Modern… who knows) so WinJS was my prefered choice.
Even more so when I saw a Cordova tutorial video on Microsoft where they used WinJS, and where they did not show but casually mention WinJS and KnockoutJS worked well together using a community library. The library turned out to be this one.
It sounded great, it looked like it could work, and if you would try some simple stuff, it really worked. There are a few snags though:
- Events don’t work well…. or at all… for many of the controls.
- No updates since November 2013, so no hope of those bugs getting fixed.
- No NuGet package (ok, thats a minor one).
So after we found out we only needed 2 controls from WinJS and just the CSS for the rest of the UI we decided to make some dedicated WinJS binding handlers for these controls. However it kept nagging me and I decided to do something about it. The rest of this post will document my findings.
First of all, I converted the original JS file to TypeScript, this makes my life so much easier since I am not really a javascript developer and I am used to C#’s type safety.
How does the original library work?
The original library dynamically creates a list of binding handlers for each WinJS control in a definition list:
function addBindings(controls) {
Object.keys(controls).forEach(function (name) {
var controlConfiguration = controls[name];
var ctor = WinJS.Utilities.getMember(name);
var eventName = controlConfiguration.event
var propertyProcessor = controlConfiguration.propertyProcessor;
var bindDescendants = controlConfiguration.bindDescendants || false;
var bindingName = "win" + name.substr(name.lastIndexOf(".") + 1);
ko.bindingHandlers[bindingName] = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// generic init stuff here
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// generic update stuff here
}
}
});
}
var controls = {
"WinJS.UI.AppBar": {
bindDescendants: true
},
"WinJS.UI.AppBarCommand": {},
"WinJS.UI.BackButton": {},
"WinJS.UI.DatePicker": {
event: "change"
}
// etc. for more controls.
}
addBindings(controls);
This code works great for most simple controls. And the author took some good steps to get the more complicated controls (like the ListView control) working. However binding the events for these somewhat more complicated controls is where things went buggy.
The main problem being that the current viewmodel is not passed to the event handler. When you click an AppBarCommand you would expect it to work as the knockout click handler and pass in the current viewmodel (more info on event binding in knockout here).
The improved way of handling events
The following updated version of the init and update methods of the generic binding handler implementation will give a better event handling experience. I have not yet thoroughly tested it though, and the solution for detecting events is a bit hacky (to put it mildly), so any suggestions there are always welcome.
The init method:
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// The options for the control
var value = valueAccessor();
// Options record for the WinJS Control
var options = {};
// Iterate over the observable properties to get their value
for (var property in value) {
// Don't parse properties starting with "on" since they are event handlers and should be treated differently.
if (value.hasOwnProperty(property) && (property.toString().substr(0, 2) != "on")) {
if (propertyProcessor && propertyProcessor[property] !== undefined) {
options[property] = propertyProcessor[property](value[property], function () { return element });
} else {
options[property] = ko.unwrap(value[property]);
}
}
}
// If the WinJS control depends on having child elements
if (element.children.length > 0 && bindDescendants) {
// This is done synchronously
// @TODO: Determine if this could be done async
ko.applyBindingsToDescendants(bindingContext, element);
}
// Create a new instance of the control with the element and options
var control = new ctor(element, options);
// After the control is created we can bind the event handlers.
for (var property in value) {
if (value.hasOwnProperty(property) && (property.toString().substr(0, 2) === "on")) {
control[property] = (eventInfo) => {
value[property].bind(viewModel, viewModel, eventInfo)();
};
}
}
// Add event handler that will kick off changes to the observable values
// For most controls this is the "change" event
if (eventName) {
ko.utils.registerEventHandler(element, eventName, function changed(e) {
// Iterate over the observable properties
for (var property in value) {
// Check to see if they exist
if (value.hasOwnProperty(property)) {
// Determine if that value is a writableObservable property
if (ko.isWriteableObservable(value[property])) {
// Kickoff updates
value[property](control[property]);
}
}
}
});
}
// Add disposal callback to dispose the WinJS control when it's not needed anymore
ko.utils.domNodeDisposal.addDisposeCallback(element, function (e) {
if (element.winControl) {
element.winControl.dispose();
}
});
return { controlsDescendantBindings: bindDescendants };
}
The update method:
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Get the WinJS control
var control = element.winControl;
var value = valueAccessor();
// Only update the control properties that are different with the unpacked value
for (var property in value) {
if (value.hasOwnProperty(property)) {
if (property.toString().substr(0, 2) != "on") {
var unwrappedValue = ko.unwrap(value[property]);
if (control[property] !== unwrappedValue) {
if (propertyProcessor && propertyProcessor[property] !== undefined) {
var returnValue = propertyProcessor[property](value[property],
function () { return element }, control[property]);
if (returnValue !== null) {
control[property] = returnValue;
}
} else {
control[property] = unwrappedValue;
}
}
} else {
// I think we are fine here if we just override the
// event handler even if it may not have changed at all.
control[property] = (eventInfo) => {
value[property].bind(viewModel, viewModel, eventInfo)();
};
}
}
}
}
The complete file can be found knockout-winjs.
I will try to fix more stuff and add more features to the knockout-winjs “connector”. Maybe even update it on a fork on github and create a nuget package for it. Who knows….