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.

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