Click here to Skip to main content
Click here to Skip to main content

ASP.NET MVC, SignalR, and Knockout based real time UI syncing - For co-working UIs and continuous clients

By , 30 Jan 2012
Rate this:
Please Sign up or sign in to vote.

Introduction

New age web applications may need to offer new age user experiences - and should handle co-working and continuous client scenarios properly. This involves ensuring that the user interface is syncing properly itself across devices and across users to ensure the state of the application and the user interface is maintained "as is".

image

Real time data syncing across user views *was* hard, especially in web applications. Most of the time, the second user needs to refresh the screen to see the changes made by the first user, or we needed to implement some long polling that fetches the data and does the update manually.

Now, with SignalR and Knockout, ASP.NET developers can take advantage of view model syncing across users, that’ll simplify these scenarios in a big way, with minimal code. This post discusses how to implement a real time to-do pad, which will sync data across users accessing the application. This means, users can make changes to their tasks (add/remove/update etc.), and other users will see the changes instantly. The focus is on the technique, I’m not trying to build a fabulous user experience here.

I know we are tired with to-do examples, but now let us build a to-do application that can sync tasks between you and your wife (or your team mates) in real time, with full CRUD support, and persistence. And yes, we’ll keep the code minimal and maintainable using a proper View Model (oh, is that possible in JavaScript?).

So, see this video, and here you can see the changes you apply to the tasks in one screen (adding, deleting, updating, etc.). You can see that the data is getting synced across multiple users. We’ll be using KnockoutJs for maintaining a View Model, and will be syncing the View Model across users using SignalR. If you are not familiar with Knockout and SignalR, we’ll have a quick look at both of them on the way.

To start with, let us create a new ASP.NET MVC 3.0 application. Create an empty project, I’ve ASP.NET MVC 3 tools update installed. Once you’ve the ASP.NET MVC project created, bring up the Nuget console (View->Other Windows-> Package Manager console), and install the Nuget packages for Knockout and SignalR.

install-package knockoutjs

And SignalR:

install-package signalr

Also, do install Entity Framework latest version if you don't have the same, so that we can use the Code First features.

Install-Package EntityFramework

If you are already familiar with Knockout and SignalR, you may skip the next two titles and go directly to the 'Building KsigDo' section.

Knockout

Knockout Js is an awesome JavaScript library that allows you to follow the MVVM convention, to bind your user controls to a JavaScript view model. This is pretty cool, because it allows you to build rich UIs pretty easily, with very minimal code. Here is a quick example that shows how you can bind your HTML elements to a JavaScript view model.

Here is a very simple view model:

// This is a simple *viewmodel*
var viewModel = {
    firstName: ko.observable("Bert"),
    lastName: ko.observable("Bertington")
};


// Activates knockout.js
ko.applyBindings(viewModel);

The attributes are of type ko.observable(..), and if you want to convert the ViewModel to an object (which you can send over the wire), you can easily do that using ko.toJS(viewModel). Now, let us bind the above view model to a view.

The binding happens in the data-bind attribute, you may see that we are binding the value of the textbox to the firstname and last name variables. When you call ko.applyBindings, Knockout will do the required wiring so that the view model properties are synced with the target control's property values.

<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>

KnockoutJs is pretty easy to learn, the best way to start is by going through the interactive tutorial hosted by Knockout guys here at http://learn.knockoutjs.com/.

Update: Found that Shawn has wrote a comprehensive post on Knockout, read that as well.

SignalR

SignalR is the “greatest thing since sliced bread” that happened for Microsoft developers recently. (To know why, you can read my post HTML5 is on a killer spree, may kill HTTP next at least partially.) Anyway, SignalR is an async signaling library for ASP.NET to help build real-time, multi-user interactive web applications. If you have heard about Node, Backbone, Nowjs, etc. recently, you know what I’m talking about. If not, you’ll know pretty soon though.

The easiest starting point to understand SignalR is, by having a look at the Hub Quickstart example. Have a look at that example and come back.

You can inherit your Hub at the server side from SignalR.Hubs.Hub – and SignalR will generate the necessary light weight JavaScript proxies at the client side so that you can make calls to your hub over the wire, even with support for typed parameters. Not just that. SignalR also provides dynamic “Clients” and “Caller” objects in your hub, so that you can invoke a client side method written in JavaScript directly via your code in the server side. Pretty smart. And SignalR hides the entire implementation under its nice little APIs.

Building the KsigDo App

Now, let us go ahead and build our KsigDo app. Let us put together the bits step by step.

Task Model for Persistence Using Entity Framework Code First

