CodeSnips

Thursday, September 5, 2013

Doing SignalR the AngularJS Way

I wanted to see how to use SignalR and AngularJS together, and worked up this example as an exercise. All my javascript code needs to be in a controller and I probably also want to encapsulate the SignalR stuff behind a service facade.

I'm assuming in this article you have some exposure to AngularJS and SignalR, so I won't get into the nitty gritty details on how to wire everything up in Visual Studio. If you are interested in the full example code, you can find it here on GitHub.

I will be interacting with this trivial SignalR Hub class:
public class NotesHub : Hub
{
    public void AddNote(string note)
    {
        Clients.All.noteAdded(note);
    }
}
This class exposes an 'AddNote' method and sends a 'noteAdded' message.

Now for the AngularJS code. I make sure that I include the jQuery module as part of my module setup, because I will be injecting that as a dependency to my service.
var mainApp = angular.module('mainApp', ['ui.bootstrap']);
mainApp.value('$', $);


now for the service code. Explanation follows below:
mainApp.factory('noteService', ['$','$rootScope',
 function($,$rootScope) {
  var proxy;
  var connection;
  return {
    connect: function () {
      connection = $.hubConnection();
      proxy = connection.createHubProxy('notesHub');
      connection.start();
      proxy.on('noteAdded', function (note) {
          $rootScope.$broadcast('noteAdded', note);
      });
    },
    isConnecting: function () {
       return connection.state === 0;
    },
    isConnected: function () {
       return connection.state === 1;
    },
    connectionState: function () {
       return connection.state;
       },
    addNote: function (note) {
       proxy.invoke('addNote', note);
    },
 }
}]);

First of all, you'll notice I am generating the proxy (line 8) for the SignalR hub explicitly. This is necessary to take control of when the proxy is created, and it replaces the need to have a <script src="signalr/hubs" type="text/javascript"></script> line on your main html page (or main layout page).
After starting the connection, (line 9), I setup an event handler on the 'noteAdded' message that the SignalR hub will emit. I use AngularJS $rootScope.$broadcast service to relay the event to any other controllers and/or services that may be interested in the event.
The service exposes 'addNote' as a facade over the proxy.invoke call for that method on the Hub class. The other functions exposed are convenience methods mainly so I can do some basic testing of this service (more on that later).

So, my controller ends up looking like this. Explanation of code follows:
mainApp.controller('mainController', 
function mainController ($scope, noteService) {
    noteService.connect();

    $scope.notes = [];

    $scope.$on('noteAdded', function (event, note) {
        $scope.notes.push(note);
        $scope.$apply();
    });

    $scope.addNote = function (note) {
        noteService.addNote(note);
        $scope.note = '';
    };
});

The noteworthy code here is in the $scope.$on handler for the 'noteAdded' event. Since we are handling a custom event, we need to explicitly call $scope.$apply() to insure any changes we've made to the scope are communicated through the DOM.
A word about testing
I use jasmine.js as well as Karma for my client-side Javascript testing. For my service test, I mainly wanted to make sure the various pieces needed to establish a connection were in place.
So, here's the example jasmine test:
/// <reference path="../apps/main/noteService.js" /&gt
/// <reference path="../jasmine.js" /&gt
'use strict';

describe('noteService Tests', function () {
    var noteSvc;

    beforeEach(module('mainApp'));
    beforeEach(inject(function (noteService) {
        noteSvc = noteService;
        noteSvc.connect();
    }));

    it('should attempt connection',function () {
        expect(noteSvc.isConnecting()).toBe(true);
    });

})

2 comments:

  1. We'll done example and explanation.

    ReplyDelete
  2. Hi Mike,

    Thanks for this nice tutorial. I would just like to add though that proxy.on should be called before connection.start(). I couldn't get it to work otherwise on my MVC4 application using signalr version 1.2.1

    ReplyDelete