Fair warning; this post doesn’t have a lot to do with the Feng Shui of VB, or does it…. <g>
I’ve been recently playing quite a lot with Javascript, and various frameworks and libraries that really turn the browser into a client development platform in its own right.
To me, that combination just rings truer than the use of server-side html-generating frameworks like ASP.net, etc. But that’s philosophy for another discussion.
In this case, I’m using JQuery, JQueryMobile, and Knockout to build a mobile capable app that can run on various devices via single source, and hopefully be deployed via PhoneGap, but still look relatively native on each device.
If you’re not already aware of these libraries/frameworks (it can be difficult to tell exactly where each of these fits in the grand spectrum of browser client side development):
- JQuery : the granddaddy of client side browser libraries. Even Microsoft is supporting this one out of the box now. This thing really makes working with the DOM and client side development a joy.
- JQueryMobile : A UI kit running on top of JQuery that renders almost native looking UI across a broad spectrum of mobile devices (think one source set running on everything from iPhones, to Kindles, to Android phones, to Win7 Phones).
- KnockOut : a framework for doing client-side data binding between DOM elements (text boxes, select lists, spans, etc), and Javascript JSON objects.
I wanted to bind a SELECT list to the equivalent of a .net ENUM in Javascript (which doesn’t technically have ENUMs, so I had to create a class to support something similar).
Here’s the KnockOut View Model:
var userProfileViewModel = { timeToTrack : ko.observable(SecondsEnum.secs15), secondsEnum : ko.observableArray(SecondsEnum.toArray()) }
The toArray function is just a convenience function to convert my ENUM class into a typical Javascript array that KnockOut can work with. Eventually my plan is to construct a KnockOut Custom Binding that would do all this automatically . But that’s for another post…
The HTML to actually view and bind to that model looks like this:
<span data-bind='text: timeToTrack'></span> <select name="selTimeToTrack" id="selTimeToTrack" data-mini="true" data-bind=" options: secondsEnum, optionsText: 'desc', optionsValue: 'value', value: timeToTrack"> </select>
Everything worked great, except for one nasty bit. When the displayed page first comes up, the select lists are empty. You can see the effect here.
Notice how the 15 that’s highlighted DOES populate properly (the value of the bound property is 15 in the view model), but the dropdown list value is empty.
Interestingly, if I click the dropdown, the resulting list does show the proper initial value (15 seconds):
After days of Google searching for answers, I finally got a few hours of free time and decided to just roll up my sleeves and start tracing through the KnockOut unminified debug version.
Eventually, I fell into this function (in KnockOut.2.1.0.js):
function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) { if (preferModelValue) { if (modelValue !== ko.selectExtensions.readValue(element)) ko.selectExtensions.writeValue(element, modelValue); } // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value. // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way, // change the model value to match the dropdown. if (modelValue !== ko.selectExtensions.readValue(element)) ko.utils.triggerEvent(element, "change"); };
Something didn’t quite look right about this.
If the first block of IF’s ends up true, the modelValue (the value of the property from view model) is going to be set as the initial value of the select DOM element, which is what we want.
BUT, if that happens, the next IF will ALWAYS be false (because the code just set the two value equal), so the “change” event will not be triggered in this case. The problem is, if it’s not triggered, the select DOM element won’t get updated to reflect that value that just got written.
My suspicion is that this was an attempt to circumvent having two lines of code to trigger then change element, but the logic just doesn’t seem to work.
So, I made a subtle change:
function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) { if (preferModelValue) { if (modelValue !== ko.selectExtensions.readValue(element)) ko.selectExtensions.writeValue(element, modelValue); // DWH added this to correct problem with the change not firing on initial population ko.utils.triggerEvent(element, "change"); } else { // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value. // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way, // change the model value to match the dropdown. if (modelValue !== ko.selectExtensions.readValue(element)) ko.utils.triggerEvent(element, "change"); } };
Here, you can see the “change” event is triggered in both situations (whether the ModelValue is preferred or not).
Once I made the change, I run the page and now I get the much more reasonable:
Notice how each dropdown list is initially populated with the current value from the view model.
The Take Away
Javascript (and dynamic languages in general) are a huge departure from static languages like VB ( and all the .net languages). Whether that’s a good thing or not is debatable, but the fact is, there is a tremendous amount of flexibility available because of the dynamic nature of Javascript. And frameworks like JQuery and KnockOut really bring some fantastically clean functionality to client-side browser development.
But, as with most code, these frameworks aren’t bug free. I’m not saying that this is definitively a bug in the KnockOut code, yet, though. I’ve posted to the KnockOut forums and am awaiting a reply.
In the meantime, though, this is a quick and easy fix to a vexing problem.
So if you’ve run into similar problems with KnockOut, it might be worth investigating.