Using SignalR and knockout js to push data in realtime to the browser

One of the most exciting developments in web technology of the past year is, in my opinion, the development of cross-browser javascript socket libraries and the addition of web sockets as part of the HTML5 standard. Although push communication has been available for a long time, it was hard to implement, used proprietary technologies and was hindered by performance and scalability problems. But libraries like socket.io rel="noopener noreferrer" and services like Pusher have changed this and have made implementing two-way communications on the web very simple. I feel that we are just at the beginning of a new generation of exciting web applications.

For our cloud auction project, we want to use web sockets to send price, availability and product updates from the server to all active clients. It’s important that these updates are synchronized as much as possible, so that every user sees the same price at the same time. We want to avoid users hammering our servers by refreshing the page in their browser, pressing F5 every second. We are starting from our ASP.NET MVC cloud auction website, a standard website based on the MVC Internet Website rel="noopener noreferrer" template. I described several parts of it in other posts in this series.

This is how it will work:

SignalR-Knockout-Auction


We will be rel="noopener noreferrer" using the Microsoft web socket library SignalR. SignalR is developed as an open source project on Github. SignalR rel="noopener noreferrer" has support for javascript web clients, but also for .NET clients and even (unsupported) Mono and Xamarinclients, which is important because we will be developing native mobile clients using Xamarin.iOS and Xamarin.Android. A SignalR service can be hosted inside an ASP.NET website, but also in a service or stand-alone program. If scalability is needed, you can cluster multiple SignalR servers into a single service using Azure Service Bus, SQL Server or Redis as the message bus. SignalR supports several client transport protocols: it prefers native Web Socket, but falls back to other transports such as Server-Sent Events, Long Polling or Forever Frame if Web Socket transport is not available. Depending on the browser and OS, it automatically selects the best transport. On the client, Internet Explorer supports web sockets starting with version 10. Chrome, Firefox, Safari and other current browsers all support it.

If you want to test “real” Web Sockets on your development computer, you will need Windows 8 or Server 2012. On Windows Azure, it is pre-configured but on Windows it is not enabled by default.

Installing websocket support on Windows 8 / Windows 2012:

- Turn Windows features on or off
- Internet Information Services
- World Wide Web Services
- Application Development Features
- WebSocket Protocol

Then, in web.config, under appSettings, add this setting:

<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />

SignalR will now automatically connect using web sockets. Programmatically, there is no difference at all between any of the transport protocols.

We will begin by installing SignalR into our ASP.NET website. Go to the package manager console, and type:

PM> install-package Microsoft.AspNet.SignalR

And, while we are at it, also enable knockout; we’ll use it later

PM> install-package knockoutjs

This will download and install several NuGet packages into the project, and open a readme.txt that tells us what to do next. Let’s do just that.

The first instruction is to call RouteTable.Routes.MapHubs(). This is to register a route that clients can use to connect. The default route for this is ~/signalr. We won’t add this in Application_Start, we will do it in App_Start\RouteConfig.cs were the other routes are registered. We will also slightly change the registration:

public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapHubs(new HubConfiguration { EnableCrossDomain = false, EnableDetailedErrors = true, EnableJavaScriptProxies = true }); // more routes... }

This registers the ~/signalr route, and does configuration for javascript clients. Having done that, we can create a Hub and start communicating between client side javascript and this hub. Our hub will be named AuctionHub, and we will use it to send price updates to all connected browser clients.

Now we can create a Hub. In the often repeated SignalR example of a chat application, the hub receives messages from the javascript client, and transmits that message to other users. In our Dutch auction scenario, we have another source for messages, which is the time that is passing. These timed events could a Worker Role application that also registers as a SignalR client. We first built it that way, but found having a separate computer (that should also be clustered) to run only a little business logic and send a message every 5 seconds is unnecessarily heavy. We refactored our domain logic to run inside the web application. I’ll write more on that later.

Now we will need to implement something called a Hub. A SignalR Hub is something like a Web API Controller, but for web socket connections. Incoming requests are handled much like Web API calls, the hub receives data via methods that are exposed to the clients. A Hub is different from a Web API in how it responds back. A Web API simply responds by returning data in a response to the caller, but SignalR makes it possible to also respond to other users that are connected to the Hub. A Hub manages connections that are persistent and makes it possible to send data to any client that is connected at any time.

When we develop communications to external clients, it is good to use a protocol with clearly defined messages. In our project, we created a separate project for those contracts called Auction.Service.Contracts. We do this because later, we will also create mobile apps for Windows Phone, iOS and Android (using Mono) and we can re-use the Service.Contracts project for those projects too.

image

This project defines all the messages that are sent between the server and the browser through SignalR and Web API. These are mostly simple POCO classes, that mimic our internal Domain Entities, but are designed to not expose internal information to the outside and can be easily consumed from javascript.

