Learn how to use Knockout in an ASP.NET MVC 3 web application to handle real-time UX updates.

Posted on July 4th, 2011

Knockout provides a great way to implement a MVVM (Mode-View-View Model) pattern in JavaScript on the client side and is easy to integrate with an ASP.NET MVC 3 application. Using a prototype application as an example, we will explore how to create a block of notification data and continuously update it from data received via a controller action. The application markup will display the inventory levels of a product and will include a message when the product has reached a reorder level. It will have a single controller responsible for rendering a single view and providing an action method for getting the current inventory data state.

First things first: get Knockout

Knockout is available via NuGet. You can search for "knockout" in the Manage NuGet Packages dialog or you can use the Package Manager Console and run the command Install-Package knockoutjs. After adding the package we can include a

The View

The first step to working with Knockout is to craft your view model(s) for your UX. Assuming we have a controller with an Index.cshtml view set up, within a

The Site.css file:

.Hidden { display:none; }
.ImportantMessage { color:#ff0000; }

The Model Classes

A basic model class named InventoryData is created in MVC to handle the inventory data structure. It only needs to contain properties for the "In Stock" and "Reserve" values. All other data is handled via the Knockout code we put in the View.

namespace Website.Models
{
    public class InventoryData
    {
        public int InStock { get; set; }
        public int Reserve { get; set; }
    }
}

To simulate a data management layer we will use a similar method that was used in an earlier post, "Build a dialog form using jQuery UI in MVC 3". We can create a class named InventoryManager with a method to get inventory data and create initial data. This class will store data in the runtime cache to give us a rudimentary way of simulating data storage and persistence in our prototype. You would want to refactor this to work with whatever data storage means you are currently or planning to implement.

using System;
using System.Web;

namespace Website.Models
{
    public class InventoryManager
    {
        public InventoryData GetInventoryData()
        {
            if(HttpRuntime.Cache["InventoryData"] == null)
                return this.CreateInitialData();
            var item = (InventoryData)HttpRuntime.Cache["InventoryData"];
            item.Reserve += this.generateRandomReserveAmount();
            return item;
        }

        public InventoryData CreateInitialData()
        {
            var item = new InventoryData { InStock = 20, Reserve = 0 };
            HttpRuntime.Cache["InventoryData"] = item;
            return item;
        }

        private int generateRandomReserveAmount()
        {
            var random = new Random();
            return random.Next(1, 6);
        }
    }
}

The private generateRandomReserveAmount is used to simulate someone placing an order for the product, thus putting X amount in "Reserve". This code is called in the GetInventoryData method so that each call to that method will trigger the simulation. Again, the code in this class is purely there to get our UX functioning in a sample without the overhead of creating a full blown data layer.

The Controller

Using a general HomeController for the prototype, we can add an action method for rendering the view and one for getting the inventory data. The Index method will just return the view. The InventoryData method will instantiate an instance of an InventoryManager class, call the GetInventoryData method to get an instance of an InventoryData model, and return the model instance as JSON.

using System.Web.Mvc;
using Website.Models;

namespace Website.Controllers
{
    public class HomeController : Controller
    {
        public static int CurrentInning;

        public ActionResult Index()
        {
            return View();
        }

        [OutputCache(Duration = 0)]
        public JsonResult InventoryData()
        {
            var manager = new InventoryManager();
            var model = manager.GetInventoryData();
            return Json(model, JsonRequestBehavior.AllowGet);
        }
    }
}

Since the jQuery call will cache the call to the content in Internet Explorer by default (a setting in jQuery), we need to decorate the InventoryData action method with the OutputCache attribute and a duration of 0. This will send the no-cache flag back in the header of the content to the browser and jQuery will pick that up and not cache the call, thus keeping our data fresh each time the controller method is polled for data.

Lights, camera, action

Everything is in place and the code is ready to run. Doing an F5 will result in the inventory levels getting rendered and the "order more" message hidden. Approximately every 5 seconds the InventoryData controller action will get hit and the UX will get updated with new "Reserve" and "Available" amounts. Once the "Available" amount falls below 5 the font color of the "Available" will change to red and the "order more" message becomes visible. If you don't feel like waiting it out you can refresh the page over and over, which will trigger the initial call to the controller action and move you through the simulation faster.

Extra credit

Lets take a look at another feature of Knockout and in the process provide us a means of resetting our data (instead of having to either kill Cassini or IIS Express to clear the cache from our data storage simulation code). We can create a button to reset levels and use Knockout to bind a click event to it. Start by adding a button element within the "InventorySummary" element and set the data-bind attribute to use the "click" binding:

  • In Stock:
  • Reserve:
  • Total Available:

This tells Knockout to monitor the click event of the element and call the function that is bound to the ResetLevels field of the view model. To add that logic, all we need to do is define a new field in the JavaScript view model instantiation and set its value to a function:

    var inventoryDataViewModel = {
        InStock: ko.observable(0),
        Reserve: ko.observable(0),
        ResetLevels: function () {
            $.post('/Home/ResetInventory', function (data) {
                inventoryDataViewModel.InStock(data.InStock);
                inventoryDataViewModel.Reserve(data.Reserve);
            });
        }
    };

The function will call the jQuery $.post() method to hit a controller action named ResetInventory and upon completion it will update the view model fields. By updating the view model fields here we can keep the UX data current without having to interrupt the current polling that is taking place via our existing logic.

The only other thing we need to do is add the controller action method to the HomeController:

public JsonResult ResetInventory()
{
    var manager = new InventoryManager();
    var model = manager.CreateInitialData();
    return Json(model);
}

Since the InventoryManager class already has the method CreateInitialData, we simply call that and use the returned value as the model for the JsonResult. Now we have a way to manually reset the data while the page is up.

Knockout has a ton of potential uses on the client side for gaining control over your application code structure and maintainability. It also helps reduce the amount of code needed to handle changes in data and user events which is a welcome relief, especially as your application grows and you realize how much jQuery you have been writing to wire up and pull strings to make your interface dance!

Download the code

Discussion

Simon
Simon
08 Jul, 2011 05:54 AM

This is a very well written article and I enjoyed it. However this isn't actually "real time" it is just close to real time. If you coupled this with a comet implementation then you could eliminate the need to do polling of the server.

08 Jul, 2011 02:43 PM

Simon
Thanks for the comment! Do you have an example or link of how to implement a Comet solution? Looks like the Wikipedia entry gives a good summary of the concept.

Michael Flynn
Michael Flynn
08 Jul, 2011 03:43 PM

I just did what you with knockout.js did but actually implemented long polling as the solution instead of setTimeout. Look into AsyncController to accomplish this. You can view the long polling on grassrootshoops.com and Firebug.

Simon Timms
Simon Timms
11 Jul, 2011 03:34 AM

Take a look at https://github.com/nmosafi/aspcomet which is a pretty good solution for .net. We put out state updates via nservicebus and have the asp.net site act as a listener for these messages. They they filter the messags and send them on to interested clients. If web sockets ever gain better acceptance, and they will, then that is a way better solution. As it stands our site defaults to long polling which isn't ideal.

Felix
Felix
11 Jul, 2011 05:23 AM

Justin, Excellent article. I have a few comments, but those are just minor gripes :)

You have css:{ ImportantMessage: StockIsLow } (in two places). I think it's just a typo and should be css:{ ImportantMessage: StockIsLow() }, as is in another place.

Also, you have second parameter in applyBindings - $("#InventorySummary")[0], but you never describe why you need it there. After all, it works just fine without it, and examples on knockout page don't have second param.

Last, you simplified your example by putting JavaScript at the end of the file. Nowadays we typically put all of JS at the top and wrap it into $function({ ... }). However, if I do that, there is a problem with setTimeout. I would recommend to put PollInventoryData inside JQuery block and then change seTimeout as follows:

function PollInventoryData() {
    $.get('/Home/InventoryData', function (data) {
        inventoryDataViewModel.InStock(data.InStock);
        inventoryDataViewModel.Reserve(data.Reserve);
        setTimeout(PollInventoryData, 5000);
    });
}
Felix
Felix
11 Jul, 2011 05:59 AM

Sorry, two more. You run setTimeout(), but never clear it - in fact never assign it to a variable, that would allow you to clear it. So, even when stock is low and the page shows "order more widgets", the code pings the server for update, and stock keeps going down...

And really minor. It would be more intuitive to show InStock going down, rather than Reserve for some reason going up. At least, that's what my retailer wife tells me ;)

11 Jul, 2011 03:40 PM

Felix
Thanks for the feedback (and contribution) as usual!

  1. The css:{ ImportantMessage: StockIsLow } for the total available doesn't need the () because the data in StockIsLow is a bool. The css: { Hidden: !StockIsLow() } in the message uses the () because there is an expression there (the check for not "stock is low"). These two are different to illustrate how you handle each case in Knockout. If you have a variable that resolves to a true/false then you don't need the (). Otherwise you need to call the function on that variable if you want to use it in an equation in the data-bind attribute. Knockout docs section for more info.
  2. The paragraph right above the ko.applyBindings(inventoryDataViewModel, $("#InventorySummary")[0]); in the article explains the second param.
  3. Removing the string for the function name (and using the reference to the function instead) in the call to setTimeout is a better approach. Good call.
11 Jul, 2011 03:53 PM

Felix

  • If you wanted halt the polling then you would need to add some logic within the PollInventoryData function to make that decision, then not call the setTimeout method. The recursive call to PollInventoryData should be the thing that is causing the repeat polling. The setTimeout should simply be delaying the call to it for a brief moment.
  • My hypothetical app is for inventory managers to monitor their stock levels and get to work ordering more from their vendors when the demand gets close to exceeding stock. :) So they need to know that they still have 20 on the shelves but there are orders out there placed for 17 and if they don't get their butt in gear and order more before the end of the day then the sales team is going to have to tell customers they are out of stock! But you could totally make your sample app do whatever you (or your wife) need it to do ;)
11 Jul, 2011 04:00 PM

Simon Timms
Thank you for sharing the reference to the "comet" implementation in .NET!
https://github.com/nmosafi/aspcomet

11 Jul, 2011 04:13 PM

Michael Flynn
Thanks for the suggestion on AsyncController. I found a summary of it on the MSDN docs site but it looks like it was written for MVC 2. Not sure if much has changed on it in MVC 3 or not. Probably still worth the read. I also found the post How to Use Asynchronous Controllers in ASP.NET MVC2 & MVC3 that does a good job expanding on the MSDN doc.

Felix
Felix
13 Jul, 2011 05:15 AM
  1. Hmm... you are right - StockIsLow works with or without parenthesis; however !StockIsLow requires parens. Well, it is kinda counter-intuitive. As my English teacher used to say, "you can't explain it - you should memorize it".
  2. Sorry, I missed the stuff before the code; was looking only at the statement after it :(

  3. Got to go with what my wife says... :D

13 Jul, 2011 02:57 PM

Felix
Always go with what the wife says :)

Anne
Anne
04 Jan, 2012 05:56 PM

Nice work Justin.

Billy Braga
Billy Braga
17 Feb, 2012 07:07 PM

This ain't realtime !!! Like the first guy said...

06 Mar, 2012 07:59 PM

Thank you for your tutorial :)

No new comments are allowed on this post.