In you ASP.NET MVC application, go to the Models folder, and add a new Code First model file. Our model is very minimal, and as you can see, we have a taskID and a title for a task, and a few validation rules defined, like title's length. Also, the Completed property decides whether the task is a completed one or not.

If you are not familiar with Entity Framework Code First, here is a good read in Scott’s blog, and here are a few more resources.

public class KsigDoContext : DbContext
{
    public DbSet<Task> Tasks { get; set; }
}

public class Task
{
    [Key]
    public int taskId { get; set; }
    [Required] [MaxLength(140)] [MinLength(10)]
    public string title { get; set; }
    public bool completed { get; set; }
    public DateTime lastUpdated { get; set; }

}

The DbContext and DbSet classes used above are provided as part of the EF4 Code-First library. Also, we are using attributes like Key, Required, etc. for data annotations, for basic validation support.

TaskHub for Basic Operations

Create a new folder named Hubs in your ASP.NET MVC project, and add a new TaskHub.cs file (no, we are not using Controllers now). And yes, you can place your Hubs anywhere. Here is our TaskHub, inherited from the SignalR.Hubs.Hub class. You may see that we are using this Hub to perform most of the CRUD operations in our Task Model.

public class Tasks : Hub
{
    /// <summary>
        /// Create a new task
    /// </summary>
        public bool Add(Task newTask)
    {
        try
        {
            using (var context = new KsigDoContext())
            {
                var task = context.Tasks.Create();
                task.title = newTask.title;
                task.completed = newTask.completed;
                task.lastUpdated = DateTime.Now;
                context.Tasks.Add(task);
                context.SaveChanges();
                Clients.taskAdded(task);
                return true;
            }
        }
        catch (Exception ex)
        {
            Caller.reportError("Unable to create task. Make sure title length is between 10 and 140");
            return false;
        }
    }

    /// <summary>
        /// Update a task using
    /// </summary>
        public bool Update(Task updatedTask)
    {
        using (var context = new KsigDoContext())
        {
            var oldTask = context.Tasks.FirstOrDefault(t => t.taskId == updatedTask.taskId);
            try
            {
                if (oldTask == null)
                    return false;
                else
                {
                    oldTask.title = updatedTask.title;
                    oldTask.completed = updatedTask.completed;
                    oldTask.lastUpdated = DateTime.Now;
                    context.SaveChanges();
                    Clients.taskUpdated(oldTask);
                    return true;
                }
            }
            catch (Exception ex)
            {
                Caller.reportError("Unable to update task. Make sure title length is between 10 and 140");
                return false;
            }
        }
    }

    /// <summary>
        /// Delete the task
    /// </summary>
        public bool Remove(int taskId)
    {
        try
        {
            using (var context = new KsigDoContext())
            {
                var task = context.Tasks.FirstOrDefault(t => t.taskId == taskId);
                context.Tasks.Remove(task);
                context.SaveChanges();
                Clients.taskRemoved(task.taskId);
                return true;
            }
        }
        catch (Exception ex)
        {
            Caller.reportError("Error : " + ex.Message);
            return false;
        }
    }


    /// <summary>
        /// To get all the tasks up on init
    /// </summary>
        public void GetAll()
    {
        using (var context = new KsigDoContext())
        {
            var res = context.Tasks.ToArray();
            Caller.taskAll(res);
        }

    }
}

The Clients and Caller properties are provided by SignalR as part of the Hub class definition. Surprise, these are dynamic objects that you can use conceptually to invoke a client side method written in JavaScript. SignalR does the plumbing using long polling or web sockets or whatever, and we don’t care. Also, as I mentioned earlier, SignalR will generate a client side proxy hub to invoke methods in our above written TaskHub, and we’ll soon see how to use this. For example, when a client invokes a GetAll method in the above Hub during initialization, that client invoking the GetAll method (Caller) will get a callback to its taskAll JavaScript method, with all the existing tasks.

In the same way, assuming that our Client hub has JavaScript methods like taskUpdated, taskAdded, taskRemoved, etc., – we are invoking those methods using the Clients dynamic object, so that whenever an update, add, or delete is happening, this information is broadcasted to all the clients connected right now.

The Main View

Now, let us go ahead and create our client side. Add a 'Home' controller and an 'Index' action. Create a new 'Index' view. Also, just make sure you’ve the necessary JavaScript script wirings to import Knockout and SignalR libraries (see the code).

Our Index page has got a couple of view models, and a bit of HTML (view). For view models, we’ve a taskViewModel, and a taskListViewModel, as shown below. You may note that our taskViewModel has almost the same properties as we have in our actual Task model, so that SignalR can manage the serialization/mapping pretty easily whenever we call the methods in our TaskHub.

