Integrating Live Web (AspNet Signal-R) in a Web Site - Part 2

 


In this Post

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.



When prompted, enter the name Startup.cs and click the Add button.  As shown above, VS will create a new class file and add it to the Project in the Solution Explorer and open it as a new tab in the editor.  This class only requires one using statement and one method.  Without changing your namespace line, update the class to match this:

 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






Integrating Live Web (AspNet Signal-R) in a Web Site - Part 1

 


In This Post

I've broken this topic into a few posts because there are several steps to successfully get up and running with Signal-R in a Visual Studio/AspNet Signal-R implementation.

In this post I'll be covering how to create the VS Solution/Project and importing the needed NuGet packages.

Introduction - Signal-R 101

I have a project that I want to integrate some elements of Live Web (Signal-R).  Signal-R is a technology that allows a client application (desktop, web, mobile) to  connect to server based Hub.  Once connected, the client can call methods on the Hub and the Hub can call methods on the client.  I guess in the old days we called this RPC (remote procedure calls).  The most typical implementation of Signal-R is to support live chat. In a live chat scenario, the parties connect to a common group on the Hub and then send messages (by calling a Hub method).  The Hub receives the message and then calls a client method for clients connected to the common group.  The client method handles the Hub call by displaying the message on the chat screen.  

Another common use of Signal-R is on-line gaming.  Game participants connect to a common group on the Hub and methods on the Hub and client allow them to experience the game together in real time.

Scope

I won't be going into the details of how Signal-R works (heck, I don't even know a lot of it).  I will instead be focused on how to implement the technology in a Visual Studio (v. 2022) web site running only HTML 5, CSS, JavaScript and C# with .Net WebApi 2.1 Controllers.  The target deployment for my application is Azure App Service - but the principles I'll be showing are generic enough to translate to a broad range of deployments.

Application Architecture

"Architecture" might be overstating it.  I do these projects, in large part, to force myself to learn and keep up with technology changes in my corner of the technology world (mostly, Microsoft stack).  NOTHING about the code you'll see or the approaches taken would reflect a production implementation.... 

Let's Get Started - Create a new Project in Visual Studio

As mentioned, I'm using VS 2022.  The screen shots you'll see are from that version.

First, create a new Project by opening VS and clicking the "Create a new project" button under "Get Started"


On the Template dialog, set the Languages drop down to C# and the Project Type drop down to Web.  There are a lot of choices, but if you want to follow along with the Blog, scroll down and double-click the ASP.NET Web Application (.Net Framework) template.


This brings you to the Configuration dialog. Here, you'll want to enter a Project Name (maybe something like MySignalRWebSite) and Solution Name (maybe SignalRProjects).  You can also designate a Location of your choice and the Framework version (my project is 4.7.2).  Click "Create" when you're ready.

The final dialog offers choices for building out the infrastructure to support various technologies.  On this dialog, select Empty and then check the Web Api option.  One of the things I wanted to experiment with on this project is "rolling my own" Single Page Application (SPA).  There are some great frameworks out there for accomplishing this (Angular, for example), but I wanted to see what it takes to create an SPA without an additional framework.  Click Create when you're ready.


Visual Studio will create the new project based on the choices you've made.  You should see an Overview tab like this


And the Solution Explorer should look like this


As you can see, Visual Studio built out the infrastructure needed to support WebApi 2.1 Controllers as well as other basics.

NuGet Packages

In order to support Signal-R, we'll have to import some NuGet Packages.  In the Solution Explorer, right click your Project and select Manage NuGet Packages...  The Package Manager defaults to show you the installed packages.  Click the Browse option in the upper left of the dialog and in the search bar enter Microsoft.AspNet.SignalR.  When the list filters, select Microsoft.AspNet.SignalR  and select Install.  

Installing Microsoft.AspNet.SignalR will likely also install the following required packages.  Check your list by clicking the Installed button and clearing the search box of any data.  

  • jQuery
  • Microsoft.AspNet.SignalR
  • Microsoft.AspNet.SignalR.Core
  • Microsoft.AspNet.SignalR.JS
  • Microsoft.AspNet.SignalR.SystemWeb
  • Microsoft.Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security
  • Newtonsoft.Json
  • Owin
