SignalR And Knockout In ASP.NET Web Pages Using WebMatrix

SignalR is a library that simplifies the creation and management of persistent connections between web servers and clients. This facilitates the development of applications that can display updates to data held on the server in real-time. Chat applications are the most obvious beneficiaries of this technology, but line-of-business applications that need to report availability to users can benefit too. Here, I look at extending the canonical SignalR chat example to incorporate a "who's typing" feature, and I also extend my previous Knockout example to use SignalR.

First, what is the problem that SignalR is designed to solve? The web works on a Request-Response model. Browsers and other user agents make requests and web server provide a response to that request.The response is sent to the delivery address provided in the request by the user agent. And that is the natural order of things on the Web - servers can't make responses without a request. For the most part, this is not an issue, but if you want to display real-time updates on your web page, you have needed to resort to techniques like repeatedly polling the server using AJAX to see if any changes had been made to data. Alternatively, you could use Comet technology, which keeps a persistent connection open between the server and the client. HTML5 introduced two new techniques - Server Sent Events and WebSockets. SignalR is a user-friendly wrapper around all these technologies that makes it a lot easier to create applications that require the real-time display of data. SignalR utilises HTML5 Web Sockets API where it is available, and falls back onto other technologies where they are not - Server Sent Events, Forever Frames or Long Polling, the last two of which are Comet techniques.

SignalR is currently in pre-release - Release Candidate 1 to be precise. It is available via Nuget or Github. However, since it is pre-release, it is not available via the WebMatrix Nuget client. But you can get it using Visual Studio (and Express For Web version, which is free) if you choose "Include Prerelease" instead of the default "Stable Only" option. Or you can use the Package Manager Console and simply type:

Install-Package Microsoft.AspNet.SignalR -Pre

It is recommended to install the package this way (via Nuget) as there are quite a few bits and pieces to be installed:

SignalR

The centre piece of a SignalR application is a hub, which is similar to a Controller in ASP.NET MVC. A hub is responsible for receiving input and generating output. Hubs are written as server-side classes that inherit from Microsoft.AspNet.SignalR.Hubs.Hub. Public methods created in a hub class are intended to be called from client code. They typically result in a response being sent to the client. Here's the hub and method that the Chat example at the ASP.NET web site features:

using Microsoft.AspNet.SignalR.Hubs;

public class ChatHub : Hub
{
    public void Send(string name, string message)
    {
        Clients.All.broadcastMessage(name, message);
    }
}

This should be saved as ChatHub.cs (a C# class file) in App_Code. A using directive is added to the top of the file which makes Microsoft.AspNet.SignalR.Hubs available to the code. At runtime, a JavaScript file is generated dynamically which contains a client-side implementation of the public Send method so that it can act as a proxy to the server-side method. You write an implementation of the broadcastMessage method in client-code so that the Hub method can call it and thereby provide a response. You can also determine who should receive the response through the options available:

SignalR

Here's the full client-side code starting with the Layout page:

@{
    
}

<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>@Page.Title</title>
        <link href="~/Styles/site.css" rel="stylesheet" type="text/css" />
        <script src="~/Scripts/jquery-1.6.4.min.js" ></script>
        <script src="~/Scripts/jquery.signalR-1.0.0-rc1.min.js"></script>
        <script src="~/signalr/hubs"></script>
        @RenderSection("script", required: false)
    </head>
    <body>
        @RenderBody()
    </body>
</html>

Notice that there is a reference to a script called hubs in a signalr folder, neither of which exist. This is a placeholder for the dynamically generated script that translates the Hub methods to client code.

@{

}

    <div class="container">
        <input type="text" id="message" />
        <input type="button" id="sendmessage" value="Send" />
        <input type="hidden" id="displayname" />
        <ul id="discussion"></ul>
    </div>

@section script{
    <script type="text/javascript">
        $(function () {
 
            var chat = $.connection.chatHub;

            chat.client.broadcastMessage = function (name, message) {
                $('#discussion').append('<li><strong>' + name
                    + '</strong>:&nbsp;&nbsp;' + message + '</li>');
            };

            
            $('#displayname').val(prompt('Enter your name:', '')); 
            $('#message').focus();
            $.connection.hub.start().done(function () {
                $('#sendmessage').click(function () {
                    var encodedName = $('<div />').text($('#displayname').val()).html();
                    var encodedMsg = $('<div />').text($('#message').val()).html();
                    chat.server.send(encodedName, encodedMsg);
                    $('#message').val('').focus();
                });
            });
        });
    </script>
}

Some HTML elements are created at the top of the file - a text box, button, a hidden field and an unordered list. In the script block, a client-side proxy is created for the server-side ChatHub class:

var chat = $.connection.chatHub;

The client-side version of the hub is generated using camel-case (first word lower case). Remember that you have to write your own client-side implementation of the dynamic broadcastMessage method? The next section shows that. It takes the two strings provided by the Hub method, and uses them to generate a list item that is added to the unordered list. The next couple of lines set up the user by prompting for a name which is stored in the hidden field, and setting the focus on the textbox. Then the connection to the hub is opened, and a click event handler is added to the button which results in the Send method in the server version of the hub being invoked:

chat.server.send(encodedName, encodedMsg);

So just to recap, the button click invokes the client-side chat.server.send method, which calls the public ChatHub.Send method, which responds with Clients.All.broadcastMessage which invokes the client-side chat.client.broadcastMessage function.

There is one final thing needed to get this working, asn that is to register the default route for hubs, which is best done in an _AppStart.cshtml file:

@using System.Web.Routing
@using Microsoft.AspNet.SignalR

@{
    RouteTable.Routes.MapHubs();
}

The chat example is easy to understand and illustrates the basic workflow nicely. But it is not a complete application by any stretch of the imagination. It needs more, and a common feature of chat applications is an indicator of who is currently typing. We can use the workflow logic above to plan an implementation.

Something on the client needs to invoke a server-side method. Keystrokes are as good an indication of typing as any, so that will do:

$('#message').keypress(function () {
    var encodedName = $('<div />').text($('#displayname').val()).html();
    chat.server.isTyping(encodedName);
});

From the definition of the client method, it should be easy to discern the name and signature of the additional method required in the ChatHub class:

public void IsTyping(string name){
    Clients.All.sayWhoIsTyping(name);
}

With each keypress, the name of the current user is passed to the server-side IsTyping method, which responds by calling a client-side function called sayWhoIsTyping:

chat.client.sayWhoIsTyping = function (name) {
    $('#isTyping').html('<em>' + name + ' is typing</em>');
    setTimeout(function () { 
        $('#isTyping').html('&nbsp;');
    }, 3000);
};

It's a rough and ready function, which takes the name passed from the server-side method, and displays it as part of a string in a div with an ID of isTyping. The text is cleared after 3 seconds.

SignalR with Knockout

I have written about Knockout before. If you are not sure what Knockout is all about, you should read the previous article first. In that previous article, the example shows a simple application where the user can select a products, and from the resulting list, select one product to view the details. There is a simple button that results in the ViewModel's unitsInStock value decreasing by one as if a purchase had been made, and the UI being updated as a result. And that's one of the benefits of Knockout - changes to observable values in the ViewModel result in that change being reflected wherever it is bound in the UI. But the change is localised to the individual user's browser. If a product unit is actually purchased, or indeed if more are made available, it would be a really good idea if ALL current users are notified of the change in unitsInStock in real-time, right? Sounds like a task for SignalR.

The number of changes needed to the existing Knockout sample code are actually quite small. I amended the existing ViewModel buy function and added another function to manage reordering products. I also added two computed observables (they used to be called dependent observables); one that works out whether the user can buy a product based on availability, and the other that works out whether the reorder level has been reached:

viewModel.buy = function() {
    var id = this.productId();
    productsHub.server.buyProduct(id);
};

viewModel.reorder = function() {
    var quantity = this.reorderQuantity();
    var id = this.productId();
    productsHub.server.reorderProduct(quantity, id);
};

viewModel.canBuy = ko.computed(function() {
    return this.unitsInStock() > 0;
}, viewModel);

viewModel.canReorder = ko.computed(function() {
    return this.unitsInStock() <= this.reorderLevel();
}, viewModel);

I also added a reorderLevel property to the ViewModel, and then added a textbox and button for reordering, and wired up its visibility to the canReorder computed observable. The reordering button and textbox will only be visible if the units in stock reaches the reorder level value. The Buy One button is now only enabled if there are units in stock. Previously, when there were no units in stock, the result of clicking the Buy One was an alert indicating that no stock existed:

<div class="row" data-bind="visible: canReorder">
    <span class="label"><input data-bind="value: reorderQuantity" class="reorder" /></span>
    <span><button data-bind="click: reorder">Reorder</button></span>
</div>
<div class="row">
    <span class="label">&nbsp;</span>
    <span><button data-bind="click: buy, enable: canBuy">Buy One</button></span>
</div>

If you have been following along so far, you can see that the two ViewModel functions: buy and reorder point to server-side Hub methods that don't currently exist: BuyProduct (indicated by productsHub.server.buyProduct) and ReorderProduct (indicated by productsHub.server.reorderProduct). So here is the code for the ProductHub with its methods:

using Microsoft.AspNet.SignalR.Hubs;
using WebMatrix.Data;

public class ProductHub : Hub
{
    public void BuyProduct(int productId) {
        using (var db = Database.Open("Northwind")) {
            var unitsInStock = db.QueryValue("SELECT UnitsInStock FROM Products WHERE ProductId = @0", productId);
            if (unitsInStock > 0) {
                db.Execute("UPDATE Products Set UnitsInStock = UnitsInStock - 1 WHERE ProductId = @0", productId);
                unitsInStock -= 1;
            }
            Clients.All.updateAvailableStock(unitsInStock, productId);
        }
    }

    public void ReorderProduct(int quantity, int productId){
        using (var db = Database.Open("Northwind")) {
            db.Execute("UPDATE Products Set UnitsInStock = UnitsInStock + @0 WHERE ProductId = @1", quantity, productId);
            var unitsInStock = db.QueryValue("SELECT UnitsInStock FROM Products WHERE ProductId = @0", productId);
            Clients.All.updateAvailableStock(unitsInStock, productId);
        }
    }
}

This time, the changes caused by user action will be stored in the database, which didn't happen in the previous Chat example. The BuyProduct method retrieves the current number of units in stock for the selected product, and permits a purchase if there are are any. The revised number of units in stock, along with the product ID are sent to the all currently connected clients via an updateAvailableStock method. The ReorderProduct method simply increases the UnitsInStock value by whatever quantity was passed in to the method by the ViewModel function. Again, the revised number of units in stock is broadcast to all connected clients.

So how does the client handle this broadcast? It needs an updateAvailableStock function so that the hub methods can call it:

productHub.client.updateAvailableStock = function(unitsInStock, productId) {
    if (viewModel.productId() == productId) {
        viewModel.unitsInStock(unitsInStock);
        $('span[data-bind="text: unitsInStock"]').effect('highlight', { color: '#9ec6e2' }, 3500);
    }
};

First is checks to see if the ID of the product being updated is the one currently bound to the ViewModel, and if it is, the value is updated and a highlight effect is applied to the span that displays the units in stock:

SignalR

All that remains to do is to create a client-side proxy for the ProductHub and establish a connection:

productsHub = $.connection.productHub;

$.connection.hub.start();

Both of these examples are available as part of a sample site available at GitHub.

SignalR is a lot more powerful than these introductory demonstrations illustrate. You can write your own PipelineModule class and inject your own processing code into the hub pipeline. You can add authorization really easily to a hub method or a complete hub using data annotiation style attributes. You can read more about the latest SignalR release here.