Integrating Live Web (AspNet Signal-R) in a Web Site - Part 4 (the finale)

 

In This Post

This will be the final post of the AspNet SignalR series.  In this post, we will create a controller class that can be used to send messages from a REST WebApi to clients connected to our SignalR hub (via our default.html web page).

Looking Back

In the first three posts of this series (links below), we created an AspNet empty web site with support for SignalR and WebApi.  When the default.html page opens, the user is automatically joined to a Hub group called All.  The hub responds by returning a list of all users currently joined to the All group and the web page displays that list.  We can enter messages into a text box and see those messages displayed in a simple DIV - like a chat application.

Now it's time to look ahead....

Create a New Controller Class

Open your SignalRProjects solution in Visual Studio and right click the the Controllers folder under the MySignalRWebSite project (if you used different names, proceed accordingly).  In the right click context menu, select Add >> Web Api Controller Class (v 2.1).  Name the class SignalRController.cs.

Controller Class Code

Here is the code for the new controller class:

 using System;  
 using System.Collections.Generic;  
 using System.Web.Http;  
 using System.Configuration;  
 using Microsoft.AspNet.SignalR;  
 using Newtonsoft.Json;  
 namespace MySignalRWebSite.Controllers  
 {  
   public class SignalRController : ApiController  
   {  
     [HttpPost]  
     [AllowAnonymous]  
     public void PostHubMessage(HubMessage msg)   
     {  
       //this is the object the client web page is expecting  
       var sendMsg = new  
       {  
         groupName = msg.MessageToGroup,  
         userName = msg.MessageFrom,  
         message = msg.Message  
       };  
       //create the connection to the hub  
       IHubContext context = GlobalHost.ConnectionManager.GetHubContext<SignalRHub>();  
       //send the message by calling the client proxy method for all users connected to the group  
       context.Clients.Group(msg.MessageToGroup).processChatMessage(sendMsg);  
     }  
   }  
   public class HubMessage  
   {  
     public string Message { get; set; }   
     public string MessageFrom { get; set; }   
     public string MessageToGroup { get; set; }  
   }  
 }  


You'll notice above that I have, for clarity, deleted the out-of-box template functions from the new class - you don't need to do that if you don't want to, but they are not needed for our purposes.

In place of those methods, we have added the PostHubMessage method.  We have also added a simple data class (HubMessage) that defines the payload expected by the action whenever it is called.

There are 3 simple steps here:

  • Populate a new object from the incoming data
  • Connect to our SignalR Hub
  • Use the connection to call the client side processChatMessage proxy function
    • The client proxy function will call a local JavaScript function that will in turn display the message
Testing the Api

We're going to test our Api using Postman - a great tool for this purpose.  If you have other ways of testing, have at it!

If you don't have Postman and want to use it to test, download it from


Once downloaded and installed, running the test is easy.  First,  run the VS site in debug mode.  You may have to right click the default.html file and select Set Start Page from the menu.  The site will open a browser window with a URL address  something like 

https://localhost:12345/default.html

Next, open Postman and click File >> New Tab.  A new Tab will open looking like this


Change GET to POST. Copy the root of the URL from your browser and paste it into the Enter request URL textbox.


Add the following to the root URL you just copied

/api/SignalR/PostHubMessage

If you changed any of the naming suggested when we created the controller, you'll have to use your names here.

The last step before we test is to create a mock payload.  Immediately below the URL you just entered, you'll see a Body option - click that.  Click raw and select JSON from the select option that defaulted to Text.  Now we can compose the payload in JSON format like this
 {  
   "Message":"Test from the SignalR controller",  
   "MessageFrom": "SignalR Controller",  
   "MessageToGroup": "All"  
 }  

The screen should now look something like this



With your VS web site running, click the Send button.  If everything is set up correctly we should see the message from Postman work its way through the SignalR controller and on to the client!

If this didn't work, go back through the SignalR controller and make sure that the code matches, allowing for any naming differences you may have implemented.

Full Demo



Wrap Up

In this series, we covered a lot of ground but at a very simplified level.
  • Created a New Solution/Project in Visual Studio
    • AspNet Framework, Empty, WebApi
  • Downloaded the necessary NuGet packages to support desired functionality
  • Created C# classed for to support our SignalR hub
    • Startup.cs
    • SignalRHub.cs
  • Created an HTML page (default.html) with JavaScript to interact with the SignalR Hub
  • Created a C# SignalR controller (SignalRController.cs) to interact with connected users
  • Tested the API with Postman
At the end of this series, you should have a functioning SignalR-ready web site (however simple) that can allows communication between users and external clients through the Hub or the API.

Series Links




HTML Table with Fixed Headers using CSS

 



This is a pretty simple solution and probably doesn't cover all sorts of use cases - but I've found it effective for most of things I do.

First, I finally took the time to research this and figure out a model for my needs because I was about to gin up pagination for a table that might have anywhere from 0 to 50 rows.  After a deep sigh, I decided it would be more effective to just have a simple scrollable table.  At this point, let me just rant a little that a table with fixed column headings, a fixed footer and a scrollable body (rows) just SEEMS like an obvious thing.  No idea why the HTML table element doesn't support this natively.

The HTML Table

   <div class="row" style="border:1px solid lightgrey;padding-top:10px;max-height:500px">  
     <div class="col">  
       <div style="overflow-y:auto; max-height:350px;">  
         <table id="tblUserListing" style="width:100%;display:none;margin:auto;">  
           <thead>  
             <tr>  
               <th>First Name</th>  
               <th>Last Name</th>  
               <th>Login</th>  
               <th>Email</th>  
               <th>Last Login</th>  
               <th>Login Count</th>  
               <th>Status</th>  
               <th>&nbsp;</th>  
             </tr>  
           </thead>  
           <tbody id="tblUserListingBody" style="font-size:90%">  
           </tbody>  
         </table>  
       </div>  
       <div id="tblUserListingLoading" style="text-align:center;padding-top:20px;padding-bottom:20px">  
         <div class="spinner-border text-dark" role="status">  
           <div class="visually-hidden">loading.....</div>  
         </div>  
       </div>  
     </div>  
   </div>  

This table is in the context of a lot of other stuff going on with a page that is styled by Bootstrap.  As you can see, the table also doesn't have any rows in the tbody section - rows are added programmatically.  However....

The DIV enclosing the table needs to support vertical scrolling and height and/or max-height.  I chose a max-height so that any enclosing borders allow the table to assume its natural height up to a point... and then scroll.  When the table doesn't have enough rows to fill 350px, there is no scroll bar and a properly sized border. In my case, the border is actually on a DIV a couple levels up.

The CSS

     thead th {  
       font-weight:400;  
       padding-left:20px;  
       position:sticky;  
       top:0;  
       background-color:whitesmoke  
     }  

This will apply to any table with thead and th elements.  The critical pieces here are position: sticky; top: 0; background-color:whitesmoke.  This sticks the column headers to the top of the parent DIV.  The background-color is needed so that the background-color of the rows scrolling up don't bleed into the headers.

Notes

One of the great things about this approach (other than being dead simple), is that it allows the table to flow the column widths as needed based on the content of the td elements AND the headers and column data will stay aligned.

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

 



Introduction

Hopefully, you've followed the steps in Part 1 and Part 2 to create a project, get the necessary NuGet packages installed and generate the Startup.cs and SignalRHub.cs class files.  If so, we're ready to create a web page that connects to the Hub.

Default.html

Let's create a web page!  Right click the Project in the Solution Explorer and select Add > HTML Page.  Enter the name default.html and click OK. This will add the file to the Solution Explorer and open the file in the VS Editor. Visual Studio will have added some basic structural code for you.

The Whole Nine Yards