If you are missing any of these, you can install them individually by going back to the Browse section and searching for them.

Wrap up Part 1

That should do it!  In Part 2 of this series we'll take care of some application start-up configuration and the creation of the SignalR hub that our site will communicate with.

Stacking Dynamically Created Bootstrap Toast Notifications


A project I'm currently working on implements the Bootstrap 5 CSS framework.  One of the things i want to do is pop toast notifications to let the user know, in a subtle way, the result of actions that they take - a pretty common thing these days.

Bootstrap has a nice Toast implementation but it expects the toast HTML structure to be built and then lets you manage that via JavaScript.  BUT, what if you want something a little more flexible and sophisticated?  Maybe the notifications will sometimes come several at a time and you'd like to stack them.

Bootstrap has a solution for that too - but again, it expects the HTML structure of multiple toast sections to be built and in place.  

What I really wanted was to be able to dynamically create the toast structure, insert it into the DOM and, if applicable, have them stack nicely rather than overlap.

Here's what I ended up with, and it works pretty nicely.

The HTML for the Toast Container looks like this:

     <!--toast messaging-->  
     <div class="toast-container" id="toastContainer"></div>  

The Javascript

   popToast: function (bodyText) {  
     //when we pop a toast notification  
     //we are dynamically creating the toast element and adding it to the default.html toastContainer  
     //we also try to figure out how to stack them b/c bootstrap doesn't do that - even if placed in a toast-container div  
   
     //get the container and count/calculate the bottom placement of the toast about to be added  
     var container = document.getElementById("toastContainer");  
     var containerCount = container.childElementCount;  
     var noticePlacement = containerCount === 0 ? "5px" : (containerCount * 105) + "px";  
   
     //the static template  
     var toastTemplate = `<div style="z-index: 9999;width:400px;${noticePlacement};position:fixed;margin-left:40%;margin-right:40%" class="toast" role="alert" aria-live="assertive" data-bs-delay="4000" aria-atomic="true">  
         <div class="toast-header" style="background-color:skyblue">  
           <strong class="me-auto" style="color:white">iQueue</strong>  
           <small style="color:white">moments ago</small>  
           <button type="button" class="btn-close" data-bs-dismiss="toast" style="color:white" aria-label="Close"></button>  
         </div>  
         <div class="toast-body" id="divMainToast">  
           ${bodyText}  
         </div>  
       </div>`;  
   
     //create the template and toast elements/add to container  
     var newToastTemplate = document.createElement("template");  
     newToastTemplate.innerHTML = toastTemplate.trim();  
     var newToast = newToastTemplate.content.firstChild;  
     document.getElementById("toastContainer").appendChild(newToast);  
   
     //create/show the bs toast object from the dynamically created toast element  
     var bsToast = new bootstrap.Toast(newToast);  
     bsToast.show();  
       
     //remove this toast element from the container when it fires the hidden event  
     newToast.addEventListener("hidden.bs.toast", function () {  
       document.getElementById("toastContainer").removeChild(newToast);  
     })  
   },  

Notes

In a nutshell, when popToast() is called, it adds a bootstrap toast structure to the container.  The toastTemplate string pulls in the values for the body of the toast and the placement needed to stack.  Placement is a calculation based on the number of existing notifications - to get a stacked effect.

I did a little playing with the standard format of the toast (made it wider, used my own color scheme, etc).

It's important to remember to remove the toast from the container when its "hidden" event is fired - that's what the addEventListener() does at the end.  This keeps the container child count accurate.

There are probably much better ways to accomplish this - but this approach does what i need it to do with little extraneous coding required.

Microsoft Power Pages - Importing JS Libraries

  INTRODUCTION Microsoft Power Pages is a platform for developing web sites - from very simple... to surprisingly complex.  However, with ad...