Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / SignalR

Real-Time Website Data Using SignalR

5.00/5 (4 votes)
2 Feb 2015CPOL7 min read 18.2K  
Real-time website data using SignalR

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:

C#
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)
        {
            // THIS IS THE DEFAULT MAPPING TO "/signalr"
            //app.MapSignalR();

            // Branch the pipeline here for requests that start with "/signalr"
            app.Map("/signalr", map =>
            {
                // Setup the cors middleware to run before SignalR.
                // By default this will allow all origins. You can
                // configure the set of origins and/or http verbs by
                // providing a cors options with a different policy.
                map.UseCors(CorsOptions.AllowAll);

                var hubConfiguration = new HubConfiguration
                {
                    // You can enable JSONP by uncommenting line below.
                    // JSONP requests are insecure but some older browsers (and some
                    // versions of IE) require JSONP to work cross domain
                    // EnableJSONP = true
                };

                // Run the SignalR pipeline. We're not using MapSignalR
                // since this branch already runs under the "/signalr" path.
                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:

C#
<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:

JavaScript
<script type="text/javascript">
$(function () {
    // ************************************************************************
    // TURN ON SIGNALR CLIENT-SIDE LOGGING
    $.connection.hub.logging = true;

    // METHOD CALLED FROM SERVER-SIDE CODE
    $.connection.APIHub.client.refreshList = function () {
        // DETERMINE IF CLIENT HAS CHECKIN OPEN
        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;
        }

        // ONLY REFRESH EXTJS STORE IF LIST IS OPEN
        if (listOpen) {
            var listStore = Ext.getStore('List.store.ListLocations');
            listStore.load({ params: { 'locationId': Session.user.LocationId } });
        }
    };

    // START CONNECTION USING THE USER'S LOCATION ID
    $.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);
    });

    // REPORT CONNECTION ERROR
    $.connection.hub.error(function (error) {
        console.log('SignalR error: ' + error);
    });

    // IF DISCONNECTED, ATTEMPT RESTART AFTER 5 SECONDS
    $.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);
    });

    // THIS ENSURES CONNECTION IS STOPPED WHEN WINDOW IS UNLOADED
    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:

C#
using System;
using System.Collections.Generic;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace API
{
    public class APIHub : Hub
    {
        // CALLED FROM EACH CLIENT THAT CONNECTS TO HUB
        public void ConnectToHub(string locationId)
        {
            // MAP LOGICAL USER "LOCATIONID" TO THE "CONNECTIONID" USING GROUPS.
            // THIS ALLOWS US TO SEND TO THE "GROUP" OUTSIDE OF THE HUB, WHERE THERE
            // IS NO CONCEPT OF THE "CONNECTIONID"
            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”:

C#
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)
        {
            // SETUP HUB CONTEXT TO THE "APIHUB"
            var context =
        Microsoft.AspNet.SignalR.GlobalHost.ConnectionManager.GetHubContext<APIHub>();

            // TRIGGER METHOD ON APPROPRIATE CLIENTS THAT CONNECTED WITH "LOCATIONID"
            context.Clients.Group(locationId).refreshList();

            // OUTPUT THAT METHOD WAS TRIGGERED AND FOR WHAT "LOCATIONID"
            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:

C#
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:

C#
	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:

C#
...
// INSTANCE VARIABLE
private IMessageHub _messageHub;
...
// DI INTO CONSTRUCTOR
public Repository(..., IMessageHub messageHub)
{
    ...
    this._messageHub = messageHub;
}
...
// WITHIN SPECIFIC METHOD TO COMMUNICATE THAT RECORD WAS ADDED TO SITE
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)