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!

15 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