Introduction to WebSockets with SignalR in .NET Part 6: the basics of publishing to groups

Introduction

So far in this series on SignalR we’ve published all messages to all connected users. The goal of this post is to show how you can direct messages to groups of people. It is based on a request of a commenter on part 5. You can think of groups as chat rooms in a chat application. Only members of the chat room should see messages directed at that room. All other rooms should remain oblivious of those messages.

We’ll build upon the SignalR demo app we’ve been working on in this series. So have it open in Visual Studio. We’ll simulate the following scenario:

  • When a client connects for the first time they are assigned a random age between 6 and 100 inclusive
  • The client joins a “chat room” based on this age by calling a function in ResultsHub
  • When the client sends a message then only those already in the chat room will be notified

The real-life version would of course involve some sign-up page where the user can define which room to join or what their age is. You can also store those users if the chat application is closed to unanonymous users. However, all that would involve too much extra infrastructure irrelevant to the main goals.

Joining a group

We first need to assign an age to the client when they navigate to the Home page. Locate HomeController.cs. The Index action currently only returns a View. Extend it as follows:

public ActionResult Index()
{
	Random random = new Random();
	int next = random.Next(6, 101);
	ViewBag.Age = next;
        return View();
}

Normally you’d pass the age into the View in a view-model object but ViewBag will do just fine. In Index.cshtml add the following markup just below the Register message header:

<div>
    Your randomised age is <span id="ageSpan">@ViewBag.Age</span>
</div>

Now we want to call a specific Hub function as soon as the connection with the hub has been set up. The server side function will put the user in the appropriate chat room based on their age. Insert the following code into results.js just below the call to hub.start():

$.connection.hub.start().done(function ()
{        
        var age = $("#ageSpan").html();
        resultsHub.server.joinAppropriateRoom(age);
});  

When the start() function has successfully returned we’ll read the age from the span element and run a method called joinAppropriateRoom in ResultsHub. Open ResultsHub.cs and define the function and some private helpers as follows:

public void JoinAppropriateRoom(int age)
{
	string roomName = FindRoomName(age);
	string connectionId = Context.ConnectionId;
	JoinRoom(connectionId, roomName);
	string completeMessage = string.Concat("Connection ", connectionId, " has joined the room called ", roomName);
	Clients.All.registerMessage(completeMessage);
}

private Task JoinRoom(string connectionId, string roomName)
{			
	return Groups.Add(connectionId, roomName);
}	

private string FindRoomName(int age)
{
	string roomName = "Default";
	if (age < 18)
	{
		roomName = "The young ones";
	}
	else if (age < 65)
	{
		roomName = "Still working";
	}
	else
	{
		roomName = "Old age pensioners";
	}
	return roomName;
}

We’ll first find the appropriate room for the user based on the age. We’ve defined 3 broad groups: youngsters, employed and old age pensioners. We extract the connection ID and put it into the correct room. Note that we didn’t need to define a group beforehand. It will be set up automatically as soon as the first member is added to it. We then also notify every client that there’s a new member.

While we’re at it let’s define the function that will be called by the client to register the message together with their age. The age is again needed to locate the correct group. Add the following method to ResultsHub.cs:

public void DispatchMessage(string message, int age)
{			
	string roomName = FindRoomName(age);			
	string completeMessage = string.Concat(Context.ConnectionId
		, " has registered the following message: ", message, ". Their age is ", age, ". ");
	Clients.Group(roomName).registerMessage(completeMessage);
}

We again find the correct group, construct a message and send out the message to everyone in that group.

There’s one last change before we can test this. Locate the newMessage function definition of the messageModel prototype in results.js. It currently calls the simpler sendMessage function of ResultsHub. Comment out that call. Update the function definition as follows:

newMessage: function () {
            var age = $("#ageSpan").html();
            //resultsHub.server.sendMessage(this.registeredMessage());
            resultsHub.server.dispatchMessage(this.registeredMessage(), age);
            this.registeredMessage("");
        },
.
.
.

Run the application. The first client should see that they have joined some room based on their age. In my case it looks as follows:

First client joining a group

Open some more browser windows with the same URL and all of them should be assigned an age and a group. The very first client should see them all. In my test I got the following participants:

First client sees all subsequent chat participants

I started up 6 clients: 3 old age pensioners, 2 youngsters and 1 still working. It’s not easy to copy all windows so that we can see everything but here are the 6 windows:

All clients joining some group and conversing