You can see that in taskListViewModel, we are accessing the $connection.tasks proxy which provides a proxy object to access methods in our TaskHub. Also, we are attaching methods like tasksAll, taskUpdated, etc. to $connection.tasks via the this.hub pointer, and these methods are ‘invoked’ from the TaskHub class as we’ve seen earlier to virtually ‘push’ data to the clients.

$(function () {
    //---- View Models
    //Task View Model
    function taskViewModel(id, title, completed, ownerViewModel) {
        this.taskId = id;
        this.title = ko.observable(title);
        this.completed = ko.observable(completed);
        this.remove = function () { ownerViewModel.removeTask(this.taskId) }
        this.notification = function (b) { notify = b }


        var self = this;

        this.title.subscribe(function (newValue) {
            ownerViewModel.updateTask(ko.toJS(self));
        });

        this.completed.subscribe(function (newValue) {
            ownerViewModel.updateTask(ko.toJS(self));
        });
    }

    //Task List View Model
    function taskListViewModel() {
        //Handlers for our Hub callbacks

        this.hub = $.connection.tasks;
        this.tasks = ko.observableArray([]);
        this.newTaskText = ko.observable();

        var tasks = this.tasks;
        var self = this;
        var notify = true;

        //Initializes the view model
        this.init = function () {
            this.hub.getAll();
        }


        //Handlers for our Hub callbacks
        //Invoked from our TaskHub.cs

        this.hub.taskAll = function (allTasks) {
            var mappedTasks = $.map(allTasks, function (item) {
                return new taskViewModel(item.taskId, item.title,
                         item.completed, self)
            });

            tasks(mappedTasks);
        }


        this.hub.taskUpdated = function (t) {
            var task = ko.utils.arrayFilter(tasks(), 
                         function (value) 
                            { return value.taskId == t.taskId; })[0];
            notify = false;
            task.title(t.title);
            task.completed(t.completed);
            notify = true;
        };

        this.hub.reportError = function (error) {
            $("#error").text(error);
            $("#error").fadeIn(1000, function () {
                $("#error").fadeOut(3000);
            });
        }

        this.hub.taskAdded = function (t) {
            tasks.push(new taskViewModel(t.taskId, t.title, t.completed, self));
        };

        this.hub.taskRemoved = function (id) {
            var task = ko.utils.arrayFilter(tasks(), function (value) { return value.taskId == id; })[0];
            tasks.remove(task);
        };
        //View Model 'Commands'

        //To create a task
        this.addTask = function () {
            var t = { "title": this.newTaskText(), "completed": false };
            this.hub.add(t).done(function () {
                console.log('Success!')
            }).fail(function (e) {
                console.warn(e);
            });
            this.newTaskText("");
        }

        //To remove a task
        this.removeTask = function (id) {
            this.hub.remove(id);
        }

        //To update this task
        this.updateTask = function (task) {
            if (notify)
                this.hub.update(task);
        }
        //Gets the incomplete tasks
        this.incompleteTasks = ko.dependentObservable(function () {
            return ko.utils.arrayFilter(this.tasks(), function (task) { return !task.completed() });
        }, this);

    }

    var vm = new taskListViewModel();
    ko.applyBindings(vm);
    // Start the connection
    $.connection.hub.start(function () { vm.init(); });


});

Whenever a taskViewModel is created, the instance of taskListViewModel will be passed as its ownerViewModel, so that we can invoke the updateTask method of taskListViewModel whenever the current task’s properties change. In taskListViewModel, we also have methods like addTask, removeTask, etc., which are bound directly to our “View”.

We are creating a new instance of taskListViewModel, and then calling Knockout to do the job of applying bindings with the view. Have a look at the “View” part.

<div id="error" class="validation-summary-errors">
</div>
<h2> Add Task</h2>
<form data-bind="submit: addTask">
   <input data-bind="value: newTaskText" 
     class="ui-corner-all" placeholder="What needs to be done?" />
   <input class="ui-button" type="submit" value="Add Task" />
</form>

<h2>Our Tasks</h2>


You have <b data-bind="text: incompleteTasks().length"> </b> incomplete task(s)
<ul data-bind="template: { name: 'taskTemplate', foreach: tasks }, visible: tasks().length > 0">
</ul>

<script type="text/html" id="taskTemplate">
<!--Data Template-->
    <li  style="list-style-image: url('/images/task.png')">
        <input type="checkbox" data-bind="checked: completed" />
        <input class="ui-corner-all" data-bind="value: title, enable: !completed()" />
        <input class="ui-button" type="button" href="#" 
          data-bind="click: remove" value="x"></input>
    </li>
</script>

<span data-bind="visible: incompleteTasks().length == 0">All tasks are complete</span>

