ToTa ToTa - 4 months ago 121
Javascript Question

SignalR client doesn't work inside AngularJs controller

I've created Angular application based on routes.
Without using the routes, the Signalr works fine, but when I work with routes, the Signalr works just in one way - from client side to server.

<html ng-app="SignalR">
<head>
//Scripts
<script src="~/signalr/hubs"></script>
</head>
<body>
<a href="#/Today">Today</a>
<a href="#/History">History</a>

<div ng-view>
</div>

<script type="text/javascript">
var hubConnetion = undefined;
hubConnetion = $.connection.hubs;
$.connection.hub.start();


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

var SignalRApp = angular.module('SignalR', ['ngRoute', 'SignalRControllers']);


SignalRApp.config(['$routeProvider',
function ($routeProvider) {
$routeProvider.
when('/Today', {
templateUrl: '@Url.Action("Today","Home")'
}).
when('/History', {
templateUrl: '@Url.Action("History", "Home")'
})
}]);

</script>
<script src="~/AngularJs/TodayController.js"></script>
</body>
</html>


TodayController.js

SignalRControllers.controller('TodayController', ['$scope', '$http',
function ($scope, $http) {

$scope.sendTask = function () {
hubConnetion.server.sendTask($("#taskName").val());//works
}

$scope.Task = "task1";
hubConnetion.client.getTask= function (Task) {//never invoked
$scope.Task = Task;
}
}]);


Today.html

<div ng-controller="TodayController">

<div>{{Task}}</div>


<input id="taskName" type="text" />
<div ng-click="sendTask()">SendTask</div>

</div>


Hubs.cs

public class Hubs: Hub
{
public void SendTask(string Name)
{
Clients.All.GetTask(Name);
}
}


I'm not sure, but I think that because the hubConnetion variable is wrapped inside the controller, signlarR couldn't find it.
Is there any angular magic that can solve this ?

Answer

I'm also fairly new to SignalR (and first time user of Angular) so I'v recreated (and simplified) the project in ASP.NET MVC 4 (SignalR 2.2) to nail the problem . And I think there is more then one...

1) SignalR is using JQuery. In jQuery (and almost any other JS framework) its relly better to execute your code after 'document ready' event. So I wrapped SignalR setup in jQuery document ready handler (see code below). Additionally I removed hubConnetion global variable because global variables are bad + after wrapping in jQuery document ready handler, variable is local to that handler and not accessible in controller code anyway...

2) Main problem IMHO is this (part of SignalR client API):

Normally you register event handlers before calling the start method to establish the connection. If you want to register some event handlers after establishing the connection, you can do that, but you must register at least one of your event handler(s) before calling the start method. One reason for this is that there can be many Hubs in an application, but you wouldn't want to trigger the OnConnected event on every Hub if you are only going to use to one of them. When the connection is established, the presence of a client method on a Hub's proxy is what tells SignalR to trigger the OnConnected event. If you don't register any event handlers before calling the start method, you will be able to invoke methods on the Hub, but the Hub's OnConnected method won't be called and no client methods will be invoked from the server.

Your event handler is setup in controller factory method which is called much later then connection start() method. Try to add temporary handler before calling start() like this:

$.connection.tasklistHub.client.getTask = function () { };

3) To be sure the problem is in SignalR and not Angular, i'v turned on both server and client side tracing. After all the changes above, event from server was successfully received on client:

SignalR: Triggering client hub event 'getTask' on hub 'TasklistHub'

But Angular failed to update div with data received. Solution is to use $rootScope.$apply function mentioned in this article about using SignalR and Angular together. Now everything works as expected. See full code below.

Sidenote: I understand this is just sample code. In any real world project I would consider wrapping SignalR hubs on client in some kind of service like in article mentioned above or using this library (looks very elegant)

I also take back my previous answer regarding the client side handler name as according to the same document, client side method name matching is case-insensitive...

Index.cshtml

<html lang="en" ng-app="SignalR">
    <head>
        <meta charset="utf-8" />
        <title>SignalR & AngularJS</title>
        <script src="~/Scripts/jquery-1.6.4.js"></script>
        <script src="~/Scripts/jquery.signalR-2.2.0.js"></script>
        <script src="~/signalr/hubs"></script>
        <script src="~/Scripts/angular.js"></script>

        <script type="text/javascript">
            $(function() {
                $.connection.hub.logging = true;

                // temporary handler to force SignalR to subscribe to server events
                $.connection.tasklistHub.client.getTask = function () { };

                $.connection.hub.start().done(function () {
                    $('#connectionStatus').text('Connected');
                    console.log('Now connected, connection ID =' + $.connection.hub.id);
                });
            });

            var SignalRControllers = angular.module('SignalRControllers', []);
            var SignalRApp = angular.module('SignalR', ['SignalRControllers']);
        </script>
        <script src="~/Scripts/app/TodayController.js"></script>
    </head>
<body>
    <div id="body">
        <div ng-view>
            <div ng-controller="TodayController">

                <div id="connectionStatus"></div>

                <div>{{Task}}</div>

                <input id="taskName" type="text"/>
                <input type="button" name="Send Task" value="Send Task" data-ng-click="sendTask()" />
            </div>
        </div>
    </div>
</body>
</html>

TodayController.js

SignalRControllers.controller('TodayController', [
        '$scope', '$http', '$rootScope',
        function ($scope, $http, $rootScope) {
            $scope.sendTask = function() {
                $.connection.tasklistHub.server.sendTask($("#taskName").val());
            };
            $scope.Task = "Empty";

            $.connection.tasklistHub.client.getTask = function (task) {
                console.log(task);
                $rootScope.$apply(function () {
                    $scope.Task = task;
                });             
            };
        }
    ]
);
Comments