In Part 1 of this series, I covered how to get started with step by step instructions for creating a Visual Studio Solution/Project and importing the needed NuGet packages to support our goal of running Signal-R.
In this post, I'll be covering the elements needed to get our project configured and get the Signal-R Hub up and running.
Startup.cs
If you followed the steps on Part 1, your VS project won't have a Startup.cs file. We need to add that file in order to initialize Signal-R when the web application first starts up.
Right click the Project name in the Solution Explorer and select Add >> Class.
using Owin;
namespace MySignalRWebSite
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
If the required NuGet packages were imported (see Part 1), there should be no errors. If there are errors, it is most likely due to missing the Owin Nuget package. Save the file and close the tab.
SignalRHub.cs
The next step is to create the Hub. This is done with another class file. Right click the Project in the Solution Explorer and select Add >> SignalR Hub Class (v2). When prompted, enter the name SignalRHub.cs and click the Ok button. VS will create the file, add it to the Project and open it as a new tab in the editor. VS adds some default code for you - the basics for a valid SignalR Hub class file:
using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MySignalRWebSite
{
public class SignalRHub : Hub
{
public void Hello()
{
Clients.All.hello();
}
}
}
The namespace line will be different for you, as will the MyHub1 reference (that should be "SignalRHub" for you), but the rest is likely the same. We will do some editing of this file to get all the pieces we need in place. I'll also try to explain some of the pieces to round out the big picture.
Using Statements
First, let's get the required using statements in place:
Using System;
using System.Configuration;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
HubName Decorator and Active User Tracking
In order for our web page to connect to this Hub, we need to decorate the class with a [HubName("SignalRHub")] decorator as shown.
Let's also add a simple data class and static List based on that class (noted in bold):
using System;
using System.Configuration;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
namespace MySignalRWebSite
{
[HubName("SignalRHub")]
public class SignalRHub : Hub
{
public static List<HubUser> hubUsers = new List<HubUser>();
public void Hello()
{
Clients.All.hello();
}
}
public class HubUser
{
public string UserName { get; set; }
public string GroupName { get; set; }
public string userConnectionId { get; set; }
}
}
What did we just do? The HubUser class defines a user who connects to the Hub from the client. The hubUsers list variable allows us to create a collection of those users. You could do this many different ways: database, dictionary, etc. Later, we'll see how to send this data back to a connected client to tell them who has connected to the Hub - like a roll call.
Hub Event Handlers
We don't need the Hello() method, delete those four lines. In place of that method, we are going to add some standard hub methods that allow us to respond to hub events, if needed.
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
public Task GetTask(string groupName)
{
return Groups.Add(Context.ConnectionId, groupName);
}
We're almost there! This is a good time to pause and explain a bit about how the Hub and Client (application, web site, etc) communicate with each other.
Hubs, Clients and Groups
Think of a SignalR Hub as an endpoint similar to a REST endpoint. A REST endpoint sits there waiting for something to do and when a client application makes a call, it responds accordingly. Maybe it performs an action and ends. Maybe it gets some data and returns that data to the client.
When a client makes a REST call, the endpoint performs its action and returns a response and the connection between the two is closed.
When a client connects to a SignalR Hub, the connection stays open and active until the client disconnects (closes the application, browses away from the page that made the connection, etc). While the connection is open, the client is able to make calls to the Hub (execute Hub methods) AND the Hub is able to make calls to the client (execute Client JavaScript methods, for example).
Because Signal-R has been around for quite a while, it can be supported by most browsers and most versions. The system detects the capabilities of the browser and establishes the Hub connection in a descending order of supported features to assure the best possible connection for the browser.
The purpose of SignalR is to allow a Hub to pro-actively communicate with connected clients so that those clients don't have to do interval polling to get information. For example, let's say that a client kicks off a time consuming process on the server. How can that client know when that process is complete and what the result was? One way would be for the server to record it's progress and for the client to poll that progress data periodically.... inefficient and clumsy. The SignalR way is for the client to make that long running process request and simply let the Hub tell the user when the process is complete. Cool!
However, in order for the Hub to identify the client(s) it should "talk" to, it must be able to identify them. There are a few different ways that the Hub can do this, and we'll look at those in future articles.
The JoinGroup Hub Method
Back to work! Let's add a method to let a client (web page, for example) join a group. This can go right below the GetTask method:
[HubMethodName("JoinGroup")]
public void JoinGroup(string groupName, string userName)
{
Groups.Add(Context.ConnectionId, groupName);
if (groupName.ToLower() == "all")
{
HubUser user = new HubUser();
user.UserName = userName;
user.GroupName = groupName;
user.userConnectionId = Context.ConnectionId;
hubUsers.Add(user);
Clients.All.updateAttendance(JsonConvert.SerializeObject(hubUsers));
}
}
Notice that we decorated this method with a HubMethodName. When the client makes a call to this Hub and this Method, the client must refer EXACTLY to the method as decorated.
The method is expecting two parameters: the name of the group to be joined (groupName) and the name of the user joining the group (userName).
The first line of the method performs the action of joining the user to the group. The moment a user connects to the hub, a persistent Context is created for that user's connection. The Context can be persistent because once the user connects, their connection remains open until they exit the application or browser page that created the connection. Every call a user makes to the Hub is associated with their unique Context and accompanying data.
In this case, if the user is joining a group called all, they are added to the hubUsers list. Notice that we are able to track their unique ConnectionId as part of their connection Context.
After the user is added to the designated group, the hub sends a message back to all connected clients (applications, web sites, etc) with a new attendance list. On a JavaScript client, this would be declared as a hub connection method called updateAttendance(data). We don't have to serialize the list, but I find it cleaner and more broadly compatible to do so. The client's updateAttendance method would receive the data and respond on the client appropriately. We'll see how that works in Part 3.
Groups: All vs ??
As shown above, we have a single method that can allow a user to join many different groups. If they happen to be joining the group called all, we are adding the connecting User to the hubUsers list.
Based on this structure, you can see that it would be very easy to have a single user joined to many different groups on the Hub at the same time - perhaps chatting with other users joined to those groups. This is what makes chat applications work the way they do!
The LeaveGroup Hub Method
If we are maintaining a list of active users, it's important to have a way to remove a client connection. Let's add a LeaveGroup method:
[HubMethodName("LeaveGroup")]
public void LeaveGroup(string groupName)
{
foreach (HubUser user in hubUsers)
{
if (user.userConnectionId == Context.ConnectionId)
{
hubUsers.Remove(user);
break;
}
}
Groups.Remove(Context.ConnectionId, groupName);
Clients.All.updateAttendance(JsonConvert.SerializeObject(hubUsers));
}
As you can see, this method loops through the hubUsers list and removes the user. It then removes the user from the designated group. Finally, it reports back to all connected clients the new attendance list.
Getting the hang of it? One last Hub method for now.
The SendMessage Hub Method
One of the importing things we're likely to want to do with our Hub and Clients is simply allow them to communicate. SendMessage does just that.
[HubMethodName("SendMessage")]
public void SendMessage(string groupName, string message, string userName)
{
var msg = new
{
groupName = groupName,
userName = userName,
message = message
};
Clients.Group(groupName).processChatMessage(msg);
}
This method needs to know the group (groupName) to send the message to, the message (message) to send to that group and (optionally) the name (userName) of the sending user. With those bits of data in place, the hub can send the incoming message to all clients joined to the designated group... and let those clients know who the message is from. Imagine this flow:
Client opens web page >> web page connects to Hub >> client now has a unique connection Context >> client joins a group >> Hub notifies connected clients of new attendance >> client sends a message to "group-1" (for example) >> Hub sends message to members of "group-1" group.
And it all happens in the blink of an eye.
Final SignalRHub.cs file
Note that your namespace and hub class name will be different
using System;
using System.Configuration;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
namespace MySignalRWebSite
{
[HubName("SignalRHub")]
public class SignalRHub : Hub
{
public static List<HubUser> hubUsers = new List<HubUser>();
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
public Task GetTask(string groupName)
{
return Groups.Add(Context.ConnectionId, groupName);
}
[HubMethodName("JoinGroup")]
public void JoinGroup(string groupName, string userName)
{
Groups.Add(Context.ConnectionId, groupName);
if (groupName.ToLower() == "all")
{
HubUser user = new HubUser();
user.UserName = userName;
user.GroupName = groupName;
user.userConnectionId = Context.ConnectionId;
hubUsers.Add(user);
Clients.All.updateAttendance(JsonConvert.SerializeObject(hubUsers));
}
}
[HubMethodName("LeaveGroup")]
public void LeaveGroup(string groupName)
{
foreach (HubUser user in hubUsers)
{
if (user.userConnectionId == Context.ConnectionId)
{
hubUsers.Remove(user);
break;
}
}
Groups.Remove(Context.ConnectionId, groupName);
Clients.All.updateAttendance(JsonConvert.SerializeObject(hubUsers));
}
[HubMethodName("SendMessage")]
public void SendMessage(string groupName, string message, string userName)
{
var msg = new
{
groupName = groupName,
userName = userName,
message = message
};
Clients.Group(groupName).processChatMessage(msg);
}
}
public class HubUser
{
public string UserName { get; set; }
public string GroupName { get; set; }
public string userConnectionId { get; set; }
}
}
Wrap Up Part 2
In this article, we built out the infrastructure needed for a working AspNet SignalR Hub. in the next article, we'll create a client side web page that connects to the hub and sends/receives a message.
Next Up: Part 3

No comments:
Post a Comment