With this in place, we can create a Hub, and hook it up to some Domain logic. Here is the AuctionHub class. When the user opens the auction page, the page script will initialize a Hub Connection and call the Initialize hub method. That method will query the domain for the current product, if there is one it will respond by sending the product to the client. The AuctionHub subclasses from an abstract BaseHub class, were we added a few helper functions to interface with our domain logic. But that is for another post: the BaseHub subclasses from Hub, SignalR automatically exposes every public Hub that it finds (using reflection) and generates a javascript proxy on the registered ‘/signalr” route that we can then use to call our .NET methods from the browser.


using Auction.Service.Contracts;
using Auction.Web.Domain.Commands;
using Auction.Web.Domain.Entities;
using Auction.Web.Domain.Queries;
using Auction.Web.Utility;
using AutoMapper;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;

namespace Auction.Web.Hubs
{
[HubName("auctionHub")]
public class AuctionHub : BaseHub
{
public void Initialize()
{
// find the lot that is currently in auction
var lot = Query(new SingleActiveLot());
if (lot != null)
{
// Create client version of internal lot entity
ClientLot clientLot = Mapper.Map<Lot, ClientLot>(lot);
// calculate remaining time
TimeSpan remainingTime = lot.GetRemainingTime(DateTime.UtcNow);
clientLot.TimeRemaining = remainingTime.Ticks / TimeSpan.TicksPerMillisecond;
clientLot.ProgressRemaining = lot.CalculateProgressRemaining(remainingTime);
// respond by sending the lot back to the client
Clients.Caller.AuctionInitialized(clientLot);
}
else
{
// send dummy lot id 0 telling the user there is no auction active now
}
}

[Authorize]
public void PlaceBid(PlaceBidRequest placeBidRequest)
{
// Authorize attribute means we should have a User
Guid userId = UserUtility.GetCurrentUserId(Context.Request.GetHttpContext());

// Place the bid. If succesfull, this Command will Publish an Event that is subscribed by this hub and is sent all Clients
var bid = ExecuteCommand(new CreateNewBid(userId, placeBidRequest.LotId, placeBidRequest.Price));

// respond individually to the calling client
Clients.Caller.BidPlaced(new PlaceBidResult()
{
Lot = new ClientLot() { Id = placeBidRequest.LotId },
BidIsAccepted = bid.IsWinningBid,
ResultCode = bid.IsWinningBid ? PlaceBidResultCode.None : PlaceBidResultCode.NotAuthorized
});
}

[Authorize]
public void CancelBid(CancelBidRequest cancelBidRequest)
{
// ...
}

[Authorize]
public void FinalizeOrder(ClientOrder orderInfo)
{
// ...
}

public void ListProducts(int page)
{
// ...
}
}
}
view rel="noopener noreferrer" rawAuctionHub.cs hosted with ❤ by GitHub


So far so good. But this could just as well have been done with ASP.NET Web API. The difference is that we now have established a connection. A process in the background of the server will now send updates to every connected client, every 5 seconds. An event handler receives the domain events, maps the event message to a client message type in Service.Contracts, and use the HubContext to send the message to all clients. Here is that handler:


using Auction.Service.Contracts;
using Auction.Web.Domain;
using Auction.Web.Domain.Events;
using AutoMapper;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Infrastructure;

namespace Auction.Web.Hubs.EventHandlers
{
public class PriceUpdatedHandler : BaseEventHandler<PriceUpdatedEvent>
{
public override void Handle(PriceUpdatedEvent ev)
{
var priceUpdateMessage = Mapper.Map<ClientLotUpdate>(ev);
IConnectionManager connections = GlobalHost.DependencyResolver.GetService(typeof(IConnectionManager)) as IConnectionManager;
connections.GetHubContext<AuctionHub>().Clients.All.auctionUpdated(priceUpdateMessage);
}
}
}
view rawPriceUpdatedHandler.cs hosted with ❤ by GitHub


The clientside code consists of two parts: the Razor view Auction.cshtml and the page specific javascript file auction.js, that is included via a bundle ~/bundles/home/auction. I have included the full sourcecode, it is a bit long but it shows how to do this.


@{
ViewBag.Title = "SignalR Auction";
}

<div class="row-fluid"">
<div class="page-header">
<h1>
<strong data-bind="text: Title"></strong>
<small data-bind="text: Info"></small>
</h1>
</div>
</div>
<div class="row-fluid">
<div class="span4">
<div class="carousel slide">
<!-- Carousel items -->
<div class="carousel-inner" data-bind="foreach: Photos">
<div class="item" data-bind="css: { active: $index() == 0 }"><img src="/Content/images/none.png" data-bind=' attr: { src: $data }' /></div>
</div>
<!-- Carousel nav -->
<a data-bind="visible: Photos().length > 1" class="carousel-control left" href="#photos" data-slide="prev">&lsaquo;</a>
<a data-bind="visible: Photos().length > 1" class="carousel-control right" href="#photos" data-slide="next">&rsaquo;</a>
</div>
</div>
<div class="span8">
<div data-bind="html: Details"></div>
<h2 data-bind="text: CurrentPrice"></h2>
<p><span data-bind="text: TimeRemainingText"></span> remaining</p>
</div>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/ko-signalr")
<script type="text/javascript" src="~/signalr/hubs"></script>
@Scripts.Render("~/bundles/home/auction")
}
view rawAuction.cshtml hosted with ❤ by GitHub
/// <reference path="../jquery-1.9.1.js" />
/// <reference path="../knockout-2.2.0.js" />
/// <reference path="../moment.js" />