The top left window was the very first client, a 77-year-old who joined the pensioners’ room. The one below that is a 65-year-old pensioner. Under that we have a 24-year-old worker. Then on the right hand side from top to bottom we have a 15-year-old, a 90-year-old and finally a 6-year-old client. Then the 65-year-old client sent a message which only the other pensioners have receiver. Check the last message in each window: the clients of 24, 6 and 15 years of age cannot see the message. Then the 6-year-old kid sent a message. This time only the 2 youngsters were sent the message. You’ll see that the 24-year old worker has not received any of the messages.

We’ve seen a way how you can direct messages based on group membership. You can define the names of the rooms in advance, e.g. in a database, so that your users can pick one upon joining your chat application. Then instead of sending in their age they can send the name of the room which to join. However, if you’re targeting specific users without them being aware of these groups you’ll need to collect the criteria from them based on which you can decide where to put them. In the above example the only criteria was their age. In other case it can be a more elaborate list of conditions.

View the list of posts on Messaging here.

Introduction to WebSockets with SignalR in .NET Part 3

Introduction

We’ll continue our discussion of SignalR where we left off in the previous post. So open the SignalRWeb demo project and let’s get to it!

Demo continued

We left off having the following “scripts” section in Index.cshtml:

@section scripts
{
    <script src="~/Scripts/jquery.signalR-2.0.3.js"></script>
    <script src="~/Scripts/knockout-3.1.0.js"></script>
    <script src="~/Scripts/results.js"></script>
}

We’ll build on this example to see how a client can interact with the server through WebSockets. At present we have an empty Hello() method in our ResultsHub class. Insert another method which will allow the clients to send a message to the server:

public void SendMessage(String message)
{

}

There’s a property called Context that comes with SignalR. It is similar to HttpContext in ASP.NET: it contains a lot of information about the current connection: headers, query string, authentication etc. Just type “Context.” in the method body and inspect the available properties, they should be self-explanatory. We have no authentication so we’ll use the connection ID property of Context to give the sender some identifier:

public void SendMessage(String message)
{
	string completeMessage = string.Concat(Context.ConnectionId
		, " has registered the following message: ", message);

	Clients.All.registerMessage(completeMessage);
}

You’ll recall from the previous post that we need to deal with JavaScript in SignalR. The above piece of code will result in a JavaScript method called “registerMessage” to be invoked from the server. Where is that method? We need to write it of course. We inserted a JavaScript file called “results.js” previously. Open that file and enter the following stub:

(function () {
    var resultsHub = $.connection.resultsHub;
}());

resultsHub is a reference to the hub we’ve been working on. The “connection” property comes from the SignalR jQuery library and creates a SignalR connection. Through the connection property you’ll be able to reference your hubs. There’s no IntelliSense for the hub names, so be careful with the spelling.

We need to stop for a second and go back to Index.cshtml. There’s one more JavaScript source we need to reference, but it’s not available in the Scripts folder. Recall that we used the MapSignalR OWIN extension in Startup.cs. That extension will map the SignalR hubs to the /signalr endpoint. Add the following script declaration to the scripts section below the jquery.signalR-2.x.x.js script reference:

<script src="~/SignalR/hubs"></script>

Start the application and navigate to http://localhost:xxxxx/SignalR/hubs. You should see some JavaScript related to SignalR in the browser, so the script reference is valid. Basically this code generates proxies for the hubs. If you scroll down you’ll see that it has found the ResultsHub and its two dynamic methods:

proxies.resultsHub = this.createHubProxy('resultsHub'); 
proxies.resultsHub.client = { };
proxies.resultsHub.server = {
   hello: function () {
    return proxies.resultsHub.invoke.apply(proxies.resultsHub, $.merge(["Hello"], $.makeArray(arguments)));
   },

   sendMessage: function (message) {
      return proxies.resultsHub.invoke.apply(proxies.resultsHub, $.merge(["SendMessage"], $.makeArray(arguments)));
             }
};

As you add other hubs and other dynamic methods they will be registered here in this script generated by SignalR using Reflection. Note that the hello and sendMessage functions have been registered on the server – proxies.resultsHub.server – which is expected.

Add the following code to results.js just below the resultsHub reference:

$.connection.hub.logging = true;
$.connection.hub.start();

We turn on logging so that we can see what SignalR is doing behind the scenes. Then we tell SignalR to start the communication. This method will go through the 4 ways of establishing a connection with the client and determine which one works best.

Next we need the JavaScript methods that will be invoked: hello and registerMessage. Add the following stubs just below the call to hub.start():

resultsHub.client.hello = function(){

}

resultsHub.client.registerMessage = function (message) {

};

We’ll concentrate on the registerMessage function, hello can remain empty. Next we need to show the responses on the screen. As mentioned before we’ll use knockout.js as it provides for a very responsive GUI. I in fact only know the basics of knockout.js, as client side programming is not really my cup of tea. If you don’t know anything about knockout.js then you might want to go through the first couple of tutorials here. It is very much based on view-models which are bound to HTML elements. As the properties change so do the values of those elements in a seamless fashion. We won’t go into any detail about the knockout specific details here. Add the following code below the resultsHub.client.registerMessage stub:

var messageModel = function () {
      this.registeredMessage = ko.observable(""),
      this.registeredMessageList = ko.observableArray()
};

The registeredMessage property will show a message sent by the client. registeredMessageList will hold all messages that have been sent from the server. Next add a model prototype:

messageModel.prototype = {

        newMessage: function () {
            resultsHub.server.sendMessage(this.registeredMessage());
            this.registeredMessage("");
        },
        addMessageToList: function (message) {
            this.registeredMessageList.push(message);
        }

    };

newMessage is meant to be a function that can be invoked upon a button click. It sends the message to the server and then clears it. Recall that we called our function in ResultsHub “SendMessage”. You can call it using the “server” property followed by the name of the function you’d like to invoke. addMessageToList does exactly what the function name implies. Then we create an instance of the view-model and instruct knockout to start binding it to our GUI elements:

var viewModel = new messageModel();
$(function () {
        ko.applyBindings(viewModel);
    });

We can now fill in the resultsHub.client.registerMessage stub:

resultsHub.client.registerMessage = function (message) {
        viewModel.addMessageToList(message);
};

It simply calls upon the view-model instance to add the new message to the message list.

Now we can create the corresponding HTML code on index.cshtml. Add the following right above the scripts section.

<div>
    <input type="text" placeholder="Your message..." data-bind="value:registeredMessage" />
    <button data-bind="click:newMessage">Register message</button>
</div>

This bit of markup will serve as the “register” section where the user enters a message in the text box and sends it to the server using the button. Note the knockout-related data-bind attributes. The text box is bound to the registeredMessage property and the click event of the button is bound to the newMessage function of the model, both defined in our results.js file.

Add the following HTML below the “register” section:

<div>
    <div data-bind="foreach:registeredMessageList">
        <div data-bind="text: $data"></div>
    </div>
</div>

This is a knockout foreach expression: for each message in the registeredMessageList array we print the message in a separate div. We bind the text property of the div to the message. The current value in the foreach loop can be accessed using “$data” in knockout.

Let’s go through the expected process step by step:

  1. The user enters a message in the text box and presses the register message button
  2. The button invokes the newMessage function of the knockout view-model ‘messageModel’
  3. The newMessage function will invoke the SendMessage(string input) method of the ResultsHub.cs Hub
  4. The SendMessage function constructs some return message and calls the registerMessage JavaScript function on each client, in our case defined in results.js
  5. The regiesterMessage function receives the string returned by the SendMessage function of ResultsHub
  6. regiesterMessage adds the message to the messages array of the view-model
  7. The view-model is updated and knockout magically updates the screen with the new list of messages

Set a breakpoint within SendMessage of ResultsHub.cs. Start the application in Chrome and press F12 to open the developer tools. You should see some SignalR related messages in the log:

Connecting to WebSockets in Devtools

It has found the ResultsHub and managed to open the web socket endpoint. Note the protocol of ‘ws’. Now insert a message in the text box and press register. Code execution should stop at the breakpoint in Visual Studio meaning that we’ve managed to wire up the components correctly. I encourage you to inspect the Context property and see what’s available in it. You should then see your message under the textbox as returned by the SendMessage:

Message collection by SignalR

In the meantime the DevTools log should fill up with interesting messages such as:

  • SignalR: Invoking resultshub.SendMessage
  • SignalR: Triggering client hub event ‘registerMessage’ on hub ‘ResultsHub’.
  • SignalR: webSockets reconnecting.

You’ll see this third message if you stop at the breakpoint to check out the available properties. As this artificially slows down the response time the web sockets connection is lost and then re-opened.

If you want to talk to yourself on two different screens then open up another browser window, Firefox or IE, and navigate to the same localhost address as in Chrome. Then start sending messages. You should see them in both windows:

Messages in two browser windows

So a message sent from one browser is immediately propagated to all listeners by SignalR with minimal code from the developer.

Read the next post in this series here.

View the list of posts on Messaging here.

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.

%d bloggers like this: