When I was tasked with using SignalR to implement the real-time updating of data on a project I was working on, I was excited. Namely because it is a newer technology and I have always welcomed learning new technologies – especially when there is an immediate need to implement it, as opposed to reading about the technology and then never using it again.
So I started where everyone else starts: Google. I found out quickly that most, if not all of the results that I turned up with example code were making use of the same thing: a Chat application. This would have been perfect for me, if that was what I was looking to build… which it wasn’t.
So I copied the code, and within five minutes, I had a Chat application up and running. It demonstrated SignalR’s functionality – how it provides a path for the server to communicate with its clients, and how clients communicate with a server Hub. Unfortunately, it was far from what I was looking to accomplish.
Background
I have a “website” that is implemented with ExtJS. It then communicates with an “API site” implemented in ASP.NET to communicate with the database. I have a page that lists out records that are entered into the website. These records can be entered on this page through my browser, or they could be entered from another browser elsewhere, anywhere.
The way it is currently updating, and how I have implemented in the past for similar situations, is to send a request to the server at one-minute intervals, and then redraw the list.
This works fine, but if a record is added to the website from another browser just one second after the list is updated, it will take nearly another minute to see that record on the other browser that is displaying the list of records.
Enter SignalR and its real-time updating.
Implementation
Enough history, here is my implementation:
1. Using Nuget, I installed SignalR and all of its associated packages into my API site.
After it installed, it brought up a README file with the code for the “Startup.cs” file that it instructs you to create.
I created that “Startup.cs” file in the “App_Start” folder.
I later found that on the ASP.NET website, it describes how to establish a cross-domain (CORS) connection, providing this “Startup.cs” file, which I am now using. You will need to use Nuget and install the “Microsoft.Own.Cors
” package also.
Here is my “Startup.cs” file:
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Cors;
using Microsoft.AspNet.SignalR;
[assembly: OwinStartup(typeof(API.Startup))]
namespace API
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
};
map.RunSignalR(hubConfiguration);
});
}
}
}
2. When SignalR was installed, it created jQuery and SignalR .js files in the “Scripts” directory. I first copied those files from the API site to the website for more efficient loading.
I then included those two local .js files in the “Views/Shared/_Layout.cshtml” file located on my website, and then a reference to the “signalr/hubs” virtual directory created by SignalR on the API site:
<script src="/Content/scripts/jquery-1.9.1.js"></script>
<script src="/Content/scripts/jquery.signalR-2.1.1.min.js"></script>
<!-- REFERENCE THE AUTO-GENERATED SIGNALR HUB SCRIPT THAT RESIDES ON THE API -->
<script src="/API/signalr/hubs"></script>
3. And then under that, I have the client side code needed to connect to the SignalR “Hub,” to define the method that the server will call on each of its connected clients, and some other nice-to-haves, such as to turn client-side logging on and to reconnect to the Hub if it becomes disconnected.
This reconnecting method has really helped me a lot as there are times where SignalR does not connect initially, and then this reconnecting method kicks in. It will try to connect every five seconds until it connects successfully.
Here is that code:
<script type="text/javascript">
$(function () {
$.connection.hub.logging = true;
$.connection.APIHub.client.refreshList = function () {
var listOpen = false;
var windows = Ext.ComponentQuery.query('openitems')[0].items;
for (var i = windows.items.length - 1; i >= 0; i--) {
var toolbar = windows.items[i];
var target = toolbar.items.items[0].id;
listOpen = target.indexOf('list') > 0 ? true : false;
if (listOpen) break;
}
if (listOpen) {
var listStore = Ext.getStore('List.store.ListLocations');
listStore.load({ params: { 'locationId': Session.user.LocationId } });
}
};
$.connection.hub.start()
.done(function () {
console.log('SignalR connected, connection id = ' + $.connection.hub.id);
console.log('Session.user.LocationId = ' + Session.user.LocationId);
$.connection.iCHub.server.connectToHub(Session.user.LocationId);
})
.fail(function (data) {
console.log('SignalR failed to connect: ' + data);
});
$.connection.hub.error(function (error) {
console.log('SignalR error: ' + error);
});
$.connection.hub.disconnected(function () {
setTimeout(function () {
$.connection.hub.start()
.done(function () {
console.log('SignalR reconnected, connection id =
' + $.connection.hub.id);
$.connection.iCHub.server.connectForCheckins
(Session.user.LocationId);
})
.fail(function (data) {
console.log('SignalR failed to reconnect: ' + data);
});
}, 5000);
});
window.onbeforeunload = function (e) {
$.connection.hub.stop();
};
});
</script>
4. On the API site, I then created a folder named “hubs” and created my “Hub” file there. To create your Hub file, you will right-click on the folder. Click “Add” and then choose “SignalR Hub Class (v2)” and name it whatever you want. I named mine “APIHub.” This will add a default hub with a function that, when called from server side code, would call the function “hello()
” on all clients that are connected to the hub, which isn’t useful for my purposes, but may be to someone else.
I need to notify clients of new records based on the “LocationId
” they are watching. So if a record gets added to the database for that LocationId
from any other browser, and I am monitoring that LocationId
on my browser, I want my list to update.
This is where using SignalR Groups comes into play. Groups provide a way to map a “connectionid
” that SignalR gives to each connected client to a specific named User, which in my case will represent a LocationId
.
This is important in my usage, as you will see later on.
Here is the APIHub.cs file:
using System;
using System.Collections.Generic;
using System.Web;
using Microsoft.AspNet.SignalR;
namespace API
{
public class APIHub : Hub
{
public void ConnectToHub(string locationId)
{
Groups.Add(Context.ConnectionId, locationId);
System.Diagnostics.Debug.WriteLine("HUB: CONNECTFORCHECKINS: " + locationId);
}
}
}
5. As you can see above, my Hub
has only a single method, which is called from clients when they want to connect to the Hub
, passing the LocationId
that they want to monitor. For my purposes, the communication from the server to the client will happen outside of the Hub
, therefore I need the “HubUtility
” class file.
The reason for this is because it implements an interface that is in the namespace “Core
,” which is where my repository class is that will need to trigger the call back to the specific clients that are monitoring a LocationId
.
Since I can’t call Hub
methods directly from outside of the context of the Client-Hub connection, I must use this HubUtility
class and it will make use of SignalR’s ability to use a “HubContext
.” It will then use it to trigger the call-back to its connected clients.
I place this HubUtility
class in the hubs directory on the API site so it is in the same directory as our Hub file.
Here is the “HubUtility.cs”:
using System;
using System.Collections.Generic;
using System.Web;
using Microsoft.AspNet.SignalR;
using Core;
namespace API
{
public class HubUtility : IMessageHub
{
public void RefreshListOnClient(string locationId)
{
var context =
Microsoft.AspNet.SignalR.GlobalHost.ConnectionManager.GetHubContext<APIHub>();
context.Clients.Group(locationId).refreshList();
System.Diagnostics.Debug.WriteLine("HUB: RefreshListOnClient: " + locationId);
}
}
}
And here is the “IMessageHub.cs” Interface that I placed with the rest of my “Core” files on the API site:
using System;
namespace ityCity.Core
{
public interface IMessageHub
{
void RefreshCheckinListOnClient(string client);
}
}
6. We use “Autofac” in our application so in the API site’s “Global.asax.cs,” we need to register the HubUtility
so it can be found. If you don’t use Autofac, then this will not be necessary.
In the “Application_Start
” method, I added the following under the config register statements:
var builder = new ContainerBuilder();
builder.RegisterType<HubUtility>().AsImplementedInterfaces().InstancePerApiRequest();
7. In my repository class file, we use dependency injection to inject an object that is of the interface “IMessageHub
” which HubUtility
implements.
This will allow me to make my call back to my HubUtility
class method passing the appropriate LocationId
, which will in turn make use of the “APIHub
” context, and will trigger the client side method call on clients that have connected to the Hub passing that particular “LocationId
.”
Here is that pertinent code:
...
private IMessageHub _messageHub;
...
public Repository(..., IMessageHub messageHub)
{
...
this._messageHub = messageHub;
}
...
this._messageHub.RefreshListOnClient( record.locid );
Conclusion
SignalR makes using web sockets very easy to facilitate real-time updating of data on a website. There is also the use of SQL Dependency that would allow the ability to monitor specific data within your database and trigger calls to clients when that data is changed in the database. We chose to not use this method as our data may not always be in the database. In fact, it is changing to be in a remote CRM located elsewhere that we are communicating with using their API.
Doing it the way we have implemented it, using the HubContext
, we can trigger the call whether we continue using a local database or some other remote data repository such as a CRM.
I am looking forward to finding new ways to make use of SignalR in the future. I hope this helps at least one other person, with either introducing them to the power of SignalR or demonstrating another possible usage, other than a Chat application.
Although my implementation may have some unique circumstances due to the complexity of how the application was constructed, I am hopeful that it has demonstrated how SignalR can be used outside of just the Client-Hub usage that a Chat application uses, and shows how you can use SignalR to trigger communication with connected clients from deep inside your application.
I feel that I have only scratched the surface of the possibilities that SignalR has to offer. For some more excellent information regarding SignalR, please see Brad Trebilcock’s post. Anywhere data is displayed and updated, SignalR may possibly hold the answer to a more useful website and, maybe more importantly, a better user experience.
— John Holland, asktheteam@keyholesoftware.com