Knockout JS and Bing Maps

Combining Knockout with Bing Maps Ajax control will give you bindable properties similar to what you may have experienced with the Bing Maps Silverlight control. Further it has a great way to communicate with your JOSN web services. I’ve put together a library that implements a Knockout custom binding for the Bing Maps V7 control. With it you can create a view model with properties like a pin collection, a mapview, the current map type and have the map and any other HTML control simply bind to it. With Knockouts full support for Ajax calls to JSON data it provides a great basis for your next Bing Maps application.

Download the library and example here

Recently I’ve been building some mapping applications back in the HTML/JavaScript world after spending many years with Silverlight. I knew I wanted a better framework then just the usual hand rolled JavaScript loosely following some classes and at the recent DDDBrisbane event Joseph Cooney introduced the Knockout JavaScript library.

What is Knockout?

It’s a well-used, robust JavaScript library that allows you to define a view model and then two-way bind to HTML objects. It has some great stuff in it including templates, even recursive templates (great for your layer panel), observable collections, support for JSON data (including from web services) and more. If you’re looking at building your next rich single page app in HTML and JavaScript you have to check it out.

Checkout the full introduction and great examples here: http://knockoutjs.com/documentation/introduction.html

Bing Maps AJAX Control V7

This is the current control for Bing Maps, you can use it in any browser, combine it with phone gap to power your mobile apps or even use it slightly modified for your Windows8 app. It is modular, fast and a great experience for your users. It is a commercial control with petabytes of data at your disposal, sign up for a developer account here, you will need Your Bing Maps Key to run the examples: https://www.bingmapsportal.com/

Checkout the interactive SDK here if it’s your first time or if you haven’t played with the latest version: http://www.bingmapsportal.com/isdk/ajaxv7

Why would you want to combine Knockout and Bing Maps?

Simple, you want to have a clean view model that you can update from your logic and/or web services and simply have the map react. You want to separate the map’s functionality (UI) from your data and follow a MVVM pattern.

Simple example – binding the map type

You want a set of radio buttons to control the current map type (road, aerial or auto), simple example I know.

The old way: This is how you may implement it currently using some JQuery:

The new way: Instead this is how we do it using Knockout, if you’re following along (there is a full download at the end of the article) clear out all the above script code and reference:

<script
type=”text/javascript”
src=”Scripts/knockout-2.2.0.js”></script>

Step 1, define a view model:

Step 2, create the bing maps custom binding:

Step 3, modify the HTML to use bindings:

Step 4, Bind it up:

As usual something simple like this took far less code the old way, but nothing stays simple for long, later we will add more properties and data but as a quick example, you have a go at adding 3 more maps to the page and synchronise all the maps to same map type. Using the new technique I simply add the 3 divs and bind to the same view model, easy! Not a typical scenario but a good demo of the power of binding.


ADVANCED: Ah so that explains the complex code that is storing each map against the element’s id in the custom binding, this allows us to have many maps on the page all bound to the same or different parts of a view model. But was is _map1Id used for you ask? In the full listing we bind the center, zoom and bounds of the maps, all four maps are synchronised but the bounds as each map maybe a different size can only come from the first map.

Pins

Let’s do something more interesting, let’s have a collection of pins, these could quite happily come from a web service, see http://knockoutjs.com/documentation/plugins-mapping.html, but to keep it simple I’ll just add them to a viewmodel:

Step 1, define a view model:


Step 2a, add a helper to observable array so we can subscribe to changes in the collect, ie when pins get added and removed. This is from Michael Best.


Step 2b, modify the bing maps custom binding using our new helper above:

Step 3, modify the HTML to use bindings:

Step 4, Bind it up, add some data

Pretty cool hey? Note the custom event to add a remove button to the table data, purely optional. Here you can add more functions. I’m sure you’re asking where do I put any interaction logic with the pins, in the createpin() method on your viewmodel you have complete control. This is where you could extend to support polygons and lines.

Full implementation

