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:
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.
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:
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!
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!
hi Ravi,
ReplyDeletethanks 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.
Thanks Anand. I will try to keep posting good content here.
DeleteI also have a good number of posts for beginners, have a look at them as well :)
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.
ReplyDeleteDo 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?
DeleteExample with Log4Net.SignalR Appender:
ReplyDeleteapp.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...
});
}]);
Thanks Juan. This approach makes the things cleaner!
DeleteAs 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.
ReplyDeleteTo 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);
});
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.
DeleteCould you post the server code or whole project?
ReplyDeleteAustin,
DeleteYou 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 :)
Just a question and a comment. Did you use $emit instead of $broadcast so that the event would only be published on $rootScope?
ReplyDeleteAlso, wouldn't it be a little cleaner to use $rootScope.$on instead of scope.$parent.$on?
Thanks for the post!
Jason,
DeleteYes, 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.
Hi, you must define functions that the hub can call back befor connection.start().
ReplyDeletehttp://www.asp.net/signalr/overview/signalr-20/hubs-api/hubs-api-guide-javascript-client#cantusegenproxy
Binh,
DeleteI 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.
This comment has been removed by the author.
ReplyDeleteHi 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?
ReplyDeleteIt's ok I got it working. I had to change the controller thus:
Deleteapp.controller('StockTickerCtrl', ['$scope','stockTickerData', function ($scope, stockTickerData) { ... }]);
Hi,
DeleteThanks 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.
Hi Ravi,
ReplyDeleteI 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
};
})
More samples with code.?
ReplyDeleteSure. 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.
DeleteHi Ravi,
ReplyDeleteDon'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.
Nice article.....
ReplyDeleteI 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,
ReplyDeleteHow to wirte jasmine unit test cases for signalr service?
ReplyDeleteFor 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.
ReplyDelete<!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>
thanks for the informative post on angularjs
ReplyDeleteHi Ravi
ReplyDeleteI'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!!
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.
ReplyDeletenice post thanks for sharing
ReplyDeleteEducation
Technology