Introduction
BikeInCity is my free-time project. One of the reasons for which I maintain this project is to have a playground to test and learn new frameworks and technologies. This article is about using KnockoutJS and MVVM pattern.
Let me give a brief description of the application. This web and mobile applications works as bike sharing aggregator. More and more cities propose bike sharing systems allowing users to pick up bikes at special stations for small routes. Usually the system provides users with web and mobile application which allows the clients to get the locations of the stations and the number of available bikes. I have build a simple application which aggregates all the bike sharing systems and provides also routing.
The application is hosted at AppHarbor (http://bikeincity.apphb.com/). AppHarbor is .NET cloud platform for fast deployment using GIT, really check it out if you do not know it. I have decided to open-source the project. The project is hosted on GitHub.
Lately I have decided to add a new functionality (lets call it feature). Users can add interesting points (I call them tips) on map, write some description and add a picture to the marker which will be added to the card. Then the application will show the 5 nearest bike stations around the interesting place. So basically I needed some CRUD operations, and the ability to maintain a consistent model behind my page. So far the site was pure JavaScript client which talked to WCF Services on the back-end site. I did not want to break this model. But I needed a way to organize things a little bit. So KnockoutJS came as a logical solution.
I have used MVVM before while working with Silverlight/WPF. I like that pattern quite lot and I prefer to define my UI declaratively and bind it to the data or functionality in the ViewModel. I have checkout out KnockoutJS and found that it allows me to do a lot to which I am used to.
So how does the result look like? Here you have the screen shot of the page and the article will describe how we can get there.
The source code
The project is open-source, so you can ofcourse get all the code from the GitHub repository. However I have create example project which contains only the exact part of the web described in this article. The back-end and database access is "mocked" by data stored in App_Data folder. So you could easily open it in VS 2010, hit F5 and debug.
Background: MVVM and KnockoutJS
Model-View-ViewModel is a design pattern which helps organize, architecture and design the user interfaces. If implemented wisely, it provides nice separation of concerns and enhances testability of the applications. I think, that the first article which formalized the pattern was published in 2009 by Josh Smith in MSDN magazine. The pattern was "build for" WPF and Silverlight applications, because it makes use of the great binding possibilities of these two platforms. You can also search here at CodeProject to get some more information.
KnockoutJS is an attempt (very cool one) to bring MVVM to JavaScript applications. HTML 5 & JavaScript do not provide the nice binding out of box. KnockoutJS implements the necessary plumbing to get things done. In this article I suppose, that you have some general knowledge of MVVM and KnockoutJS. Be sure to take a look at the documentation page, which provides detailed description of the framework.
The model on the server side
In simple words: we have a collection of cities. Each city can contain multiple bike stations and as well as multiple tips. Both tips and stations have latitude and longitude. Stations do also contain the information about available bikes. Tips contains some picture and description of the place. The cities are also grouped into countries. I use NHibernate as ORM framework the configuration is achieved using FluentNHibernate. But that is not the subject of this article. However you can always check how it is done in the GitHub repository. Here are the model classes.
I use Data Transfer Objects which in this case are very similar. The CityDto just does not contain the collections of tips and stations. Instead the StationDto holds the Id of the City. So in fact the DTO's are very similar to the lines in your database.
Restful web services
Before delving into the front-end the necessary infrastructure has to be build on the back-end. Later you can see how easy it is to plug the ViewModel to the restful web services described in this part of the article.
Four services are used at the backed part:
- Service which returns list of available countries
- Service which returns all the tips to visit
- Service which enables the user to create a “tip”
- Service which returns all the bike stations in the city
- Service which is called while adding the tip in order to upload the image
The first three services are all exposed using WCF. The fourth service for the upload of the image isn't actually a WCF service. Instead it is poor old HttpHandler which takes care of the update.
The code to expose these services is straightforward. The consultation services just retrieve the objects from database and optionally transfer them to DTOs. I am not entering into details of my repository or service classes which use NHibernate to store the values. If the service is accepting integer input (for example the ID of the city) we have to convert it from the String value. String values are needed only when we are using parameters mapped directly from URL. If these parameters would be passed in the query, integer values could be used directly.
[OperationContract]
[WebGet(UriTemplate = "countries/{countryId}/cities", ResponseFormat = WebMessageFormat.Json)]
public List<CityDto> GetCities(String countryId)
{
int countryID = Convert.ToInt32(countryId);
var cities = _repository.Find<City>(x => x.Country.Id == countryID);
return Mapper.Map<List<CityDto>>(cities);
}
[WebGet(UriTemplate = "countries", ResponseFormat = WebMessageFormat.Json)]
public List<CountryDto> GetCountries()
{
var list = _repository.GetAll<Country>();
var clearedList = list.Select(x => new CountryDto{ Name = x.Name, Id = x.Id }).ToList();
return clearedList;
}
[OperationContract]
[WebGet(UriTemplate = "city/{cityID}/stations", ResponseFormat = WebMessageFormat.Json)]
public List<StationDto> GetStations(String cityID)
{
int id = Int32.Parse(cityID);
var stations = _repository.Find<Station>(x => x.City.Id == id);
var dtos = Mapper.Map<List<StationDto>>(stations);
return dtos;
}
[OperationContract]
[WebGet(UriTemplate = "city/{cityId}/tips", ResponseFormat = WebMessageFormat.Json)]
public List<InformationTipDto> InformationTips(String cityId)
{
var id = Convert.ToInt32(cityId);
var list = Mapper.Map<List<InformationTipDto>>(_repository.Find<InformationTip>(x => x.City.Id == id));
return list;
}
[OperationContract]
[WebInvoke(Method = "POST", BodyStyle = WebMessageBodyStyle.Bare,
RequestFormat = WebMessageFormat.Json, UriTemplate = "tips/add")]
public void AddTip(InformationTipDto tip)
{
var infoTip = Mapper.Map(tip);
var city = _repository.Find<City>(x => x.Id == tip.CityId).SingleOrDefault();
infoTip.City = city;
_repository.Save<InformationTip>(infoTip);
}
Now let’s take a look at the configuration. All three methods reside in the same class, thus there is only one service and one endpoint to expose. The services are configured to use JSONP format in order to enable cross-site scripting. Since .NET 4 is available you can use the crossDomainScriptAccessEnabled on webHttpBinding to enable the JSONP behavior.
<system.serviceModel>
<services>
<service behaviorConfiguration="JsonServicesBehavior" name="CPKnockout.Web.Services.Bike">
<endpoint address="json" behaviorConfiguration="JsonServicesBehavior" binding="webHttpBinding" bindingConfiguration="jsonpBinding" contract="CPKnockout.Web.Services.Bike" />
</service>
</services>
<bindings>
<webHttpBinding>
<binding name="jsonpBinding" crossDomainScriptAccessEnabled="true" />
</webHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="JsonServicesBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="JsonServicesBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>
As mentioned before HttpHandler is used in order to handle the upload of the image. I am not exposing this as REST method. There is no reason to use WCF - it would not facilitate the task, just add some complexity.
public void ProcessRequest(HttpContext context)
{
System.Web.HttpPostedFile file = context.Request.Files[0];
var fileName = RandomString(20) + "." + extension;
var folderName = "Uploads";
var filePath = System.Web.HttpContext.Current.Server.MapPath("~") + folderName + "\\" + fileName;
var link = String.Format("/{0}/{1}", folderName, fileName);
using (var img = Image.FromStream(file.InputStream, true, false ) )
{
Size sz = adaptProportionalSize(new Size(250,250), img.Size);
var smallImage = img.GetThumbnailImage(sz.Width,sz.Height,null,IntPtr.Zero );
smallImage.Save(filePath);
}
context.Response.StatusCode = 200;
context.Response.Write(link);
}
The handler returns the link to the uploaded file. The format of response is pure text - no JSON in this case.
The ViewModels
The client side model logically copies the server side model. So we have one ViewModel for each model class on the server. In addition to that we also have one ViewModel which aggregates the ViewModel. This is not the first time that while using MVVM pattern I have an impression of also conforming to the idea of Aggregate pattern. I am wondering if anyone out there has somehow spotted and formalized this relation.
Back to the work. The aggregate ViewModel is called CountryListViewModel - because it is basically a list of Countries with some added logic. Each country contains list of cities and each city a list of stations and tips. Here is the ViewModels hierarchy:
CountryListViewModel
|-- List<CountryViewModel>
|-- List<CityViewModel>
|-- List<StationViewModel>
|-- List<TipViewModel>
I will now describe all the ViewModels in this hierarchy bottom up, starting with
TipViewModel. This ViewModel holds the information about one tip (described by it’s title, description, image url). The ViewModel also holds the ID of the the city (it’s parent).
function TipViewModel(cityId, parent) {
var self = this;
self.parent = parent;
self.cityId = cityId;
self.title = ko.observable();
self.description = ko.observable();
self.imageUrl = ko.observable();
self.saving = ko.observable(false);
self.lat = ko.observable();
self.lng = ko.observable();
self.selected = ko.observable(false);
self.select = function () {
if (self.parent.selectedTip() != null) {
self.parent.oldTip = self.parent.selectedTip();
self.parent.oldTip.selected(false);
}
self.parent.selectedTip(self);
self.selected(true);
};
self.save = function () {
self.saving(true);
var data = JSON.stringify(self.toDTO());
$.ajax("/Services/Bike.svc/json/tips/add", {
data: data,
type: "post", contentType: "application/json",
success: function (result) {
self.saving(false);
}
});
};
self.toDTO = function () {
var dto = new Object();
dto.CityId = self.cityId;
dto.Description = self.description();
dto.Title = self.title();
dto.ImageUrl = self.imageUrl();
dto.Lat = self.lat();
dto.Lng = self.lng();
return dto;
};
}
The save method, performs an Ajax call to the web service exposed on the server side. When there are no errors - the success flag is set to TRUE. Notice that there is also toDto method which I use to create a JSON of the DTO structure exposed on the server side. The service for adding a Tip takes TipDto as paramter.
The next ViewModel is the station. The station is even less complex - there are is no “save” method. This ViewModel is used just for the visualization.
function StationViewModel(cityId, parent) {
var self = this;
self.parent = parent;
self.cityId = cityId;
self.address = ko.observable();
self.lat = ko.observable();
self.lng = ko.observable();
}
Let’s pursue with CityViewModel. Here we add a little bit of complexity in order to load the stations and the tips. But there are also other things to notice, take a look at the code first:
function CityViewModel(data) {
var self = this;
self.name = ko.observable(data.Name);
self.id = data.Id;
self.tips = ko.observableArray([]);
self.stations = ko.observableArray([]);
self.newTip = ko.observable(new TipViewModel(self.id, self));
self.saving = ko.observable();
self.selectedTip = ko.observable();
self.oldTip = null;
self.lat = data.Lat;
self.lng = data.Lng;
self.description = ko.observable();
self.stationImageUrl = ko.observable();
self.bikeImageUrl = ko.observable();
$.getJSON("/Services/Bike.svc/json/city/" + this.id + "/tips", function (allData) {
var mappedTips = $.map(allData, function (item) {
var tip = new TipViewModel(self.id, self);
tip.title(item.Title);
tip.description(item.Description);
tip.imageUrl(item.ImageUrl);
tip.lng(item.Lng);
tip.lat(item.Lat);
return tip;
});
self.tips(mappedTips);
});
self.getStations = function () {
$.getJSON("/Services/Bike.svc/json/city/" + this.id + "/stations", function (allData) {
var mappedStations = $.map(allData, function (item) {
var station = new StationViewModel(self.id, self);
station.address(item.Address);
station.lng(item.Lng);
station.lat(item.Lat);
return station;
});
self.stations(mappedStations);
});
};
self.addTip = function () {
self.newTip().save();
self.tips.push(self.newTip());
self.newTip(new TipViewModel(self.id, self));
};
}
What we need here is somehow to handle the “new tip” which is being added by the user. Hence the NewTip property which holds the data of the new tip and is replaced by new empty tip any time the save occurs. Here we go with the CountryViewModel. Country contains a list of cities and that’s it.
function CountryViewModel(data) {
var self = this;
self.name = data.Name;
self.id = data.Id;
self.cities = ko.observableArray([]);
self.selectedCity = ko.observable();
self.oldCity = null;
$.getJSON("Services/Bike.svc/json/countries/" + self.id + "/cities", function (allData) {
var mappedCities = $.map(allData, function (item) { return new CityViewModel(item) });
self.cities(mappedCities);
});
}
The last and the one which aggregates all of them is CountryListViewModel. CountryListViewModel contains the list of countries as you would suspect. There is a small snippet of interesting code here. The afterCountriesRendered method is executed right after the menu is rendered and hides the items of the menu. I will point that out later, when discussing how exactly the menu is rendered.
function CountryListViewModel() {
var self = this;
self.countries = ko.observableArray([]);
self.selectedCity = ko.observable();
self.oldCity = null;
self.setSelected = function (city) {
city.getStations();
self.oldCity = self.selectedCity();
self.selectedCity(city);
};
$.getJSON("Services/Bike.svc/json/countries", function (allData) {
var mappedCountries = $.map(allData, function (item) { return new CountryViewModel(item) });
self.countries(mappedCountries);
});
self.afterCountriesRendered = function (elements) {
$(elements[1]).mouseenter(function () {
$(this).find("ul").slideToggle("fast");
});
$(elements[1]).mouseleave(function () {
$(this).find("ul").hide();
});
$(elements[1]).find("city").hide();
};
}
Let’s now take a look at the web page which uses all of these ViewModels.
The View
Before anything can be done, the main ViewModel has to be initialized (in this case CountryListViewModel) and passed to Knockout as the ViewModel which will be used to bind the elements on the page.
countryList = new CountryListViewModel();
ko.applyBindings(countryList);
The page is composed of 4 interesting regions: the menu, the tips column, the new tip dialog and the map. Let’s start with the div element holding the tips table.
Tips table
The main part of this element is a table using the Knockouts foreach binding.
<div class="tips_column">
<table style="background-color: #F7B54A;">
<tr>
<td>
<div data-bind="text: selectedCity()? selectedCity().name : 'Select the city'" />
</td>
<td>
<div data-bind="visible: selectedCity()" id="dialog_link">
<span style="margin: 5px">add new</span>
</div>
</td>
</tr>
</table>
<!---->
<!---->
<div data-bind="foreach: selectedCity().tips">
<div data-bind="latitude: lat, longitude:lng, map:map, selected:selected">
</div>
</div>
<table data-bind="foreach: selectedCity().tips" style="width: 100%">
<tr style="cursor: pointer;">
<td data-bind="click: select,style: { backgroundColor: selected() ? '#66C547' : '#808080'}">
<img data-bind='attr: { src: imageUrl }' height="60px" width="60px" style="float: left" />
<div style="float: left; color: White; margin: 4px">
<div data-bind='text: title' style="font-weight: bold">
</div>
<div data-bind="text: description">
</div>
</div>
</td>
</tr>
</table>
You can see that the background color of each row is bound to the selected property of the InformationTipViewModel. You can also see, that in order to render the image the attribute binding is used. The whole region uses this directive:
<!---->
This basically tells to Knockout to ignore the whole region when no city was chosen.
New tip dialog
JQuery UI is used in order to render the following part of the web page as a dialog:
<div id="newTip" style="background-color: Gray;" title="New bike tip"
data-bind="with:selectedCity()">
<table>
<tr>
<td>
Title:
</td>
<td>
<input data-bind="value: $data ? newTip().title : '' " />
</td>
</tr>
<tr>
<td>
Description:
</td>
<td>
<textarea data-bind="value: $data? newTip().description : ''" ></textarea>
</td>
</tr>
<tr style="height:100px;margin-bottom:4px">
<td>
Image:
</td>
<td>
<input type="radio" name="uploadVSURL" id="enableUpload" onchange="imageUrlUploadSwitch()"
checked="checked" /><label>Upload image</label>
<input type="radio" name="uploadVSURL" onchange="imageUrlUploadSwitch()" /><label>Image
from web</label>
<input data-bind="value: $data? newTip().imageUrl : ''" id="providedUrl"/>
<form id="uploadForm" action="/Services/Upload.ashx" method="POST" enctype="multipart/form-data">
<table>
<tr>
<td><input type="file" name="file" /></td>
<td><input type="hidden" name="MAX_FILE_SIZE" value="100000" /></td>
<td><input type="submit" value="Submit" /></td>
</tr>
</table>
</form>
</td>
</tr>
<tr>
<td>
Latitude/Longitude:
</td>
<td>
<input data-bind="value: $data? newTip().lat : ''" />
<input data-bind="value: $data? newTip().lng : '' " />
</td>
</tr>
</table>
<button data-bind='click: $data? addTip : new function(){}'>
Add a tip</button>
</div>
Here I am using with binding. This binding enables us to define a region inside which the $data wild card will be substituted with the observable passed to the with binding. This enables the developers to reduce the repetitive copying of the observable name for each bounded element in the form or region. I could not use the Knockouts if directive here. In order to turn the div into a dialog a JQuery's dialog method has to be called on the div. Knockout seeing that there is no selected city when loading the page for the first time, would just omit the div element and the dialog initialization would be called on non-existent div tag. That’s way I have to use the conditional binding:
value: $data ? NewTip().title : ' '
This is interpreted the following way: if there is a city, than show the new tips value, if not just show an empty string.
The menu
As you can see on the screen shots the menu is composed of list of countries, each country having several cities. In Knockout a double foreach binding will result in the demanded effect. However I have decided to make the outer foreach binding render a template. Template has an advantage of providing some more flexibility to the process of rendering. Especially useful can be the ‘afterRender’ callback.
<nav id="menu">
<a href="#" class="trigger">choose city</a>
<ul data-bind="template: {name:'country-menu-template', foreach: countries,afterRender: afterCountriesRendered}" style="background-color:#808080"></ul>
</nav>
<script type="text/html" id="country-menu-template">
<li class="li_country"><span data-bind="text: name"></span>
<ul id="cityList" data-bind="foreach:cities" class="ul_city" style="display:none">
<li data-bind="click: $root.setSelected" style="cursor:pointer"><span data-bind="text: name"></span></li>
</ul>
</li>
</script>
Here the afterRender action defined in the CountryListViewModel comes into play. I wanted to define some callbacks for mouse events of this menu (namely the mouse enter and mouse leave event) which are responsible for the ‘hover’ effect. This can be achieved using JQuery mousenter function and passing the callback. However when the page is loaded, there are no elements rendered. The menu is rendered once all the data had been acquired from the rest services. For this reason I have decided to use the afterRender callback. This callback has to be set to a function taking a set of elements as a parameter.This set will contain the HTML elements which are rendered by the template. Once we have the rendered elements we can call any JQuery function on them, defying their behavior. Notice that this is the only place where the ViewModels contain or more precisely show some knowledge of the View. This is not great but I have not found a way around.
Changing the city is done by data-binding the click action of the LI element of each city. The action is quite simple: $root.setSelected. $root is a wild card for the root ViewModel (or in other words the aggregate) and because there is no parameter specified to the setSelected method, the element to which the click action is binded will be passed as parameter. The result is that the setSelected method of CountryListViewModel is called taking the current city as parameter and takes care of the changes. As shown before the table of tips is bound to the tips of selectedCity by changing the selectedCity the tips change automatically. Now the second thing which has to be bound to the changes of current city is of course the map.
The MAP
There is nothing special in the initialization of the map, which would diverge from the standard procedure described in the Google Maps API. In order to make the binding between the points on the map and the ViewModels possible I set myself to define new binding. I have already explained the way which I have done this on my blog, and I have also provided a JsFiddle snippet containing a minimum running example. So excuse me for repeating myself if you have already stumbled upon these. The inspiration for this binding came from Silverlight and Bing Maps. Because Silverlight offers excellent support for MVVM and data–to–UI binding it is no surprise that it is used also for the maps. There is a nice way of adding a collection of items to a map which resembles this:
<map>
<itemscollections datasource="itemsVM" style="wayToRenderMarker"/>
</map>
This goes well with the principle of declarative UI definition: We are not forced to write imperative good just to define “How” things should look like. And I have to say that, this principle is something that I miss too much any time when I end up using JS with JQuery. Enough of philosophy – let’s focus on getting things done. Here is the declarative part:
<div id="map_canvas">
</div>
<div data-bind="foreach: selectedCity().tips">
<div data-bind="latitude: lat, longitude:lng, map:map, selected:selected">
</div>
</div>
You can see that I am using a new binding which takes latitude, longitude, map and a magic selected callback. Now we can take a look at the necessary plumbing needed for this code. Declaration of new binding is done by declaring new object on the bindingHandlers property of Knockout.
ko.bindingHandlers.map = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var position = new google.maps.LatLng(allBindingsAccessor().latitude(), allBindingsAccessor().longitude());
var marker = new google.maps.Marker({
map: allBindingsAccessor().map,
position: position,
icon: 'Icons/star.png',
title: name
});
google.maps.event.addListener(marker, 'click', function () {
viewModel.select()
});
markers.push(marker);
viewModel._mapMarker = marker;
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var latlng = new google.maps.LatLng(allBindingsAccessor().latitude(), allBindingsAccessor().longitude());
viewModel._mapMarker.setPosition(latlng);
if (viewModel.selected()) {
} else {
}
}
};
Two functions are defined in the binding declaration. The init function is executed the first time, that Knockout uses the ViewModel to be render. The update function is logically executed every time that ViewModel changes. Knockout passes some very useful parameters to both of these functions:
- element – the HTML element used to render the ViewModel
- valueAccessor – the value passed to the binding. Here we are defying a MAP binding, so this will hold the value which is passed to the map parameter.
- allBindingsAccessor – this object can be used to discover all the objects passed on all bindings. This is useful, because we can recuperate all the values passed to the other binding parameters (latitude, longitude and selected).
- ViewModel – obviously the ViewModel object – in our case it will correspond to one tip to be shown on the map.
Map binding initialization
The init function actually obtains all the values : latitude, longitude, the map and the callback which should be executed when the marker is clicked. Marker is created and placed on the map. Now the marker has to be saved somewhere, in order to enable later changes. The nicest place to save the marker is the ViewModel itself:
viewModel._mapMarker = marker;
Here we obviously assume, that there is now property called _mapMarker defined on the ViewModel.
Map binding update
The update is executed any time that something about the ViewModel (the Information Tip) changes. In our case it might be the position or the value of the selected property. By saving the marker to the ViewModel we can easily make a change of the markers position:
viewModel._mapMarker.setPosition(latlng);
Subscribing to the city changed event
There is one more thing to take care of. Each time the city changes, there has to be some cleanup of the map performed. The markers, that we define stay in the map until the setMap(null) trick is called on them (that’s the way Google Maps API behaves). Luckily Knockout comes up with a way to subscribe to change of any observable. On the other way that would be a weird observable if we would not be able to actually observe it wouldn’t it?
countryList.selectedCity.subscribe(function (newValue) {
var oldCityTips = countryList.oldCity.tips
for (var i = 0; i < markers.length; i++) {
var marker = markers[i];
marker.setMap(null);
}
markers = [];
);
Points of Interest
One thing to point out is that KnockoutJS is very powerful. I thought that the transition from Silverlight/WPF to HTML/JavaScript will be much more painful. But actually any time I was facing some issue, I found a solution in one of the KnockoutsJS features (custom binding, templates, after-rendering callback, subscribing to observables).
Also you have to note that JavaScript is very flexible language, but code written in JavaScript is really hard to maintain and organize. KnockoutJS gives us at least a design pattern which can be used to keep the code organized.
More about the project
The project is up and running in AppHarbor. The mobile application which was written for Windows Phone 7 and which I have described about year ago, was taken out from market place (by me actually ). I could not support backwards compability and change the application any time that web services interface have changed (and there were quite some changes since). And I do not actually possess WP7 phone and it is a little bit cumbersome to test on the emulator. I am more and more busy with other projects so right now I am wondering whether there are people out there interested to help me maintain this as an open source project. If you wish let me know.
History
- 20/04/2012 - Initial posting of the article