Knockout.js with Web API : The perfect compination

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.
webapiKnockoutDiagram
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.
webapiKnockout_01
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).
webapiknockout
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!

Advertisements


Categories: ASP.NET

Tags: , ,

4 replies

  1. 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.

  2. 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.

  3. Can you please create more articles about Knockout, thank you 😉

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Chara Plessa

The purpose of this blog is to broaden my education, promote experimentation and enhance my professional development. Albert Einstein once said that “If you can’t explain it simply, you don’t understand it well enough” and I strongly believe him!

chsakell's Blog

Anything around ASP.NET MVC,WEB API, WCF, Entity Framework & AngularJS

Kumikoro

A Front End Developer's Blog

Muhammad Hassan

Full Stack Developer | ASP.NET | MVC | WebAPI | Advanced Javascript | AngularJS | Angular2 | C# | ES6 | SQL | TypeScript | HTML5 | NodeJS, MS candidate @LUMS, Grad & EX-Adjunct Faculty @NUCES-FAST, seasonal blogger & open-source contributor. Seattle, WA.

Software Engineering

Web development

IEvangelist

.NET, ASP.NET, C#, MVC, TypeScript, AngularJS

leastprivilege.com

Dominick Baier on Identity & Access Control

Happy DotNetting

In Love with Technology

Knoldus

Knols of experience to your advantage

knowshnet

Search - Read - Request - Share

Rahul's space

Learn, Share and Grow with me !

Dhananjay Kumar

Developer Evangelist @Infragistics | MVP @Microsoft |

SQL Authority with Pinal Dave

SQL Server Performance Tuning Expert

Conficient Blog

Random bits of tech from @conficient

Code! Code! Code!

SOLID & KISS

Code Wala

Designing and coding

Microsoft Mentalist

A way to start with Microsoft Technologies

Tony Sneed's Blog

A glimpse into the lives of Tony & Zuzana Sneed

Sriramjithendra Nidumolu

Personal Notes of Sriramjithendra

%d bloggers like this: