Integrating SignalR with Angular

Home / Integrating SignalR with Angular

SignalR is a nice framework for broadcasting messages to all clients connected to your web server. It also provides mechanisms to allow those clients to send messages to other clients. It’s not “Angular friendly” out of the box, though, since it’s designed more for use with (imho) jQuery. Here’s a simple service that I like to use with SignalR and Angular


On the server-side of things, SignalR uses what it calls a hub to transmit and receive messages. Based on the specification of this hub, JavaScript proxies get created for the server-side methods that exist within the hub. Using these proxies, you get a handle to other the send and receive methods that the server provides. This is pretty slick and is where SignalR is very powerful. On the client, SignalR has a polling mechanism to go get messages that are transmitted from the server. This is, primarily, where we want to hook into SignalR.

In the Angular context, I set up my SignalR connections and the message receive handlers through an Angular service. This makes this easily reusable and it avoids having multiple subscriptions to the messages. Keeping in line with Angular fashion, I push those messages received out as Angular events. Then, whatever controllers I have that want to consume those messages and listen for them.

To start, I create a list of all know “server methods” that I can define callbacks for and which hub they exist on. Note that to get a handle to the hub proxy, we use “$.connection..”

SignalR provides direct access to the callbacks or assigning events with the jquery deferred events “.on” method. There is a caveat to the latter method, though. It requires setting empty callback handlers initially, prior to starting the hub connection. This does allow decoupling the starting of the proxy from the event/callback subscriptions, though.

var
    messageHubs = [
        {
            hub: $.connection.someHub, name: 'someHub', reconnectTimer: undefined,
            broadcastMessages: [
                {
                    serverMethodName: 'method1', subscribed: false, self: this,
                    handler: function (data1, data2) {
                        var data = { data1: data1, data2: data2 };
                        broadcastHandler('method1Received', data);
                    }
                },
                {
                    serverMethodName: 'method1', subscribed: false,
                    handler: function (data1, data2) {
                        var data = { data1: data1, data2: data2 };
                        broadcastHandler('method1Received', data);
                    }
                }
            ]
        }
    ];

// Must attach an empty handler initially (limitation of signalr)
for (var i = 0; i < messageHubs.length; i++) {
    var messageHub = messageHubs[i];
    for (var j = 0; j < messageHub.broadcastMessages.length; j++) {
        var broadcastMessage = messageHub.broadcastMessages[j];
        var serverMethodName = broadcastMessage.serverMethodName;
        messageHub.hub.client[serverMethodName] = function () { };
    }
}

Additionally, in the service, we want to attach handlers to each hub to deal with its connection state. This deals with state changes, reconnecting (timeout retry based), and that sort of thing:

var attachHandlers = function (messageHub) {
    messageHub.hub.connection.stateChanged(function (change) {
        if (change.newState === $.signalR.connectionState.reconnecting) {
            $log.log('SignalR connection lost to hub ' + messageHub.name);
        } else if (change.newState === $.signalR.connectionState.connected) {
            $log.log('SignalR connected. Hub: ' + messageHub.name);
        } else if (change.newState === $.signalR.connectionState.disconnected) {
            $log.log('SignalR disconnected. Hub: ' + messageHub.name);
        };
    });

    messageHub.hub.connection.error(function (error) {
        if (error) {
            $log.log('Error on hub ' + messageHub.name + ':' + error.message);
        }

        if (messageHub.reconnectTimer) {
            $timeout.cancel(messageHub.reconnectTimer);
        }

        // Try to restart the connection
        messageHub.reconnectTimer = $timeout(function () { $.connection.hub.start(signalrTransport); }, 2000);
    });

    messageHub.hub.connection.reconnected(function (error) {
        $log.log('SignalR reconnected.  Hub: ' + messageHub.name);
        if (messageHub.reconnectTimer) {
            $timeout.cancel(messageHub.reconnectTimer);
        }
    });
}

var attachConnectionEventHandlers = function () {
    for (var i = 0; i < messageHubs.length; i++) {
        var messageHub = messageHubs[i];
        attachHandlers(messageHub);
    }
};

We’ll expose a start listener method that our consumer can call that fires up the hub(s). It does not subscribe to any events. It’s wrapped in a promise since we can’t subscribe to events until after the SignalR connection start is completed. This method is what attaches our connection event handlers as well.

