Wednesday 11 September 2013

A Better Way of Using ASP.NET SignalR With Angular JS

A few days back, I blogged on using SignalR and Angular JS together and on Implementing SignalR stock ticker sample using Angular JS(Part 1 and Part 2). In those posts, I have used the traditional call-back model to call the functions defined in controller to modify data whenever an update is received from the server.

One of the readers sent me feedback saying that we have a better way to use SignalR and Angular JS together. The way to go is using event methods defined on $rootscope object. This approach is based on publishing and subscribing events. As events can be published from anywhere and subscribed from anywhere, the source and destination will remain completely unaware of each other. Both of them have to depend on just one object, $rootScope.

Official documentation on scope contains details on each method defined on $rootScope. We will be using the following methods for publishing and subscribing the events:

  • $emit(name, args): Publishes an event with specified name with given arguments
  • $on(name, listener): Subscribes to an event with specified name. Listener is a function containing logic to be executed once the event has occurred

To manage SignalR’s client functionality, it is better to create a service, as services are singletons. There will be only one instance of the service in entire application. This behaviour of services makes it possible to have multiple SignalR client pages in the applications and they can be kept in sync without putting any extra amount of effort.

Let’s modify the example discussed in the post titled Hooking up ASP.NET SignalR with Angular JS to use event model. Server hub, references and structure of the HTML page remains the same as past. The only components to be modified are Controller and Service.

Service carries the responsibility to initialize a connection to the hub and call the SignalR’s server methods. Once a response is received from the server, we will broadcast an event from the service with data received.


app.service('signalRSvc', function ($, $rootScope) {
    var proxy = null;
 
    var initialize = function () {
        //Getting the connection object
        connection = $.hubConnection();
 
        //Creating proxy
        this.proxy = connection.createHubProxy('helloWorldHub');
 
        //Starting connection
        connection.start();
 
        //Publishing an event when server pushes a greeting message
        this.proxy.on('acceptGreet', function (message) {
            $rootScope.$emit("acceptGreet",message);
        });
    };
 
    var sendRequest = function () {
        //Invoking greetAll method defined in hub
        this.proxy.invoke('greetAll');
    };
 
    return {
        initialize: initialize,
        sendRequest: sendRequest
    }; 
});


To keep the things simple, I kept names of the server hub event and event rose using $emit the same. The names can be different. Let’s modify the controller to have a listener to the event raised by the service. Following is the implementation of the controller:

function SignalRAngularCtrl($scope, signalRSvc, $rootScope) {
    $scope.text = "";
 
    $scope.greetAll = function () {
        signalRSvc.sendRequest();
    }
 
    updateGreetingMessage = function (text) {
        $scope.text = text;
    }
 
    signalRSvc.initialize();
 
    //Updating greeting message after receiving a message through the event

    $scope.$parent.$on("acceptGreet", function (e,message) {
        $scope.$apply(function () {
            updateGreetingMessage(message)
        });
    });
}


Now open the modified page on multiple browsers and click the Greeting button randomly from all browsers. Messages printed on all browsers should be updated whenever the button is clicked. This behaviour is same as it was earlier. We just adopted a better approach to make it work.

Happy coding!

