Thursday, April 10, 2014

AngularJs + SignalR

Using SignalR in an AngularJs application...

Background

I recently had a situation at work where asynchronous functionality behind a web api necessitated bi-directional functionality between my angular client application and the web api. In a typical RESTful AngularJs application, I would create a service, using the $resource factory to set up some touch points. The problem with that approach in this scenario was - the server might respond n number of times, with n number of results.

***dilemma?***

Not exactly. I decided to use SignalR. It's there. It works. Decision indecision is time lost that could be spent working on something.

What is SignalR?

SignalR is an asp.net library that allows bi-directional operations between client applications and servers/hubs. (More info here)

SignalR is useful in situations like mine, when a server needs to send real time data to connected clients. Other examples of use cases, for instance, might include alerting connected client applications that someone has joined an IRC, or a stock price has changed, or perhaps a new message from a friend has been sent, etc. In addition to bi-directional communication in web applications, SignalR also has .net client libraries for windows and Xamarin.

This article, however, is about wiring up an AngularJs client application. So, on to angular...

What is AngularJs?

AngularJs is a superheroic js framework that truly makes developing web applications a very tenable process, if not a more manageable one. (More info here)

Angular's homepage sums it up best in their Why AngularJS? paragraph:

HTML is great for declaring static documents, but it falters when we try to use it for declaring dynamic views in web-applications. AngularJS lets you extend HTML vocabulary for your application. The resulting environment is extraordinarily expressive, readable, and quick to develop.

Enough of the formalities. On to the goodness...

Setting up the Hub

I am going to assume that you have read this article (or another like it) and have some familiarity setting up SignalR in your application.

Here is my hub to start:

public class GoodnessHub : Hub
{
    public void SayHello()
    {
        Clients.All.sayHello("Ooey, shuga!");
    }
}

Setting up the Html

Here is my html to start:

<!DOCTYPE html>
<html>
   <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width" />
       <title>SignalR Demo</title>

       <script src="~/Scripts/jquery-1.8.2.js"></script>
       <script src="~/Scripts/angular.js"></script>
    
       <script src="~/Scripts/jquery.signalR-2.0.3.js"></script>
       <script src="~/signalr/hubs"></script>
    
       <script src="~/app/signalrDemo.js"></script>
   </head>
   <body ng-app="signalrDemo">
       <div ng-controller="myClientGoodness as goody">
           <span ng-bind="goody.hello"></span>
       </div>
   </body>
</html>

Setting up the AngularJs App

Here is my js to start:

var app = angular.module('signalrDemo', []);
app.controller('myClientGoodness', ['$scope', function ($scope) {
        var goody = this;
        this.hello = 'hi world';
    }
]);

When we visit the page, we should see: hi world

Fantastic! Now that we know our angular app is working, let's wire up a call to the hub.
First, add a button to the html with an ng-click that calls goody.getHubHello(), like this:

<!DOCTYPE html>
<html>
   <head>
       <!--omitted for brevity-->
   </head>
   <body ng-app="signalrDemo">
       <div ng-controller="myClientGoodness as goody">
           <span ng-bind="goody.hello"></span>
           <button ng-click="goody.getHubHello()">Get Hub Hello</button>
       </div>
   </body>
</html>

Don't forget to add the getHubHello() function to your controller:

var app = angular.module('signalrDemo', []);

app.controller('myClientGoodness', ['$scope','$window', function ($scope, $window) {
        var goody = this;
        this.hello = 'hi world';

        this.getHubHello = function () {

           //instance a hub connection-->
           var hub = $window.jQuery.hubConnection();

           //instance a proxy from hub connection-->
           var proxy = hub.createHubProxy('GoodnessHub');

           //set callback on proxy-->
           //function invoked when server calls the client-->
           proxy.on('sayHello', function (hello) {
               goody.hello = hello;
           });

           //start hub with success/fail hooks-->
           hub.start(function () { /*hub started*/ })
            .done(function () {
                proxy
                  .invoke('sayHello')
                  .done(function () { /* proxy invoke done/successful */ })
                  .fail(function (failure) { /* proxy invoke failure */ });
            })
            .fail(function (failure) { /* hub start failure */ });
       };
    }
]);

There you go. That's it, right? Now when you click the AngularJs client starts the hub connection and calls the server method. The server method, in turn, then calls the AngularJs client via the proxy function we've configured, which will set goody.hello to whatever the server gives it.

At this point, if you've run the example, you may be wondering why the view didn't update with the server value. The answer is: because we subverted our angular application with our signalR $window.jQuery.hubConnection() stuff, our angular application doesn't know anything happened to goody.hello. AngularJs !== magic.

Angular applications work via watchers that keep track of things. When something happens that warrants a digest, angular queues up a digest, which will evaluate all watchers. Watchers are the things behind the scenes that angular is using to keep tabs on what it should update and how it should update it.