If you look below the Add Task header, you’ll see that we are binding the textbox’s value to the newTaskText property of our taskListViewModel, and the form submit to the addTask method in the taskListViewModel. The <ul> is bound to the Tasks property of the view model. If you look at it, Tasks property of taskListViewModel is a koObservableArray, which is almost like an ObservableCollection that notifies the bound controls whenever items are inserted/removed in the array.

Adding and Removing items

Have a look at the addTaskMethod in the taskListViewModel, you’ll see that we are creating a new task, and then invoking the ‘add’ method of the ‘hub’, which internally calls the TaskHub’s Add method in the server. In TaskHub’s Add method, you’ll see that we are broadcasting the added task to all the clients by invoking the taskAdded method in the client-side back – and there we are updating the items observable array so that Knockout will internally manage the rendering of a new <li> under the <ul> based on the data template tasktemplate (see the above view code where we have the tasktemplate).

Delete also works in the same way, you can see the ‘x’ button is bound to the remove method of each individual taskViewModel, which internally calls the taskListViewModel’s removeTask method to invoke the Remove method in TaskHub using the hub proxy, and from there, taskRemoved will be invoked on all clients, where we actually remove the item from the items collection.

Updating an item

Once an item is bound to the template, please note that we are subscribing to the change events of a task in taskViewModel. Whenever a property changes, we call the updateTask method in the ownerViewModel, which again calls hub’s update method which sends the task to our TaskHub’s update method – thanks to the wiring from SignalR. There, we try to save the item, and if everything goes well, the updated item will be broadcasted from TaskHub to all clients by invoking the taskUpdated JavaScript method we attached to the hub, where we actually update the properties of the item in all clients.

Conclusion

Surprise, we are done. Very minimal code, very little effort, great results. Thank you ASP.NET, SignalR, Entity Framework, and Knockout. And that is why I love .NET Smile. Happy coding, but follow me in Twitter @amazedsaint and subscribe to this blog.

You may also like these articles on a similar taste: Kibloc – Real time, distance based object tracking and counting using Kinect and 5 Awesome Learning Resources For Programmers (to help you and your kids to grow the geek neurons).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author


Comments and Discussions

 
Questionerror at startup: this.hub.taskAll not recognized Pinmemberdkonstantopoulos19-Feb-13 12:55 
AnswerRe: error at startup: this.hub.taskAll not recognized [modified] PinmemberMbunaFish28-Feb-13 3:15 
QuestionDownload PinmemberRaaj_00114-Nov-12 1:23 
GeneralMy vote of 5 Pinmembermacdaddy_o2-Nov-12 3:24 
QuestionSignalr whether to support the load balance PinmemberMember 780670426-Aug-12 17:54 
GeneralMy vote of 5 PinmemberMember 14599026-Aug-12 19:59 
GeneralNice one Pinmemberhackrogenius30-Jul-12 9:17 
GeneralMy vote of 5 PinmemberDinesh Mani24-Jul-12 9:03 
QuestionI think you forget about AutoMapper Nuget Pkg Pinmembercongox31-May-12 12:19 
QuestionEnable and disable of a textbox through a checkbox in asp.net mvc2 Pinmemberkansel29-Mar-12 0:38 
GeneralMy vote of 5 PinmemberRabinDl4-Feb-12 15:56 
QuestionVery Nice [modified] PinmemberMember 45654334-Feb-12 4:50 
QuestionOk Anoop I did it PinmvpSacha Barber4-Feb-12 0:59 
AnswerRe: Ok Anoop I did it PinmemberAnoop Madhusudanan5-Feb-12 4:03 
GeneralRe: Ok Anoop I did it PinmvpSacha Barber5-Feb-12 5:00 
GeneralMy vote of 2 Pinmembermagoicochea1-Feb-12 5:52 
GeneralRe: My vote of 2 PinmemberAnoop Madhusudanan2-Feb-12 19:23 
QuestionHa PinmvpSacha Barber31-Jan-12 3:55 
AnswerRe: Ha PinmemberAnoop Madhusudanan31-Jan-12 6:49 
GeneralRe: Ha PinmvpSacha Barber31-Jan-12 7:05 
GeneralRe: Ha PinmemberAnoop Madhusudanan31-Jan-12 7:10 
GeneralRe: Ha PinmvpSacha Barber31-Jan-12 8:35 
GeneralMy vote of 5 Pinmemberrmx9930-Jan-12 22:26 
GeneralRe: My vote of 5 PinmemberAnoop Madhusudanan31-Jan-12 19:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140415.2 | Last Updated 31 Jan 2012
Article Copyright 2012 by Anoop Madhusudanan
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid