qc

AD

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

Saturday, 11 May 2013

SignalR, built by David Fowler (@davidfowl) and Damian Edwards (@DamianEdwards), is an async signaling library for ASP.NET to help build real-time, multi-user interactive web applications. Adding it to an ASP.NET MVC 3 web application is ridiculously easy. Mashing it up with a client side library like Knockout can be downright magical! Wondering how to get started using SignalR with MVC? How about we new up some sample code and dive in to see what all this "real-time" fuss is about?
I posted about using Knockout to handle real-time UX updates in an ASP.NET MVC 3 application back in July of 2011. It covered creating a product page in which the stock counts for a product would update over time without refreshing the page. The Knockout code handled the display updates to the DOM like a champ, but the "real-time" update calls were handled with some pseudo-polling using the JavaScript setTimeout method. We can build a similar sample to that one, but this time we can implement SignalR for the count updates.
Our sample application this time around will be an Event Ticket page that allows a visitor to buy a ticket with the click of a button. The page will display the current available ticket count, show a message when the stock level of the tickets is getting low, and will disable the ability to buy a ticket when the tickets are out of stock.

Packages to Install

First we need to install the SignalR NuGet package into an ASP.NET MVC 3 project.
Install-Package SignalR
Out of the box, SignalR does not support versions of Internet Explorer prior to 8. There is an additional package, JSON2, that can be added to provide this support. We will add this to our sample project.
Install-Package Json2
Finally, we need to add the Knockout package:
Install-Package knockoutjs

Scripts to Reference

In our _Layout.cshtml file we need to reference jQuery, the Json2 script, the SignalR script and the Knockout script.
<script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/json2.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.signalr.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout-1.3.0beta.js")" type="text/javascript"></script>
Note the order here. The Json2, SignalR and Knockout libraries require the jQuery library. In addition, the SignalR library requires Json2 to be referenced before it to include the pre IE 8 support.

Implementing a SignalR Hub

The Hub logic in SignalR allows us to broadcast and interact with multiple connected clients (for more information see the official documentation). Our event ticket sample needs to allow clients to click and buy a ticket. It also needs to keep all clients informed of the current available ticket count. We will create a ticket hub that will inherit from the SignalR.Hubs.Hub class. The hub will contain a method named GetTicketCount for requesting the current ticket count and a method named BuyTicket to buy a ticket. For the sake of this example, we will create a static field to store the total ticket count.
using SignalR.Hubs;
namespace Mvc3SignalR.Models
{
    public class TicketHub : Hub
    {
        static int TotalTickets = 10;

        public void GetTicketCount()
        {
            Clients.updateTicketCount(TotalTickets);
        }

        public void BuyTicket()
        {
            if(TotalTickets > 0)
                TotalTickets -= 1;
            Clients.updateTicketCount(TotalTickets);
        }
    }
}
The SignalR.Hubs.Hub class has a public property named Clients for accessing the connected clients. Both of our methods will call a dynamic method that we have created off of the Clients property called updateTicketCount. This method takes in the current ticket count. When the hub executes this method it is essentially telling the clients that this method was called with the TotalTickets value.

The View from the Client

We will run our event tickets sample page from a HomeController with an Index action method and a corresponding Index.cshtml view file. The markup for the display in the Index.cshtml file:
@{ ViewBag.Title = "Ticket Sales"; }
<h1>Event Tickets</h1>
<div id="TicketSummary">
    <div data-bind="text:Available, css:{ ImportantMessage: StockIsLow }" class="TicketCount"></div>
    <div class="Hidden ImportantMessage" data-bind="css: { Hidden: !StockIsLow() }">
        Event is almost sold out!</div>
    <button data-bind="click: BuyTicket, enable: CanBuyTicket">Buy a Ticket</button>
</div>
To learn the details behind using Knockout, read through the July post.
The JavaScript consisting of Knockout code and SignalR code in the same Index.cshtml file:
<script type="text/javascript" src="/signalr/hubs"></script>
<script type="text/javascript">
    var ticketDataViewModel = {
        Available: ko.observable(),
        BuyTicket: function () { tickets.buyTicket(); }
    };

    ticketDataViewModel.StockIsLow = ko.dependentObservable(function () {
        return this.Available() < 5 && this.Available() > 0;
    }, ticketDataViewModel);

    ticketDataViewModel.CanBuyTicket = ko.dependentObservable(function () {
        return this.Available() > 0;
    }, ticketDataViewModel);

    ko.applyBindings(ticketDataViewModel, $("#TicketSummary")[0]);

    var tickets;
    $(function () {
        tickets = $.connection.ticketHub;
        tickets.updateTicketCount = function (data) { ticketDataViewModel.Available(data); };
        $.connection.hub.start(function () { tickets.getTicketCount(); });
    });
</script>
The first script tag references the route /signalr/hubs that is created by the SignalR framework to expose accessibility to the hubs. Note that the usage of the hubs does not require any additions to the routing table (for example, in the Global.asax.cs file).
The ticketDataViewModel is our Knockout view model. It has an observable for the Available ticket count and a function for handling theBuyTicket event. We also add 2 dependent observable methods to handle a check on the ticket count getting low and the ability to buy a ticket. With those declared we can apply the Knockout view model to the TicketSummary element block.
Next up is the SignalR JavaScript code. We declare a variable named tickets outside of the jQuery document ready block so that it is available to theticketDataViewModel.BuyTicket method. Then within the document ready block we instantiate a connection to the TicketHub. A method is created for handling the calls from the server to the updateTicketCount method. Our client JavaScript function receives the data (in this case thedata parameter contains the total ticket count available) and sets the Knockout ticketDataViewModel.Available observable. When the server hub broadcasts a call to the updateTicketCount method, all clients connected to the hub will receive the broadcast and update the observable, which triggers Knockout to update the display.
The final bit of JavaScript on the client is the call to start the hub connection. This method can take in a function to execute when the connection is established. We need to update the display with the current ticket availability count as soon as a new client connects. This function calls thegetTicketCount method in the TicketHub. Since that method on the server broadcasts out a call to the updateTicketCount method on each connected client, the tickets.updateTicketCount function we declared on the client will get called (which leads to our Knockout observable update and thus our display update).

Buy Buy Buy!

With everything wired up, we can F5 the project and start buying some tickets. Since we are trying to illustrate and learn about the power of SignalR it is imperative, dare I say of the utmost importance, that we fire up at least one other instance of a browser and navigate to the same URL to fully experience the magic.
Note that this isn't a Session type architecture, so feel free to ignore that ingrained reaction to launch a different user agent (Chrome, FireFox, etc). Go nuts and copy the url into another tab in the same browser instance that was launched when you ran the debugging.
Start buying tickets in each browser tab/instance and watch the other update in real-time! Spam the "buy a ticket" button and watch the availability count drop to oh noes levels. Purchase the last remaining tickets with the browser on your right and watch all connected browser clients have their "Buy a Ticket" button become disabled at the same time.
Now get out there and start implementing all of those potential ideas cultivating in your mind, because this stuff is a blast to work with!

No comments:

Post a Comment