Currently I’m supporting:

  • Multiple maps on the same page
  • Only bind the properties you want to use
  • Map Type
  • Mapview: centre, zoomlevel and bounds
  • Pins, although not too hard to make this into any entity
  • TileLayers
  • Map Info, 4x4px bounds on the mouse click, handy for tile layers, gives a nice bounds string to pass off to your server for info tooling raster layers
  • Full Options object can be passed in to configure the map how you like.
  • Works really well from JSON web services, trick is to set the collection using the Knockout mapping on an empty array, then load in the data async:
    self.pins = ko.mapping.fromJS([]);

Download the library and example here. If you have any questions or feedback catch me on http://twitter.com/soulsolutions

For completeness here is the full listing for the BingMapsKO.js library as text, feel free to do with it what you like, I’ll put it under Ms-PL license that gives you maximum flexibility to use it however you like. If you enhance it or find a bug feel free to let me know (or not).

//Bing Maps library for KnockoutJS.com

//John O’Brien Soul Solutions 2012-12-31

//Microsoft Public License (Ms-PL)

//See blog post for examples and updates: http://www.soulsolutions.com.au/Blog/tabid/73/EntryId/818/Knockout-JS-and-Bing-Maps.aspx

 

 

//add helper to ko.observableArray

ko.observableArray.fn.subscribeArrayChanged = function (addCallback, deleteCallback) {


var previousValue = undefined;


this.subscribe(function (_previousValue) {

previousValue = _previousValue.slice(0);

}, undefined, ‘beforeChange’);


this.subscribe(function (latestValue) {


var editScript = ko.utils.compareArrays(previousValue, latestValue);


for (var i = 0, j = editScript.length; i < j; i++) {


switch (editScript[i].status) {


case
“retained”:


break;


case
“deleted”:


if (deleteCallback)

deleteCallback(editScript[i].value);


break;


case
“added”:


if (addCallback)

addCallback(editScript[i].value);


break;

}

}

previousValue = undefined;

});

};

 

ko.computed.fn.subscribeArrayChanged = function (addCallback, deleteCallback) {


var previousValue = undefined;


this.subscribe(function (_previousValue) {

previousValue = _previousValue.slice(0);

}, undefined, ‘beforeChange’);


this.subscribe(function (latestValue) {


var editScript = ko.utils.compareArrays(previousValue, latestValue);


for (var i = 0, j = editScript.length; i < j; i++) {


switch (editScript[i].status) {


case
“retained”:


break;


case
“deleted”:


if (deleteCallback)

deleteCallback(editScript[i].value);


break;


case
“added”:


if (addCallback)

addCallback(editScript[i].value);


break;

}

}

previousValue = undefined;

});

};

 

 

ko.bindingHandlers.bingmaps = {

_uniqueid: 0,

_map1id: ,

_map: {},

_entityGeoLookup: {},

_entityLayerLookup: {},

init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {


var _valueAccessor = valueAccessor();

 


if (!_valueAccessor.map) {

alert(‘Map property not set on KO Bing Maps’);


return;

}

 


//create map and store against the element’s ID.


var map = new Microsoft.Maps.Map(element, _valueAccessor.map.options);

ko.bindingHandlers.bingmaps._map[element.id] = map;


if (ko.bindingHandlers.bingmaps._map1id == ) ko.bindingHandlers.bingmaps._map1id = element.id;

 


if (_valueAccessor.map.mapview) {


//check if we need to bind to the event at all


if (_valueAccessor.map.mapview.zoom || _valueAccessor.map.mapview.center || _valueAccessor.map.mapview.bounds) {

Microsoft.Maps.Events.addHandler(map, ‘viewchange’, function (e) {


if (_valueAccessor.map.mapview.zoom) _valueAccessor.map.mapview.zoom(map.getTargetZoom());


if (_valueAccessor.map.mapview.center) _valueAccessor.map.mapview.center(map.getTargetCenter());


//clear the bounds during the animation, only set it at the viewchangeend, for multiple maps only the first map can set the bounds.


if (_valueAccessor.map.mapview.bounds && ko.bindingHandlers.bingmaps._map1id == element.id) _valueAccessor.map.mapview.bounds();

});

}

 


//check if we need to bind to the event at all, for multiple maps only the first map can set the bounds.


if (_valueAccessor.map.mapview.bounds && ko.bindingHandlers.bingmaps._map1id == element.id) {

Microsoft.Maps.Events.addThrottledHandler(map, ‘viewchangeend’, function (e) {

_valueAccessor.map.mapview.bounds(map.getBounds().toNwse());

}, 100);

}

}

 


if (_valueAccessor.maptype) {

Microsoft.Maps.Events.addHandler(map, ‘maptypechanged’, function (e) {

_valueAccessor.maptype(map.getMapTypeId());

});

}

 


if (_valueAccessor.info) {

Microsoft.Maps.Events.addHandler(map, ‘click’, function (e) {


if (_valueAccessor.info) _valueAccessor.info(map.getBoundsFromPixelXY(e.getX(), e.getY()));

});

}

 


if (_valueAccessor.pins) {

_valueAccessor.pins.subscribeArrayChanged(function (data) {


//add


if (_valueAccessor.createPin) {


var geo = _valueAccessor.createPin(data);

map.entities.push(geo);


//add a unique id to the object


if (typeof data.__uniqueid == “undefined”) {

data.__uniqueid = ++ko.bindingHandlers.bingmaps._uniqueid;

}

ko.bindingHandlers.bingmaps._entityGeoLookup[element.id + data.__uniqueid] = geo;

}

}, function (data) {


//delete


if (_valueAccessor.createPin) {


var geo = ko.bindingHandlers.bingmaps._entityGeoLookup[element.id + data.__uniqueid];

map.entities.remove(geo);


delete ko.bindingHandlers.bingmaps._entityGeoLookup[element.id + data.__uniqueid];

}

});

}

 


if (_valueAccessor.layers) {

_valueAccessor.layers.subscribeArrayChanged(function (data) {


//add


if (_valueAccessor.createTileLayer) {


//add a unique id to the object


if (typeof data.__uniqueid == “undefined”) {

data.__uniqueid = ++ko.bindingHandlers.bingmaps._uniqueid;

}


var tilelayer = _valueAccessor.createTileLayer(data);

map.entities.push(tilelayer);

ko.bindingHandlers.bingmaps._entityLayerLookup[element.id + data.__uniqueid] = tilelayer;

}

}, function (data) {


//delete


if (_valueAccessor.createTileLayer) {


var layer = ko.bindingHandlers.bingmaps._entityLayerLookup[element.id + data.__uniqueid];

map.entities.remove(layer);


delete ko.bindingHandlers.bingmaps._entityLayerLookup[element.id + data.__uniqueid];

}

});

}

},

update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {


var _valueAccessor = valueAccessor(), allBindings = allBindingsAccessor();

 


if (!_valueAccessor.map) {


return;

}


var map = ko.bindingHandlers.bingmaps._map[element.id];

 


if (_valueAccessor.map.mapview) {


var currentTargetCenter = map.getTargetCenter();


var currentCenter = map.getCenter();

 


var zoom = map.getTargetZoom();


if (_valueAccessor.map.mapview.zoom) zoom = parseInt(ko.utils.unwrapObservable(_valueAccessor.map.mapview.zoom()));


var center = currentTargetCenter;


if (_valueAccessor.map.mapview.center) center = ko.utils.unwrapObservable(_valueAccessor.map.mapview.center());


var bounds = ;


if (_valueAccessor.map.mapview.bounds) bounds = ko.utils.unwrapObservable(_valueAccessor.map.mapview.bounds());

 


if (zoom != map.getTargetZoom() || Math.abs(center.latitude – currentTargetCenter.latitude) + Math.abs(center.longitude – currentTargetCenter.longitude) > 0.00001) {

map.setView({ zoom: zoom, center: center });

} else
if (ko.bindingHandlers.bingmaps._map1id == element.id && bounds != && bounds != map.getBounds().toNwse() && zoom == map.getZoom() && Math.abs(center.latitude – currentCenter.latitude) + Math.abs(center.longitude – currentCenter.longitude) < 0.00001) {

map.setView({ bounds: Microsoft.Maps.LocationRect.fromString(bounds) });

}

}

 


if (_valueAccessor.maptype) {


var maptype = ko.utils.unwrapObservable(_valueAccessor.maptype());


if (maptype != map.getMapTypeId()) {

map.setMapType(maptype);

}

}

}

};

 