$(function () {

console.log("starting auction script");

function AuctionViewModel() {
var self = this;
self.Id = ko.observable(0);
self.AuctionTitle = ko.observable("Momenteel geen veiling actief");
self.Title = ko.observable("BCC veilingsite");
self.Info = ko.observable("Starten...");
self.Details = ko.observable();
self.RegularPrice = ko.observable();
self.CurrentPrice = ko.observable();
self.Available = ko.observable();
self.Photos = ko.observableArray();
self.TimeRemaining = ko.observable(0);
self.TimeRemainingText = ko.computed(function () {
return moment.duration(self.TimeRemaining()).humanize();
});
self.ProgressRemaining = ko.observable(100);
self.loading = ko.observable(true);
}

// declare the viewmodel
var vm = new AuctionViewModel();

// apply knockout bindings
ko.applyBindings(vm);

// start signalr and get current product
$.connection.hub.logging = true;
$.connection.hub.start()
.done(function () {
// Call the Initialize function on the server. Will respond with auctionInitialized message
$.connection.auctionHub.server.initialize();
})
.fail(function () {
console.log("Could not Connect!");
});

// Handle connection loss and reconnect in a robust way
var timeout = null;
var interval = 10000;
$.connection.hub.stateChanged(function (change) {
if (change.newState === $.signalR.connectionState.reconnecting) {
timeout = setTimeout(function () {
console.log('Server is unreachable, trying to reconnect...');
}, interval);
}
else if (timeout && change.newState === $.signalR.connectionState.connected) {
console.log('Server reconnected, reinitialize');
$.connection.auctionHub.initialize();
clearTimeout(timeout);
timeout = null;
}
});

$.connection.auctionHub.client.auctionInitialized = function (args) {
console.log("auctionInitialized called", args);

vm.Id(args.Id);
vm.AuctionTitle(args.AuctionTitle);
vm.Title(args.Title);
vm.Info(args.Info);
vm.Details(args.Details);
vm.RegularPrice(Globalize.format(args.RegularPrice, "c"));
vm.CurrentPrice(Globalize.format(args.CurrentPrice, "c"));
vm.Available(args.Available);

// need to stop carousel during data refresh: it can't handle it
$('.carousel').carousel('pause');
vm.Photos.removeAll();
vm.Photos.push(args.Photo1);
if (args.Photo2) { vm.Photos.push(args.Photo2); }
if (args.Photo3) { vm.Photos.push(args.Photo3); }
if (args.Photo4) { vm.Photos.push(args.Photo4); }
if (args.Photo5) { vm.Photos.push(args.Photo5); }
$('.carousel').carousel('cycle');

vm.TimeRemaining(args.TimeRemaining);
vm.ProgressRemaining(args.ProgressRemaining);

if (args.Id == 0) {
vm.loading(false);
}
}

$.connection.auctionHub.client.auctionUpdated = function (args) {
console.log("auctionUpdated called", args);

vm.CurrentPrice(Globalize.format(args.CurrentPrice, "c"));
vm.ProgressRemaining(args.ProgressRemaining);
vm.TimeRemaining(args.TimeRemaining);

vm.loading(false);
}

$.connection.auctionHub.client.availableUpdated = function (args) {
console.log("availableUpdate called : " + arguments[0]);
vm.Available(args.Available);
}


// start the bootstrap image carousel component
$('.carousel').carousel({
interval: 10000
});

console.log("finished page initialization!");
});
view rawauction.js hosted with ❤ by GitHub
// ....
bundles.Add(new ScriptBundle("~/bundles/ko-signalr").Include(
"~/Scripts/jquery.signalR-{version}.js",
"~/Scripts/knockout-{version}.js"));
//...
bundles.Add(new ScriptBundle("~/bundles/home/auction").Include(
"~/Scripts/site/auction.js"));
//...
view rawBundleConfig.cs hosted with ❤ by GitHub


The page uses knockout.js for databinding between the javascript viewmodel and the DOM. This combines wonderfully with SignalR: on every hub event, we just have to update the viewmodel and the page is automatically updated. This page shows various techniques: a simple string for the title, an array containing images mapped to a Bootstrap carousel using an ObservableArray and a TimeSpan field mapped to a moment.js duration object using a knockout computed function.

The resulting page looks like this:

image

That looks quite good already. And thanks to SignalR, Knockout and Bootstrap getting this advanced scenario setup was not hard at all, and we did it with relatively little code! Building this out to a real-world application is now just implementing more logic, repeating the same pattern of requests, queries, commands, and events between the .NET code running on the server and the javascript code running in the browser.

[This post is part of the series Development of a mobile website with apps and social features]