Rather than try to step through snippets of the default.html page, the entire content is shown below.... under that will be an explanation of each piece.

 <!DOCTYPE html>  
 <html>  
 <head>  
   <meta charset="utf-8" />  
   <title></title>  
   <link href="Content/bootstrap.min.css" rel="stylesheet" />  
   <script src="Scripts/bootstrap.bundle.min.js"></script>  
   <script src="Scripts/jquery-1.6.4.min.js"></script>  
   <script src="Scripts/jquery.signalR-2.4.3.min.js"></script>  
   <script src="signalr/hubs"></script>  
   <script>  
     var hubConnection;  
     //wait until the page is fully loaded  
     document.addEventListener("DOMContentLoaded", function () {  
       initPage();  
     });  
     //listen for the page unloading  
     window.addEventListener("beforeunload", function (e) {  
       hubLeaveGroup();  
     });  
     //init the page/hub connection  
     function initPage() {  
       hubConnection = $.connection.SignalRHub;  
       hubConnection.client.processChatMessage = function (message) { hubProcessMessage(message) };  
       hubConnection.client.updateAttendance = function (userList) { hubUpdateAttendance(userList) };  
       //start a hub session  
       $.connection.hub.start()  
         .done(function () {  
           //execute the local function to join the All group  
           hubJoinGroup();  
         })  
         .fail(function () {  
           alert("hub failed to start!");  
         });  
     }  
     //resolved hub visible function - see above  
     function hubProcessMessage(message) {  
       //example of the hub sending an object directly... no JSON.parse required  
       document.getElementById("messagesFromHub").innerHTML += message.userName + " says <br/>" + message.message + "<br/><br/>";  
     }  
     //resolved hub visible function - see above  
     function hubUpdateAttendance(data) {  
       //example of the hub sending the array of objects as a serialized JSON string  
       //parse it first, then process it  
       var userList = JSON.parse(data);  
       document.getElementById("hubAttendance").innerHTML = "";  
       //roll through the list and output the logged in users  
       userList.forEach(user => {  
         var userLine = `${user.UserName}: [${user.userConnectionId}]<br/>`;  
         document.getElementById("hubAttendance").innerHTML += userLine;  
       });  
     }  
     //local function  
     function hubJoinGroup() {  
       hubConnection.server.JoinGroup("All", "Robert Crossings")  
         .done(function () { })  
         .fail(function () { });  
     }  
     //local function  
     function hubLeaveGroup() {  
       //note the event listener above... this is executed when the user navigates away from the page  
       //this will remove the current user from the All group  
       //and execute the client visible hubUpdateAttendance function above  
       hubConnection.server.LeaveGroup("All")  
         .done(function () { })  
         .fail(function () { });  
     }  
     //local function  
     function hubSendMessage() {  
       var msg = document.getElementById("hubMsg").value;  
       //this sends the message data to the hub... the hub will turn around  
       //and execute the client visible hubProcessMessage function above  
       hubConnection.server.SendMessage("All", msg, "Robert Crossings")  
         .done(function () { })  
         .fail(function () { });  
     }  
   </script>  
 </head>  
 <body>  
   <div class="container">  
     <div class="row">  
       <div class="col">  
         <h1>My SignalR Web Site</h1>  
       </div>  
     </div>  
   </div>  
   <div class="row">  
     <div class="col-6">  
       <div class="container-fluid">  
         <div class="row" style="margin-bottom:10px">  
           <div class="col-3">  
             Enter Message  
           </div>  
           <div class="col-9">  
             <input type="text" id="hubMsg" onchange="javascript:hubSendMessage()" style="width:100%" />  
           </div>  
         </div>  
         <div class="row">  
           <div class="col" id="messagesFromHub" style="border:1px solid darkgrey;min-height:300px"></div>  
         </div>  
       </div>  
     </div>  
     <div class="col-6">  
       <div class="container-fluid">  
         <div class="row" style="margin-bottom:10px">  
           <div class="col">Attendance</div>  
         </div>  
         <div class="row">  
           <div class="col" id="hubAttendance" style="border:1px solid darkgrey;min-height:300px"></div>  
         </div>  
       </div>  
     </div>  
   </div>  
 </body>  
 </html>  


Bootstrap

I've gotten used to the simplicity of Bootstrap....  notice the references in the <head> section: 1 for the CSS and 1 for a supporting JS file (the JS file wasn't really needed for this page - creature of habit).  If you want to reproduce this page, simply open the NuGet Package Manager and search for Bootstrap.

jQuery

Also in the <head> section are the 3 SignalR JS script references.  Notice that for this blog, I did not update the jQuery version from 1.6.4.  Normally, I do update that and change the script reference to align with the version I'm using.  

signalr/hubs

There's also an odd reference to signalr/hubs.  This is a reference to a virtual file that gets created on the fly by the SignalR system because of what we did in the startup.cs file.

The Script Block

I've commented this as clearly as I can.  There are some ideas to note:

In the initPage function, the first thing we do is initialize the SignalRHub. The local reference to the hub is always $.connection

Next, we declare 2 functions that are visible to the Hub.  As shown, these proxy functions in turn call a local function.  The Hub can only call client functions declared like this.  For the purposes of this blog, we only needed 2 functions, but you can have as many as you need to provide the functionality desired.

After declaring the proxy functions, we then start the hub with $.connection.hub.start().  This is the command that gives the current user their unique Connection Context ID.  Every subsequent call to the server automatically has that unique value.

 Five Local Functions

hubProcessMessage: Called by the client proxy function processChatMessage. In this case, the Hub sends an object that contains userName, message and groupName.  Because the Hub is sending the object directly (rather than serialized), we can process it directly - no JSON.parse required.

hubUpdateAttendance: Called by the client proxy function updateAttendance. The Hub sends an array of objects as a string - so have to JSON.parse it prior to processing.

hubJoinGroup: Called immediately after we start() the Hub in order to assure that the current user joins at least one group (in this case, the All group).  On the Hub side, the user is joined to the group and added to a list of logged in users.  That list is then sent back to clients joined to the All group by way of the updateAttendance client proxy function.

hubLeaveGroup: Called when the beforeunload window event is fired (when the user navigates away from the page or closes the browser).  This executes the Hub's LeaveGroup method which removes the user from the All group and the userList list.  The Hub then calls the updateAttendance client proxy function for any users still logged in.

hubSendMessage: Called when the user enters a message in the textbox and presses the enter key (for simplicity).  This function calls the Hub's SendMessage method.  SendMessage turns around and calls the client's processChatMessage proxy function which displays the message on the screen for any users joined to the All group on the Hub.

The HTML Block

Other than the implementation of Bootstrap, the HTML for this example is quite simple.  

  • A textbox to enter/send messages
  • A div to display messages
  • A div to display attendance
What's Next?

The last installment in this series will be to create a simple API that can receive and incoming payload of message data and forward that message to connected users.  Go to Part 4.

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...