There are valid use cases when you must subvert your angular application to get things to work. If you're new to angular, you are likely seeing many more of these kinds of cases than you should. AngularJs has a mechanism built into it for these kinds of circumstances; the $apply() function. The $apply() function basically rolls behavior back into angular's digest. $digest() is also an AngularJs function, and it works very similarly to $apply(); however, $digest() is better avoided. In our case, $apply() is more appropriate anyhow. So let's add it to the getHubHello() function and get our view/model updating properly.

var app = angular.module('signalrDemo', []);
app.controller('myClientGoodness', ['$scope','$window', function ($scope, $window) {
        var goody = this;
        this.hello = 'hi world';
        this.getHubHello = function () {

           //instance a hub connection-->
           var hub = $window.jQuery.hubConnection();

           //instance a proxy from hub connection-->
           var proxy = hub.createHubProxy('GoodnessHub');

           //set callback on proxy-->
           //function invoked when server calls the client-->
           proxy.on('sayHello', function (hello) {
               $scope.$apply(function () { //<--APPLY!
                   goody.hello = hello;
               });
           });

           //start hub with success/fail hooks-->
           hub.start(function () { /*hub started*/ })
            .done(function () {
                proxy
                  .invoke('sayHello')
                  .done(function () { /* proxy invoke done/successful */ })
                  .fail(function (failure) { /* proxy invoke failure */ });
            })
            .fail(function (failure) { /* hub start failure */ });
       };
    }
]);

BOOM! Now we're cooking with fire! Now the model is updating when it hits the server... great... While our sayHello function seems pretty synchronous, it's not synchronous. If the server were to respond n more times with different things, our app would still work correctly to display that info. Check it out...

Hop back into your server hub and make it look like this:

   public class GoodnessHub : Hub
   {
        private static readonly string[] Goodnesses =
        {
            "buttery", "chocolatey", "salty", "sexy", "BIG sexy", 
            "cold steel", "cobalt blue", "rapid fire", 
            "random act of", "Gandalfian", "Millenium Falcon",
            "optional", "syntactic", "sugary sweet", "jolly grand",
            "hypertexty", "malnourished domain modely (aka anemic)"
        };

        public void SayHello()
        {
            Clients.All.sayHello("Ooey, shuga!");
        }

        //this is a crummy hack that fires a client proxy over and over-->
        public void GetGoodness(string goodnessKind = null)
        {
            const int upperbound = 100;
            var currentCount = 0;
            while (currentCount++ < upperbound)
            {
                var kindOfGoodness = string.IsNullOrEmpty(goodnessKind) 
                        ? GetRandomGoodness() 
                        : goodnessKind;
                Clients.All.getGoodness(kindOfGoodness + " goodness");
                Thread.Sleep(1500);
            }
        }

        private string GetRandomGoodness()
        {
            var r = new Random();
            var indx = r.Next(0, Goodnesses.Length);
            return Goodnesses[indx];
        }
   }

Then hop over to your html and add another input, button, and label, like this:

<!DOCTYPE html>
<html>
   <head>
       <!--omitted for brevity-->
   </head>
   <body ng-app="signalrDemo">
       <div ng-controller="myClientGoodness as goody">
           <span ng-bind="goody.hello"></span>
           <button ng-click="goody.getHubHello()">Get Hub Hello</button>
           <br /><br />
           <input ng-model="goody.overrideKind"/>
           <button ng-click="goody.getTrulyAsyncGoodness()">Get Goodness</button>
           <div ng-bind="'current goodness: ' + goody.kind"></div>
       </div>
   </body>
</html>

And then finally, rip the guts out of our getHubHello function (slightly modifying it, of course) and put into a new function called getTrulyAsyncGoodness. It should look like something like this:

var app = angular.module('signalrDemo', []);
app.controller('myClientGoodness', ['$scope','$window', function ($scope, $window) {
        var goody = this;
        this.hello = 'hi world';
        this.overrideKind = '';
        this.kind = '';

        this.getHubHello = function () {
           //omitted for brevity-->
        };

        this.getTrulyAsyncGoodness = function() {
           //instance a hub connection-->
           var hub = $window.jQuery.hubConnection();

           //instance a proxy from hub connection-->
           var proxy = hub.createHubProxy('GoodnessHub');

           //set callback on proxy-->
           //function invoked when server calls the client-->
           proxy.on('getGoodness', function (goodnessKind) {
               $scope.$apply(function () {
                   goody.kind = goodnessKind;
               });
           });

           //start hub with success/fail hooks-->
           hub.start(function () { /*hub started*/ })
            .done(function () {
                proxy
                  .invoke('getGoodness', goody.overrideKind)
                  .done(function () { /* proxy invoke done/successful */ })
                  .fail(function (failure) { /* proxy invoke failure */ });
            })
            .fail(function (failure) { /* hub start failure */ });
    };
}]);

After you rebuild, re-run it and click both buttons, it should resemble something like this:


Next article: AngularJs factory connections to SignalR hub resources.

1 comment: