We have seen in many posts on this blog, how to call asynchronously a Web API method from a simple HTML page, using jQuery Ajax. Each time we were retrieving some JSON formatted data, we had to update the respective HTML elements using jQuery code. Here’s a simple example (got from this post..)
$(document).ready(function () { var URL = 'home/ManufacturerList/'; $.getJSON(URL, function (data) { var items = '<option value="">Select Manufacturer</option>'; $.each(data, function (i, manufacturer) { items += "<option value='" + manufacturer.Value + "'>" + manufacturer.Text + "</option>"; }); $('#ManufacturersID').html(items); }); });
Take a good look at the highlighted code. We wrote some jQuery code to fill a select list with some ‘option’ elements manually. Let’s be honest here. This may work just fine, but adding manually ‘option’ elements to a ‘select’ list? Wouldn’t it be much more better to fill a Javascript array variable (let’s say “manufacturersArray”) and bind this element to the respective select list? I mean something like this..
<select id="selectManufacturers" data-bind="options: manufacturersArray, optionsText: 'Name', optionsValue: 'Id'">
This is actually how Knockout.js works. You use the MVVM pattern to keep synchronized both the View and the ViewModel (your javascript objects). This isn’t though a Knockout.js tutorial but a complete example that shows you how to use Web API and knockout.js library together in order to write more clearer, elegant, manageable and productive code. Here’s the scenario for this post. We are going to use the Chinook database to create a UI in order to
a) Search an Artist
b) View it’s Albums
c) View and Edit each Album’s Tracks
You will need to create the Chinook database to follow with this post. You can download it for free here or simply run the SQL script you will find inside the App_Data directory of the project we are going to create. For our solution, we will create the Model using the Entity Framework and we will use Web API controller classes for serving data. Those controllers will always return and get only DTO objects (Data Transfer Objects). In this way you can pass or retrieve from client, only certain types of objects. Our Knockout – ViewModel code, will consist of the respective Artist, Album, Track, and Genre Javascript objects which are going to have only those properties and functions we need, so that we can create the functionality we mentioned before. The View that is a simple HTML page will consist of HTML elements where each of those will have the common data-bind knockout attribute bound to a relative ViewModel property. Here is the diagram for our solution.
Because of the fact that this project has a lot of code, I will be only showing you every time, the code for each of the respective componet (Model, ModelView, View) while working a particular feature. Of course you can download this project from the bottom of this post so you can get the most of it. (In fact I strongly recommend you to do so). Let’s start. First of all you need to have the Chinook database created in your SQL Server instance. I have created a new empty ASP.NET Web Application in which I installed the Web API core libraries from the Nuget Packages.. Then I created a new Web API route in the Global Configuration file “Global.asax” (which I also created”).
protected void Application_Start(object sender, EventArgs e) { var config = GlobalConfiguration.Configuration; config.Routes.MapHttpRoute("DefaultHttpRoute", "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); }
Inside a folder named “Model” create an ADO.NET Entity Data Model file named “ChinookModel”, pointing to the Chinook database and adding the “Artist”, “Album”, “Track” and “Genre” table. This will create the respective Domain classes. From the Nuget Packages install Knockout.js and jQuery libraries. This will create folder named Scripts where you will find the respective javascript files. Also add a new javascript file named “index.js”. This will hold our Knockout code. Create a new HTML page named Index.html and make sure you import the above script files, right above the body closing tag element.
<script src="../Scripts/knockout-2.3.0.js"></script> <script src="../Scripts/jquery-2.0.3.min.js"></script> <script src="../Scripts/index.js"></script> </body>
As we mentioned previously we want first to find an Artist. So what we need is a input element and a button to search artists. Inside the Model folder add a new “DTO” folder and create a class named “ArtistDTO”, pasting the following code.
public class ArtistDTO { public int ArtistId { get; set; } public string Name { get; set; } }
Inside the “Controllers” folder add a new Web API Controller class named ArtistsController. Paste the following code..
public class ArtistsController : ApiController { public HttpResponseMessage Get(string name) { HttpResponseMessage responseMessage; using (var context = new ChinookEntities()) { var artists = context.Artists.Where(a => a.Name.Contains(name)).AsEnumerable(); var artistsDTO = new List<ArtistDTO>(); if (artists != null) { foreach (var artist in artists) { artistsDTO.Add(new ArtistDTO { ArtistId = artist.ArtistId, Name = artist.Name }); } responseMessage = Request.CreateResponse<List<ArtistDTO>>(HttpStatusCode.OK, artistsDTO); } else { responseMessage = Request.CreateErrorResponse(HttpStatusCode.NotFound, new HttpError("Artist not found!")); } } return responseMessage; } }
We only need a simple Method to find Artists by name or more specifically, find all artists that their name contains a specific word. When those artists are found, a list of ArtistDTO is returned in JSON format. Before binding the returned data to our ViewModel, first let’s bind the click event of the button to a ViewModel’s function that calls the above GET method. The required HTML elements would be..
Search an Artist: <input type="text" id="inputSearchArtist" /> <button data-bind="click: searchArtists" type="button" id="btnSearch">Search</button>
You need create an “Artist” class in your index.js file.
function Artist(artistId, artistName) { var self = this; self.ArtistId = ko.observable(artistId); self.Name = ko.observable(artistName); self.ArtistAlbumsUrl = ko.computed(function () { return "../api/albums?artistId=" + this.ArtistId(); }, this); }
The button with id=”btnSearch” has it’s click event bound to a ViewModel’s searchArtists function. This function will call the Web API action we created previously and fill a ko.observableArray([]) ViewModel’s property named “ArtistsSearched”.
function ChinookViewModel() { // Properties var self = this; this.ArtistsSearched = ko.observableArray([]); // Functions this.searchArtists = function () { var name = $('#inputSearchArtist').val(); $('#tbodySearchArtists').empty(); $.getJSON("/api/artists?name=" + name, function (artists) { $.each(artists, function (index, artist) { self.ArtistsSearched.push(new Artist(artist.ArtistId, artist.Name)); }); }); }; // code omitted
All we need to do now, is to bind this ArtistsSearched observable array to a table, using the Knockout’s foreach attribute.
<table style="visibility: hidden" id="tblSearchArtists"> <thead> <tr> <th>ID</th> <th>Artist</th> <th>Albums</th> </tr> </thead> <tbody data-bind="foreach: ArtistsSearched" id="tbodySearchArtists"> <tr> <td><span class="first" data-bind="text: ArtistId"></span></td> <td><span data-bind="text: Name"></span></td> <td> <button data-bind=" click: $root.showArtistAlbums">View Albums</button></td> </tr> </tbody> </table>
You will have noticed that the last cell has a button with it’s click event bound to a $root.showArtistAlbums expression. We added the $root word so that we can move up to the ViewModel’s root functions. Otherwise you could only see only functions existed in an Artist object (we don’t have any). This has to do with the current Knockout context you work with and since you used the “foreach: ArtistsSearched” data-bind, everything inside it represents an Artist object. At this moment you should at list be able to search for Artists.
Now we have to display all albums for a selected Artist, that is to implement the $root.showArtistAlbums functionality. Again let’s start from the DTO objects. Add a new class named “AlbumDTO” inside the DTO folder.
public class AlbumDTO { public int AlbumId { get; set; } public string Title { get; set; } public int ArtistId { get; set; } }
Then add a new Web API Controller class named “AlbumsController” as follow.
public HttpResponseMessage Get(string artistId) { HttpResponseMessage responseMessage; using (var context = new ChinookEntities()) { int id = Int32.Parse(artistId); var albums = context.Albums.Where(a => a.ArtistId == id).AsEnumerable(); var albumsDTO = new List<AlbumDTO>(); if (albums != null) { foreach (var album in albums) { albumsDTO.Add(new AlbumDTO { AlbumId=album.AlbumId, Title=album.Title }); } responseMessage = Request.CreateResponse<List<AlbumDTO>>(HttpStatusCode.OK, albumsDTO); } else { responseMessage = Request.CreateErrorResponse(HttpStatusCode.NotFound, new HttpError("No albums found!")); } } return responseMessage; }
We only need one GET method that can fetch all albums that are associated with an Artist with id “artistId”. The result will be an list of AlbumDTO data in JSON format. Take a look at Knockout.js library power now. When we created the Artist javascript object, we added a ko.computed property..
self.ArtistAlbumsUrl = ko.computed(function () { return "../api/albums?artistId=" + this.ArtistId(); }, this);
This property will always return the previously created Web API GET method URL, for a respective Artist. So simple. If you remember, the button element that will invoke the ViewModel’s showArtistAlbums function (not implemented yet) is inside the “foreach: ArtistsSearched” context. That means that it can pass the Current Artist ko.observable object, associated in the respective table row! All we need now is to implement that function in the ViewModel. Before doing so, we need to add an Album javascript class and the respective ko.observable array in the ViewModel.
function Album() { var self = this; self.AlbumId = ko.observable(); self.Title = ko.observable(""); self.ArtistId = ko.observable(); }
function ChinookViewModel() { // Properties var self = this; this.ArtistsSearched = ko.observableArray([]); this.ArtistAlbumsSearched = ko.observableArray([]); // Functions this.showArtistAlbums = function (artist) { self.ArtistAlbumsSearched([]); $.getJSON(artist.ArtistAlbumsUrl(), function (albums) { $.each(albums, function (index, album) { self.ArtistAlbumsSearched.push(album); }); firstAlbumId = albums[0].AlbumId; }).done(function () { self.showAlbumTracks(); }); };
Now again, we need to bind this ko.observable array to an HTML element in the View. Instead of using a table, this time let’s use a select list.
<select id="selectArtistAlbums" data-bind="options: ArtistAlbumsSearched, optionsText: 'Title', optionsValue: 'AlbumId', event: { change: function (data, event) { showAlbumTracks($('#selectArtistAlbums').val(), data, event) } }"> </select>
The most important part of the above code is the
data-bind="options: ArtistAlbumsSearched, optionsText: 'Title', optionsValue: 'AlbumId'
which indicates that a list of albums will be bound to the options list of a select HTML element. For each option, the Album.Title property will be used for it’s text property, while the Album.AlbumId will be used for it’s value. Last but not least, we have bound the select element’s list ‘change’ event to a “showAlbumTracks” function. This function accepts as a parameter the selected dropdown list item’s value and is supposed to display the respective selected album tracks. Let’s start over, once again. We need to create a TrackDTO transfer object inside the Model/DTO folder as follow..
public class TrackDTO { public int TrackId { get; set; } public string Name { get; set; } public string Genre { get; set; } public int Milliseconds { get; set; } public decimal UnitPrice { get; set; } }
And of course, a Web API Controller class, named TracksController which will be able to return all Tracks according to an AlbumId parameter passed.
public class TracksController : ApiController { public HttpResponseMessage Get(string albumId) { HttpResponseMessage responseMessage; using (var context = new ChinookEntities()) { int id = Int32.Parse(albumId); List<TrackDTO> tracksDTO = new List<TrackDTO>(); var tracks = context.Tracks.Where(t => t.AlbumId == id).AsEnumerable(); if (tracks.Count() > 0) { foreach (var track in tracks) { tracksDTO.Add(new TrackDTO { TrackId = track.TrackId, Name = track.Name, Genre = track.Genre.Name, Milliseconds = track.Milliseconds, UnitPrice = track.UnitPrice }); } responseMessage = Request.CreateResponse<List<TrackDTO>>(HttpStatusCode.OK, tracksDTO); } else { responseMessage = Request.CreateErrorResponse(HttpStatusCode.NotFound, new HttpError("No tracks found for this album")); } } return responseMessage; } }
In the client side, we need to add a Track Javascript class..
function Track(trackId, name, genre, milliseconds, unitPrice) { // Properties var self = this; self.TrackId = ko.observable(trackId); self.Name = ko.observable(name); self.Genre = ko.observable(genre); self.Milliseconds = ko.observable(milliseconds); self.UnitPrice = ko.observable(unitPrice); self.FormattedPrice = ko.computed(function () { return self.UnitPrice() + " $"; }, this); self.CurrentTemplate = ko.observable("displayTrack"); }
Notice the self.CurrentTemplate Track’s property. We are going to use this property in order to alternate between different templates in the View’s side. In other words, we will create two different templates for displaying tracks. One to display them (displayTrack) and another for editing (editTrack). Only one template can be active at a time and that depends on the corresponding CurrentTemplate property. We need to add another ko.observableArray([]) in the ViewModel for holding the Track list to be displayed in the respective template. I believe now it’s a good time to show the complete code for our ViewModel.
function ChinookViewModel() { // Properties var self = this; this.ArtistsSearched = ko.observableArray([]); this.ArtistAlbumsSearched = ko.observableArray([]); this.AlbumTracks = ko.observableArray([]); this.Genres = ko.observableArray([]); // Functions this.searchArtists = function () { //$('#tblSearchArtistAlbums').css('visibility', 'hidden'); $('#divSelectArtistAlbums').css('visibility', 'hidden'); $('#tblSelectedAlbumTracks').css('visibility', 'hidden'); var name = $('#inputSearchArtist').val(); $('#tbodySearchArtists').empty(); $.getJSON("/api/artists?name=" + name, function (artists) { $.each(artists, function (index, artist) { self.ArtistsSearched.push(new Artist(artist.ArtistId, artist.Name)); }); }); $('#tblSearchArtists').css('visibility', 'visible'); }; this.showArtistAlbums = function (artist) { self.ArtistAlbumsSearched([]); $('#tbodySearchArtistAlbum').empty(); $.getJSON(artist.ArtistAlbumsUrl(), function (albums) { $.each(albums, function (index, album) { self.ArtistAlbumsSearched.push(album); }); firstAlbumId = albums[0].AlbumId; }).done(function () { self.showAlbumTracks(); }); $('#divSelectArtistAlbums').css('visibility', 'visible'); }; this.showAlbumTracks = function (data, event) { if (data == null) { data = firstAlbumId; } $('#tblSelectedAlbumTracks').css('visibility', 'visible'); self.AlbumTracks([]); $.getJSON('/api/tracks?albumId=' + data, function (tracks) { $.each(tracks, function (index, track) { self.AlbumTracks.push(new Track(track.TrackId, track.Name, track.Genre, track.Milliseconds, track.UnitPrice)); }); }); } }
Now to display the current AlbumTracks array we need to add a table. This table though will make use of the template data-bind. First let’s add the table.
<table id="tblSelectedAlbumTracks" style="visibility: hidden; width: 570px"> <thead> <tr> <td>Id</td> <td>Name</td> <td>Genre</td> <td>Milliseconds</td> <td>Price</td> </tr> </thead> <tbody id="tbodyAlbumTracks" data-bind="foreach: AlbumTracks"> <tr data-bind="template: { name: CurrentTemplate }"> </tr> </tbody> </table>
Notice that for each Track the respective ‘tr’ being displayed depends of the CurrentTemplate value. In Knockout, you can create templates simply by creating scripts with their type=”text/html”. Let’s create the two templates we mentioned before, one for displaying tracks and another for editing them. Add those scripts after the title HTML element.
<script type="text/html" id="displayTrack"> <td><span class="first" data-bind="text: TrackId"></span></td> <td><span data-bind="text: Name"></span></td> <td><span data-bind="text: Genre"></span></td> <td><span data-bind="text: Milliseconds"></span></td> <td><span data-bind="text: FormattedPrice"></span></td> <td> <button data-bind="click: editTrack">Edit</button></td> </script> <script type="text/html" id="editTrack"> <td> <span data-bind="text: TrackId" /></td> <td> <input data-bind="value: Name" /></td> <td> <select id="selectGenres" data-bind="options: $root.Genres, optionsText: 'Name', optionsValue: 'Name', value: $data.Genre"> </select></td> <td> <input data-bind="value: Milliseconds" /></td> <td> <input data-bind="value: UnitPrice" /></td> <td> <button data-bind="click: updateTrack">Update</button></td> <td> <button data-bind="click: cancelEditTrack">Cancel</button></td> </script>
I think it’s quite simple to understand the difference between them. You will notice that some new functions must be added in the Javascript Track object (editTrack, updateTrack, cancelTrack)
function Track(trackId, name, genre, milliseconds, unitPrice) { // Properties var self = this; self.TrackId = ko.observable(trackId); self.Name = ko.observable(name); self.Genre = ko.observable(genre); self.Milliseconds = ko.observable(milliseconds); self.UnitPrice = ko.observable(unitPrice); self.FormattedPrice = ko.computed(function () { return self.UnitPrice() + " $"; }, this); self.CurrentTemplate = ko.observable("displayTrack"); // Functions self.editTrack = function () { self.CurrentTemplate("editTrack"); } self.cancelEditTrack = function () { self.CurrentTemplate("displayTrack"); } self.updateTrack = function () { var updatedTrack = { TrackId: self.TrackId(), Name: self.Name(), Genre: self.Genre(), Milliseconds: self.Milliseconds(), UnitPrice: self.UnitPrice() }; $.ajax({ type: "PUT", url: "/api/tracks/"+self.TrackId(), dataType: "json", data: updatedTrack, success: function () { $('#divTrackUpdated').slideDown(3000); $('#divTrackUpdated').slideUp(500); self.CurrentTemplate("displayTrack"); }, error: function () { alert('Error while trying to update track..'); } }); } }
In the “editTrack” template, I used a select tag to display the all Genres available. I won’t explain how I did that, you can check the code inside the project (of course I followed the same procedure). To update a Track you need to add another PUT method to the TracksController.
public HttpResponseMessage Put(int id, TrackDTO track) { HttpResponseMessage responseMessage; using (var context = new ChinookEntities()) { var oldTrack = context.Tracks.Find(id); if (oldTrack != null) { int newGenreId = context.Genres.Where(g => g.Name == track.Genre).SingleOrDefault().GenreId; oldTrack.Name = track.Name; oldTrack.GenreId = newGenreId; oldTrack.Milliseconds = track.Milliseconds; oldTrack.UnitPrice = track.UnitPrice; try { context.SaveChanges(); responseMessage = Request.CreateResponse<TrackDTO>(HttpStatusCode.OK, track); } catch (Exception ex) { responseMessage = Request.CreateErrorResponse(HttpStatusCode.BadRequest, new HttpError("Track wasn't updated!")); } } else { responseMessage = Request.CreateErrorResponse(HttpStatusCode.BadRequest, new HttpError("Track wasn't found!")); } } return responseMessage; }
Let’s see our application in action now (you may have to click on the following GIF image to see it working).
That’s it, we saw how to use Web API with Knockout.js library and how this awesome Javascript library can simplify model data binding in View side. You can download the project we created from here. I understand it’s been a lot of code posted in this post but I hope it’s worth it!
Categories: ASP.NET
Cool, nice Knockout.js example!
Line 4 of the first code sample puzzles me a bit, I was expecting a Select tag containing options. Creation of HttpResponseMessage goes a bit fast for me, I will look it up. I prefer code commands to state what it achieves, not what language features is used. I like simple to understand code like ajax calls that take prepared arguments like “url” and “data”, not long calls with inline arguments creations. But that all a matter of taste. Good job.
Whether I will start using Knockout is another matter. Currently I lean more towards Bootstrap because I feel it’s demands less additions in the html markup and more controle over databinding. But I am a newby w.r.t using databinding js libs.
Hi Jaap, thanks for your comments. Line 4 you mentioned, isn’t difficult to understand, it’s actually code taken from another post on this blog. If you notice, line 9 of the same code adds all the ‘option’ elements created, to a ‘select’ element with id=”ManufacturersID”. I hope now it’s clearer to you to understand. Kind regards,
C. S.
Okay understood thanks.
“select” in html and “options” scripted imho is confusing, I prefer the style used in jQery doc:
$.getJSON( “ajax/test.json”, function( data ) {
var items = [];
$.each( data, function( key, val ) {
items.push( “” + val + “” );
});
$( “”, {
“class”: “my-new-list”,
html: items.join( “” )
}).appendTo( “body” );
});
Using the first option from a select element to inform the user that something can be selected doesn’t feel right. I feel the html5 standard should allow placeholder not being an option.
Can you please create more articles about Knockout, thank you 😉