var startListener = function () {
    var defer = $q.defer();
    var isAnyHubNull = false;
    for (var i = 0; i < messageHubs.length; i++) {
        if (messageHubs[i].hub == null) {
            isAnyHubNull = true;
            break;
        }
    }
    if (isAnyHubNull) {
        defer.reject();
    } else {
        attachConnectionEventHandlers();
        $.connection.hub
            .start(signalrTransport)
            .done(function () {
                defer.resolve();
            })
            .fail(function (error) {
                if (error) {
                    $log.log(error.message);
                }
                else {
                    $log('Error - disconnected.');
                }
                defer.reject();
            });
    }
    return defer.promise;
};

We expose subscribe and unsubscribe methods. These are pretty simple. The event (server method) name is passed in along with the hub name. We search for the hub in our list of hubs and then find the appropriate message. Once found, we use the “.on” method to attach the specified handler to the event.

var findHubByName = function (name) {
    var criteriaFunction = function (c) {
        return c.name === name;
    };
    var results = messageHubs.filter(criteriaFunction);
    return results && results.length > 0 ? results[0] : null;
};

var findBroadcastMessage = function (messageHub, serverMethodName) {
    var criteriaFunction = function (c) {
        return c.serverMethodName == serverMethodName;
    };
    var results = messageHub.broadcastMessages.filter(criteriaFunction);
    return results && results.length > 0 ? results[0] : null;
};

var subscribe = function (name, event) {
    var messageHub = findHubByName(name);
    var broadcastMessage = findBroadcastMessage(messageHub, event);
    if (!broadcastMessage.subscribed) {
        messageHub.hub.on(event, broadcastMessage.handler);
        broadcastMessage.subscribed = true;
        $log.log('SignalR Subscribed. Hub: ' + messageHub.name + ', Event: ' + event);
    }
};

var unsubscribe = function (name, event) {
    var messageHub = findHubByName(name);
    var broadcastMessage = findBroadcastMessage(messageHub, event);
    if (broadcastMessage.subscribed) {
        messageHub.hub.off(event, broadcastHandler.handler);
        broadcastMessage.subscribed = false;
        $log.log('SignalR Unsubscribed. Hub: ' + messageHub.name + ', Event: ' + event);
    }
};

Finally, in our controller, we would do something like this to start the service, subscribe to messages, and listen for our defined events:

signalrService.startListener().then(function (response) {
    signalrReady = true;

    signalrService.subscribe('someHub', 'method1');
    signalrService.subscribe('someHub', 'method2');
});

$scope.$on("method1Received", function (event, data) {
    // Do stuff
});

$scope.$on("method2Received", function (event, data) {
    // Do stuff
});

One nice thing about defining our service with respect to multiple hubs, message types, and events is that we only have to update our list without changing the underlying code that relies upon the service. We can use the same service regardless of the number of SignalR hubs or message-types that we add to the server. Below is the full code for the service, and I’ll pop it into my github repo later.