30 comments:

  1. hi Ravi,
    thanks for maintaining an excellent blog on angularjs. I am a newbie starting to learn angularjs. your topics shows advanced usage in angularjs. keep the good work coming.

    ReplyDelete
    Replies
    1. Thanks Anand. I will try to keep posting good content here.

      I also have a good number of posts for beginners, have a look at them as well :)

      Delete
  2. I would go a step out... simply have *your* service work with $emit and $on, then from there have the client do signalRSvc.on('eventName',listener) ... don't go to the root level with this... otherwise, what's the point of subscribing to your service in the ui handler.

    ReplyDelete
    Replies
    1. Do you mean, you will handle both $emit and $on in the service itself and pass a callback function to be called when an event occurs?

      Delete
  3. Example with Log4Net.SignalR Appender:
    app.factory('$signalR', ['$rootScope', function ($rootScope) {
    var self = $rootScope.$new();

    //Log4Net.SignalR
    var log4Net = $.connection.signalrAppenderHub;
    log4Net.client.onLoggedEvent = function (loggedEvent) {
    self.$emit('loggedEvent', loggedEvent);
    };


    $.connection.hub.start();
    return self;
    }]);

    And in the controller:
    app.controller('LogsCtrl', ['$scope', '$signalR',
    function ($scope, $signalR) {

    $signalR.$on('loggedEvent', function (e, loggedEvent) {
    //do something here...
    });

    }]);

    ReplyDelete
    Replies
    1. Thanks Juan. This approach makes the things cleaner!

      Delete
  4. As an addition to that: with this approach there is a possibility of collision with names. In example name 'aceptGreet' can already emitted in some other module for different purpose.

    To solve that how about adding 2 additional methods in service:
    var prefix = "myGreatPrefix."
    var $emit = function (name, args) {
    $rootScope.$apply(function () {
    $rootScope.$emit(prefix + name, args);
    });
    };

    var $on = function (name, listener) {
    $rootScope.$on(prefix + name, listener);
    };



    In that case you would use call to service

    sigalRSvc.$on("greeting", function (e, message) {
    console.debug(message);
    });

    ReplyDelete
    Replies
    1. How about creating an open source project where we can include all of these approaches? Anyone with a new idea can send a pull request and contribute code to it.

      Delete
  5. Could you post the server code or whole project?

    ReplyDelete
    Replies
    1. Austin,

      You can find the server code in the following post: http://sravi-kiran.blogspot.com/2013/05/HookingUpAspNetSignalRWithAngularJS.html

      You can find a more advanced sample in this github repo: https://github.com/sravikiran/SignalRAngularJSStockTicker

      If you still need any specific help, shoot me a mail using the contact me link at the top. I will be happy to help :)

      Delete
  6. Just a question and a comment. Did you use $emit instead of $broadcast so that the event would only be published on $rootScope?

    Also, wouldn't it be a little cleaner to use $rootScope.$on instead of scope.$parent.$on?

    Thanks for the post!

    ReplyDelete
    Replies
    1. Jason,

      Yes, I used $emit as it would be listened only through $rootScope. $broadcast bubbles the event to scopes at all levels, which would slow down in large scale.

      Thanks for the suggestion. I could use $rootScope.$on as well, scope.$parent is something you won't like to use in a real app. I used it to just make aware that a parent scope can be accessed using the $parent property as well.

      Delete
  7. Hi, you must define functions that the hub can call back befor connection.start().
    http://www.asp.net/signalr/overview/signalr-20/hubs-api/hubs-api-guide-javascript-client#cantusegenproxy

    ReplyDelete
    Replies
    1. Binh,

      I agree. That's a good point. It makes sense to have all client callbacks created before starting connection as the callbacks can be invoked at any point from the server.

      Delete
  8. This comment has been removed by the author.

    ReplyDelete
  9. Hi Ravi, very useful post but I can't make this work with angular 1.3.7. I am a total angular newb. Something seems to be stopping the controller from executing. Any ideas why?

    ReplyDelete
    Replies
    1. It's ok I got it working. I had to change the controller thus:

      app.controller('StockTickerCtrl', ['$scope','stockTickerData', function ($scope, stockTickerData) { ... }]);

      Delete
    2. Hi,

      Thanks for the update. Yes, Angular 1.3 doesn't allow global functions as controllers and that is the recommended way of creating a controller. I will update the post accordingly.

      Delete
  10. Hi Ravi,

    I had to make the following changes to get this to work for cross domain (PhoneGap/ Cordova). Hope this helps others...

    .factory('SignalRSvc', function ($, $rootScope, applicationConfig) {
    var proxy = null;
    var initialize = function(){
    $.getScript( applicationConfig.apiUrl + 'signalr/hubs', function () {
    $.connection.hub.url = applicationConfig.apiUrl + 'signalr';
    proxy = $.connection.signalRMessageServiceProviderHub;
    $.connection.hub.url = applicationConfig.apiUrl + 'signalr/hubs'

    proxy.on('acceptGreet', function (message) {
    $rootScope.$emit("acceptGreet",message);
    });

    $.connection.hub.start().done(function () {
    proxy.server.greetAll();
    });
    });

    };
    var sendRequest = function () {
    proxy.invoke('greetAll');
    };
    return {
    initialize: initialize,
    sendRequest: sendRequest
    };


    })

    ReplyDelete
  11. Replies
    1. Sure. I am working on a sample that will cover some of the latest updates in both technologies. Will post them in some time. Stay tuned.

      Delete
  12. Hi Ravi,

    Don't you need to declare your method before you start the connection ? I specifically remember that this was said during an intro to SignalR in the MS virtual academy.

    So the code should be in this order :

    //Publishing an event when server pushes a greeting message
    this.proxy.on('acceptGreet', function (message) {
    $rootScope.$emit("acceptGreet",message);
    });
    //Starting connection
    connection.start();

    I personally tried your code and it didn't work until i switched these parts.

    ReplyDelete
  13. I am using angularjs and Entity framework 6 with signalR. The issue I facing is, the sqldependency doesn't triggers on data change. Please let me know if i need to change anything else,

    ReplyDelete
  14. How to wirte jasmine unit test cases for signalr service?

    ReplyDelete
  15. For those who would like the complete html/js to make this sample work...this example requires the controller be added to the 'app' variable as the snippet in the article does not work as shown.

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
    <script src="Scripts/jquery.signalR-2.2.0.min.js"></script>
    </head>
    <body>
    <div ng-app="app" ng-controller="SignalRAngularCtrl">
    <input type="button" name="GreetAll" value="GreetAll" data-ng-click="greetAll()" />
    <span>{{text}}</span>
    </div>

    <script>
    var app = angular.module('app', []);
    app.value('$', $);
    app.service('signalRSvc', function ($, $rootScope) {
    proxy: null;
    var initialize = function () {
    connection = $.hubConnection();
    this.proxy = connection.createHubProxy('myHub');
    connection.start();
    this.proxy.on('acceptGreet', function (message) {
    $rootScope.$emit("acceptGreet", message);
    });
    };

    var sendRequest = function () {
    this.proxy.invoke('greetAll');
    };

    return {
    initialize: initialize,
    sendRequest: sendRequest
    };
    });

    app.controller('SignalRAngularCtrl', function ($scope, signalRSvc, $rootScope) {
    $scope.text = "";

    $scope.greetAll = function () {
    signalRSvc.sendRequest();
    }

    updateGreetingMessage = function (text) {
    $scope.text = text;
    }

    signalRSvc.initialize();

    $scope.$parent.$on("acceptGreet", function (e,message) {
    $scope.$apply(function () {
    updateGreetingMessage(message);
    });
    });
    });
    </script>
    </body>
    </html>

    ReplyDelete
  16. thanks for the informative post on angularjs

    ReplyDelete
  17. Hi Ravi
    I'm using angular ,jquery and signal together for my project but When I try to create hub connection using angular factory approach I am seeing different connections ids getting instead of connection being singleton !!Do we have any solution or any sample code with above posted example .
    Any help on this very much appreciated!!

    ReplyDelete
  18. Thanks for the information bro/., A clear post to know about the subject. While it is not intended- and surely beyond the scope of this ebook - to give an introduction or even deep dive into SignalR, we need to have a look at some concepts and code in order to realize a smooth integration of SignalR and AngularJS.

    ReplyDelete

Note: only a member of this blog may post a comment.