$(document).ready(function () {


//extension methods 🙂

Microsoft.Maps.LocationRect.prototype.toNwse = function () {


return
this.getNorth().toFixed(5) + ‘,’ + this.getWest().toFixed(5) + ‘,’ + this.getSouth().toFixed(5) + ‘,’ + this.getEast().toFixed(5);

};

 

Microsoft.Maps.Map.prototype.getBoundsFromPixelXY = function (x, y) {


var r = Microsoft.Maps.LocationRect.fromCorners(


this.tryPixelToLocation(new Microsoft.Maps.Point(x – 2, y – 2)),


this.tryPixelToLocation(new Microsoft.Maps.Point(x + 2, y + 2)));


return r.toNwse();

};

});

Unobtrusive Validation and KnockoutJS form submit

I’ve been playing with some MVC4, Knockout JS and some unobtrusive validation, specifically I was trying to replicate the Grid Editor Example.

 

I had a problem with the example when I had:

$("form").validate({ submitHandler: function () {
    viewModel.save();
} });

 

It would still call the viewModel.save(), which would in term post my data back to the server even if I had errors. This worked “ok” in most cases as my model validation picked up most things, but if for example I’d put a string in a number field, that’s when everything came to a screaming halt.

After a bit of poking around I found that changing the previous statement for this one made everything happy again:

$.validator.setDefaults({ submitHandler: function() {
             viewModel.save();
        } });

 

DDDBrisbane 2012 Wrapup

1

Yesterday over 100 of Brisbane’s keenest developers descended upon QUT for the 2012 DDD Brisbane.

2

3

4

The doors opened at QUT at 7:45 and we had a bunch of people keen to get inside and out of what was a very hot day. After registation and handing out the bags and swag it was off to the sessions.

5_patick_marice

 6

7

First session we had Maurice/Patrick, Rod and Robert.

33drinks

While the first session was on Dave and I did an ice run and cleaned Coles out all their bagged ice. Wanted to make sure it was back and in the esky in time for the first break as we know people would been keen for an icy cold drink on such a hot day.

8_brendan

9_kay

 10

2nd round of sessions we had Brendan, Kay and Andrew.

11

12_adam

 12_maurice

3rd session and standing between the attendees and the food was Bydren, Adam and Maurice.

34_pizza

35sub

 36sub

Then the all important lunch! Amazing how much food a crowd can eat through : 45 pizzas, 8 plates of subway and 9 platters of sushi!

13

14_william

15_patick

Now that everyone was full and happy, back to session 4  with Liam, William and Patrick.

13_a_postlunch

While we were cleaning up after lunch, the swag was memorised by John’s surface.

16

17

18_joseph

Session 5 we had  Philip, Andrew and Joseph.

19_

20_eric 

For the last session of the day we were very lucky to have Brian Beckman and Erik Meijer as our special guest speakers!

21_crowd

They were very entertaining and had the crowd in lots of laughter as they recreated Linq in Mathematica.

22_prizes

Finally, Damian ran through the 18 prizes donated by all our kind sponsors. David from Infragistics also gave out an extra impromptu license suit to one of the attendees who helped out with some of the mathematica demo.

What a excitement filled day! It wouldn’t have happened of course without our great sponsors, the great speakers, the attendees and Damian heading up the organisation and of course Lin, David, and John helping out with all those extra tasks before and during the day. Thanks EVERYONE!

Here’s hoping next year’s is bigger and better yet!

If you’re keen to see the rest of the pics we took, check out our smugmug album.