(function () {
    var signalrService = function ($rootScope, $q, $log, $location, $timeout) {
        var baseUrl = $location.protocol() + '://' + $location.host() + ($location.port() !== 80 && $location.port() !== 443 ? ':' + $location.port() : '');
        $.connection.hub.url = baseUrl + '/signalr/hubs';
        var
            messageHubs = [
                {
                    hub: $.connection.someHub, name: 'someHub', reconnectTimer: undefined,
                    broadcastMessages: [
                        {
                            serverMethodName: 'method1', subscribed: false, self: this,
                            handler: function (data1, data2) {
                                var data = { data1: data1, data2: data2 };
                                broadcastHandler('method1Received', data);
                            }
                        },
                        {
                            serverMethodName: 'method1', subscribed: false,
                            handler: function (data1, data2) {
                                var data = { data1: data1, data2: data2 };
                                broadcastHandler('method1Received', data);
                            }
                        }
                    ]
                }
            ],
            signalrTransport = { transport: ['longPolling', 'webSockets'] },
            findHubByName = function (name) {
                var criteriaFunction = function (c) {
                    return c.name === name;
                };
                var results = messageHubs.filter(criteriaFunction);
                return results && results.length > 0 ? results[0] : null;
            },
            findBroadcastMessage = function (messageHub, serverMethodName) {
                var criteriaFunction = function (c) {
                    return c.serverMethodName == serverMethodName;
                };
                var results = messageHub.broadcastMessages.filter(criteriaFunction);
                return results && results.length > 0 ? results[0] : null;
            };

        // Must attach an empty handler initially (limitation of signalr)
        for (var i = 0; i < messageHubs.length; i++) {
            var messageHub = messageHubs[i];
            for (var j = 0; j < messageHub.broadcastMessages.length; j++) {
                var broadcastMessage = messageHub.broadcastMessages[j];
                var serverMethodName = broadcastMessage.serverMethodName;
                messageHub.hub.client[serverMethodName] = function () { };
            }
        }

        var broadcastHandler = function (eventName, data) {
            $log.log('SignalR message received: ' + eventName);
            $rootScope.$broadcast(eventName, data);
        };

        var attachHandlers = function (messageHub) {
            messageHub.hub.connection.stateChanged(function (change) {
                if (change.newState === $.signalR.connectionState.reconnecting) {
                    $log.log('SignalR connection lost to hub ' + messageHub.name);
                } else if (change.newState === $.signalR.connectionState.connected) {
                    $log.log('SignalR connected. Hub: ' + messageHub.name);
                } else if (change.newState === $.signalR.connectionState.disconnected) {
                    $log.log('SignalR disconnected. Hub: ' + messageHub.name);
                };
            });

            messageHub.hub.connection.error(function (error) {
                if (error) {
                    $log.log('Error on hub ' + messageHub.name + ':' + error.message);
                }

                if (messageHub.reconnectTimer) {
                    $timeout.cancel(messageHub.reconnectTimer);
                }

                // Try to restart the connection
                messageHub.reconnectTimer = $timeout(function () { $.connection.hub.start(signalrTransport); }, 2000);
            });

            messageHub.hub.connection.reconnected(function (error) {
                $log.log('SignalR reconnected.  Hub: ' + messageHub.name);
                if (messageHub.reconnectTimer) {
                    $timeout.cancel(messageHub.reconnectTimer);
                }
            });
        }

        var attachConnectionEventHandlers = function () {
            for (var i = 0; i < messageHubs.length; i++) {
                var messageHub = messageHubs[i];
                attachHandlers(messageHub);
            }
        };

        var subscribe = function (name, event) {
            var messageHub = findHubByName(name);
            var broadcastMessage = findBroadcastMessage(messageHub, event);
            if (!broadcastMessage.subscribed) {
                messageHub.hub.on(event, broadcastMessage.handler);
                broadcastMessage.subscribed = true;
                $log.log('SignalR Subscribed. Hub: ' + messageHub.name + ', Event: ' + event);
            }
        };

        var unsubscribe = function (name, event) {
            var messageHub = findHubByName(name);
            var broadcastMessage = findBroadcastMessage(messageHub, event);
            if (broadcastMessage.subscribed) {
                messageHub.hub.off(event, broadcastHandler.handler);
                broadcastMessage.subscribed = false;
                $log.log('SignalR Unsubscribed. Hub: ' + messageHub.name + ', Event: ' + event);
            }
        };

        var sendMessage = function () {
            messageHubs[0].hub.server.send("Hello!", "I'm a button click and you received a response from SignalR!");
        };

        var startListener = function () {
            var defer = $q.defer();
            var isAnyHubNull = false;
            for (var i = 0; i < messageHubs.length; i++) {
                if (messageHubs[i].hub == null) {
                    isAnyHubNull = true;
                    break;
                }
            }
            if (isAnyHubNull) {
                defer.reject();
            } else {
                attachConnectionEventHandlers();
                $.connection.hub
                    .start(signalrTransport)
                    .done(function () {
                        defer.resolve();
                    })
                    .fail(function (error) {
                        if (error) {
                            $log.log(error.message);
                        }
                        else {
                            $log('Error - disconnected.');
                        }
                        defer.reject();
                    });
            }
            return defer.promise;
        };

        return {
            startListener: startListener,
            sendMessage: sendMessage,
            subscribe: subscribe,
            unsubscribe: unsubscribe
        };
    };

    signalrService.$inject = ['$rootScope', '$q', '$log', '$location', '$timeout'];
    angular.module('long2know.services')
        .factory('signalrService', signalrService);
})()

Leave a Reply