Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Real Time Web Solution for Chat by MVC SignalR Hub

0.00/5 (No votes)
3 Apr 2019 62  
This article explains the SignalR concept via chat implementation and interaction between client and server visually and involving hub class.

Introduction

Nowadays, due to increase in the amount of information and necessity of achieving data in short time, we need technologies to cover our requirement in this issue. Assume when in stock market prices are changing each moment, do you think that user should refresh page every moment to inform the last price? Obviously, it is not a reasonable solution for such a problem. Or with increase in producing products and services, we need customer service to help user and buyer, the best and cheaper communication is conversation by chat program. By the same token, we cannot force user to press button for receiving our last message.

SignalR is a real time technology which is using the set of asynchrony library to make a persistence connection between client and server. User can receive last update data from server without the traditional way such as refresh page or press button.

Background

You need to know MVC 4.0 Technology and EntityFramework > 4.0 to get this article better.

In the other hand, SignalR uses the below approaches to establish real time web:

1. WebSocket

Websocket is a full duplex protocol and uses http handshaking internally and allow stream of messages flow on top of TCP. It supports: Google Chrome (> 16) Fire Fox (> 11) IE (> 10) Win IIS (>8.0). Due to encrypt message and full duplex, websocket is the best solution and at first signalR checks both web server and client server whether they support websocket or not.

Simplex Communication

It just spreads in one way when one point just broadcasts while another point just can listen without sending message, such as television and radio.

Half Duplex

One point sends message and at this moment, another point cannot send message and should wait until the first point finishes its transmission and then send its message, it is just a one communication line at a time, such as old wireless device walkie-talkie and HTTP protocol.

Full Duplex

Both points can send and receive message at a time simultaneously, there is no need to wait until the other point finishes its transmission such as telephones and websocket protocol.

Full Duplex

2. Server Sent Events (SSE)

The next choice for signalr is server sent event, because of persistence communication between server and client. In this approach, communication does not disconnect and last data from server will update automatically and transmit to client via HTTP connection. EventSource is part of HTML5 technology.

var evsrc = new EventSource("url");
       // Load and Register Event Handler for Messages in this section

       evsrc.addEventListener("message", function (event) {
           //processing data in this section
       });

3. Forever Frame

When client sends request to server, then server sends a hidden iframe as chunked block to client so this iframe is responsible to keep connection between client and server forever. Whenever server changes data, then send data as script tag to client (hidden iframe) and these scripts will be received sequentially.

4. Polling

Client sends request to server and server responses immediately but after that, server disconnects connection so again for establishing communication between server and client, we should wait for next request from client. To solve this problem, we have to set timeout manually and for each 10 seconds client sends request to server to check new modification in server side and gets last update data. Polling uses resources and it is not an economic solution.

5. Long Polling

Client sends request to server and server responds immediately and this connection remains until a specific time and during this period clients do not have to send explicit request to server while in polling client has to send explicit request to server during timeout. Comet programming covers this concept.

Briefly, SignalR library chooses one type of transmit data between client and server, its priority is websocket, server sent event, long polling and forever iframe. There are two classes inside this library as follows:

1. Persistentconnection

It is low level so it is complex and needs more configuration but in return gives more facility to handle class personally.

2. Hub

It is high level and more popular to use it.

How to Implement Simple Chat Scenario With the Aid of Signalr and Hub Class?

My aim is just to issue a random scenario for involving signalr. You can use it for your personal scenario and I just follow the below steps to make challenge with server (hub class) and client side and illustrate how client send request and server respond? How they interact with each other?

Scenario Description

I want to establish an application for customer service department. There are some administrations that are responsible for helping the client and on the other side, there are clients who ask question and need help.

Assume two admins are online and connect to chat service and the first client comes to ask a question, so system connects the first client to first free admin and for the second client, this story will repeat, but the third client gives alarm from system that there is no admin to help. Whenever the first client disconnects, the first admin becomes free.

My contract for this scenario is to use flag for reminding which user who is connected is user or admin and which one is free or busy. In my database, if admincode is equal to zero so it is user otherwise it is admin, and I define flag “tpflag” (in application) is equal to zero for user and equal to one for admin. Whenever they connect to chat flag, “freeflag” becomes zero which shows busy user and as soon as client leaves conversation becomes one which shows free status.

if freeflag==0 ==> Busy
if freeflag==1 ==> Free
if tpflag==0 ==> User
if tpflag==1 ==> Admin
Prerequisites
  1. Visual Studio 2012
  2. SQL Server 2008
  3. Install necessary dependency from package manager console

Step 1: Create Project

File --> New Project --> ASP.NET MVC 4 Web Application { Give Name and Directory} --> { Template=Basic & View Engine=Razor }

Step 2: Open PM for Installing Dependency Files

Menu (Tools) --> Library Package Manager --> Package Manager Console

Step 3: Instruction for Removing Old Dependency

At first, remove all of the old dependencies for installing new version of SignalR 2.x.x In Line:

PM> Uninstall-Package Microsoft.AspNet.SignalR –RemoveDependencies

Step 4: Instruction for Installing Necessary Dependency Files

For new version, use:

PM> Install-Package Microsoft.AspNet.SignalR

I have used signalr version 2.0.1 for this practice:

PM> Install-Package Microsoft.AspNet.SignalR -Version 2.0.1

PM> Install-Package Microsoft.Owin

By writing this instruction, nuget does all of the dependency injection that you need to run signalr. If you look at the Reference part in the solution, you will find Microsoft.ASPNet.SignalR.x, Microsoft.Owin.x.x.., etc., or if you look at the Scripts part in solution jquery-1.x , jquery.signalR 2.x.x, etc. so feel comfortable about all of the dependencies.

Solution --> Open Reference -->

Solution --> Scripts -->

On the other hand, after installing signalR dependencies successfully, you will find complete help as readme.txt above the package console. It contains all the necessary instructions to get started with signalr. I explain these instructions in the next steps.

Tips (1): NuGet

If you encounter this error “The remote name could not be resolved: 'www.nuget.org'” So you should change Package Manager Settings which is located in front of the Package Source.

You should change the source from https to http protocol to solve this problem.

Tips (2): Owin

Check your references part to be sure there is Owin, otherwise follow this instruction: Right Click on References --> Manage NuGet Packages --> Select Online in left side --> search Owin --> Select Owin (Owin IAppBuilder startup interface) --> Install.

Then, you should see Owin in your reference part.

Step 5: Startup Class

For enabling signalr in project, you should create class as startup. (If in the previous version of signalr, I mean the first version, you used to write RouteTable.Routes.MapHubs(); in Application Start in global.asax, now forget about it and just use startup class. Right Click: On Project Name {SignalR} --> Add Class --> Name: Startup.cs

using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(MvcSignal.Startup))]
namespace MvcSignal
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

Step 6: Organize Database According To Our Scenario

Step 6.1: Create “tbl_User”

tbl_user” will collect user and admin, if “AdminCode” was filled by number from previous table so it is admin who belongs to department otherwise if it was filled by (zero) illustrate to ordinary user.{ “UserIDint + identity=yes and “AdminCode” default value = 0 }

Step 6.2: Create “tbl_Conversation”

tbl_Conversation” which will collect data from conversation between user and admin. This table will be filled after finishing conversation. { “ConID” int + identity=yes }

Step 7: Create Hub Class

Step 7.1: Model (folder) --> Create class “UserInfo.cs”

public class UserInfo
    {
        public string ConnectionId { get; set; }
        public string UserName { get; set; }
        public string UserGroup { get; set; }

        //if freeflag==0 ==> Busy
        //if freeflag==1 ==> Free
        public string freeflag { get; set; }

        //if tpflag==2 ==> User Admin
        //if tpflag==0 ==> User Member
        //if tpflag==1 ==> Admin

        public string tpflag { get; set; }

        public int UserID { get; set; }
        public int AdminID { get; set; } 
    }

Step 7.2: Model (folder) --> Create class “MessageInfo.cs”

public class MessageInfo
  {
      public string UserName { get; set; }

      public string Message { get; set; }

      public string UserGroup { get; set; }

      public string StartTime { get; set; }

      public string EndTime { get; set; }

      public string MsgDate { get; set; }
  }

Step 7.3: Controller (folder) ? Create “HomeController.cs”

public ActionResult Chat()
   {
       ViewBag.Message = "Your contact page.";

       return View();
   }

Right click on Chat() --> Select Add View -->

Step 7.4: Create Chat.cshtml { Client Side }

@{
    ViewBag.Title = "Chat";
}

<div id="divLogin" class="mylogin">

    User Name:<input id="txtUserName" type="text" /><br />
       Password :   <input id="txtPassword" type="password" /><br />
    <input id="btnLogin" type="button" value="Login" />
    <div id="divalarm"></div>
</div>

<div id="divChat" class="mylogin">

<div id="welcome"></div><br />
<input id="txtMessage" type="text" />
<input id="btnSendMessage" type="button" value="Send" />
<div id="divMessage"></div>

</div>

    <input id="hUserId" type="hidden" />
    <input id="hId" type="hidden" />
    <input id="hUserName" type="hidden" />
    <input id="hGroup" type="hidden" />

@section scripts {
   
    <script src="~/Scripts/jquery-1.8.2.min.js"></script>
    <script src="~/Scripts/jquery.signalR-2.0.1.min.js" type="text/javascript"></script>
    <script src="~/signalr/hubs" type="text/javascript"></script>
    @*<script type="text/javascript" src="@Url.Content("~/signalr/hubs")"></script>*@
    @* <script type="text/javascript" 

       src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>*@
   
   <script>
       $(function () { //This section will run whenever we call Chat.cshtml page

           $("#divChat").hide();
           $("#divLogin").show();

           var objHub = $.connection.myHub;

           loadClientMethods(objHub);

           $.connection.hub.start().done(function () {

               loadEvents(objHub);

           });
       });

       function loadEvents(objHub) {

           $("#btnLogin").click(function () {

               var name = $("#txtUserName").val();
               var pass = $("#txtPassword").val();

               if (name.length > 0 && pass.length > 0) {
                   // <<<<<-- ***** Return to Server [  Connect  ] *****
                   objHub.server.connect(name, pass);

               }
               else {
                   alert("Please Insert UserName and Password");
               }

           });

           $('#btnSendMessage').click(function () {

               var msg = $("#txtMessage").val();

               if (msg.length > 0) {

                   var userName = $('#hUserName').val();
                   // <<<<<-- ***** Return to Server [  SendMessageToGroup  ] *****
                   objHub.server.sendMessageToGroup(userName, msg);

               }
           });

           $("#txtPassword").keypress(function (e) {
               if (e.which == 13) {
                   $("#btnLogin").click();
               }
           });

           $("#txtMessage").keypress(function (e) {
               if (e.which == 13) {
                   $('#btnSendMessage').click();
               }
           });
       }

       function loadClientMethods(objHub) {

           objHub.client.NoExistAdmin = function () {
               var divNoExist = $('<div><p>There is no Admin 
                                   to response you try again later</P></div>');
               $("#divChat").hide();
               $("#divLogin").show();

               $(divNoExist).hide();
               $('#divalarm').prepend(divNoExist);
               $(divNoExist).fadeIn(900).delay(9000).fadeOut(900);
           }

           objHub.client.getMessages = function (userName, message) {

               $("#txtMessage").val('');
               $('#divMessage').append('<div><p>' + 
               userName + ': ' + message + '</p></div>');

               var height = $('#divMessage')[0].scrollHeight;
               $('#divMessage').scrollTop(height);
           }

           objHub.client.onConnected = function (id, userName, UserID, userGroup) {

               var strWelcome = 'Welcome' + +userName;
               $('#welcome').append('<div><p>Welcome:' + 
               userName + '</p></div>');

               $('#hId').val(id);
               $('#hUserId').val(UserID);
               $('#hUserName').val(userName);
               $('#hGroup').val(userGroup);

               $("#divChat").show();
               $("#divLogin").hide();
           }
       }
    </script>
}

Step 7.5: Create Model1.edmx

To have a simple way to fetch and insert data from and to database, I create model as follows: Right click on project name --> Add New Item --> Select “ADO.NET Entity Data Model” --> Select “Generate From Data Base” --> Make Connection to your data base --> Select your tables.

Step 8: Create Folder and Name It Hubs Then Create Simple Class and Name It “MyHub.cs”

{If you have the last update version of Visual Studio, you can add new item and select “SignalR Hub Class”}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;
using MvcSignal.Models;
using Microsoft.AspNet.SignalR.Hubs;

namespace MvcSignal
{
    public class MyHub : Hub
    {
        static List UsersList = new List();
        static List<messageinfo> MessageList = new List<messageinfo>();

        //-->>>>> ***** Receive Request From Client [  Connect  ] *****
        public void Connect(string userName, string password)
        {
            var id = Context.ConnectionId;
            string userGroup="";
            //Manage Hub Class
            //if freeflag==0 ==> Busy
            //if freeflag==1 ==> Free

            //if tpflag==0 ==> User
            //if tpflag==1 ==> Admin


            var ctx = new TestEntities();

            var userInfo =
                 (from m in ctx.tbl_User
                  where m.UserName == userName && m.Password == password
                  select new { m.UserID, m.UserName, m.AdminCode }).FirstOrDefault();

            try
            {
                //You can check if user or admin did not login before 
                //by below line which is an if condition
                //if (UsersList.Count(x => x.ConnectionId == id) == 0)

                //Here you check if there is no userGroup which is 
                //same DepID --> this is User otherwise this is Admin
                //userGroup = DepID               
               
                if ((int)userInfo.AdminCode == 0)
                {
                    //now we encounter ordinary user which needs userGroup and at this step, 
                    //system assigns the first of free Admin among UsersList
                    var strg = (from s in UsersList where (s.tpflag == "1") 
                    && (s.freeflag == "1") select s).First();
                    userGroup = strg.UserGroup;

                    //Admin becomes busy so we assign zero to freeflag 
                    //which is shown admin is busy
                    strg.freeflag = "0";

                    //now add USER to UsersList
                    UsersList.Add(new UserInfo { ConnectionId = id, 
                                                 UserID = userInfo.UserID, 
                                                 UserName = userName, 
                                                 UserGroup = userGroup, 
                                                 freeflag = "0", 
                                                 tpflag = "0", });
                    //whether it is Admin or User now both of them has userGroup 
                    //and I Join this user or admin to specific group 
                    Groups.Add(Context.ConnectionId, userGroup);
                    Clients.Caller.onConnected(id, userName, userInfo.UserID, userGroup);
                }
                else
                {
                    //If user has admin code so admin code is same userGroup
                    //now add ADMIN to UsersList
                    UsersList.Add(new UserInfo { ConnectionId = id, 
                                                 AdminID = userInfo.UserID, 
                                                 UserName = userName, 
                                                 UserGroup = userInfo.AdminCode.ToString(), 
                                                 freeflag = "1", 
                                                 tpflag = "1" });
                    //whether it is Admin or User now both of them has userGroup and 
                    //I Join this user or admin to specific group 
                    Groups.Add(Context.ConnectionId, userInfo.AdminCode.ToString());
                    Clients.Caller.onConnected(id, userName, userInfo.UserID, 
                                               userInfo.AdminCode.ToString());
                }                       
            }

            catch
            {
                string msg = "All Administrators are busy, please be patient and try again";
                //***** Return to Client *****
                Clients.Caller.NoExistAdmin();
            }
        }
        // <<<<<-- ***** Return to Client [  NoExist  ] *****

        //--group ***** Receive Request From Client [  SendMessageToGroup  ] *****
        public void SendMessageToGroup(string userName, string message)
        {
            if (UsersList.Count != 0)
            {
                var strg = (from s in UsersList 
                            where (s.UserName == userName) select s).First();
                MessageList.Add(new MessageInfo 
                { UserName = userName, Message = message, UserGroup = strg.UserGroup });
                string strgroup = strg.UserGroup;
                // If you want to Broadcast message to all UsersList use below line
                // Clients.All.getMessages(userName, message);

                //If you want to establish peer to peer connection use below line 
                //so message will be send just for user and admin who are in same group
                //***** Return to Client *****
                Clients.Group(strgroup).getMessages(userName, message);
            }
        }
        // <<<<<-- ***** Return to Client [  getMessages  ] *****

        //--group ***** Receive Request From Client ***** 
        //{ Whenever User close session then OnDisconneced will be occurs }
        public override System.Threading.Tasks.Task OnDisconnected()
        {

            var item = UsersList.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);
            if (item != null)
            {
                UsersList.Remove(item);

                var id = Context.ConnectionId;

                if (item.tpflag == "0")
                {
                    //user logged off == user
                    try
                    {
                        var stradmin = (from s in UsersList where 
                        (s.UserGroup == item.UserGroup) && (s.tpflag == "1") select s).First();
                        //become free
                        stradmin.freeflag = "1";
                    }
                    catch
                    {
                        //***** Return to Client *****
                        Clients.Caller.NoExistAdmin();
                    }                    
                }

                //save conversation to dat abase
            }

            return base.OnDisconnected();
        }
    }
}

Rules and Contracts

The prefix when client wants to call server method in server side:

( Client --> Server ) // Client send request to server

1. objHub.server.methodname() { methodname of server side }

and there is exactly the same methodname in server side (myHub.cs class) .

The prefix when server wants to call client method in client side:

( Server --> Client ) // Server calls client method & { methodname of client side }

  1. Clients.caller.methodname() // caller means only user who sends request
  2. Clients.all.methodname() // all means all of connected user
  3. Clients.Group(groupName).methodname() // Group means just users who are in same group

When there is ***** Return to Client ***** in “MyHub.cs” class, it means you have to write jquery function with the same name on client side.

Indeed, their interaction is as follows:

Tips (3): Call Server Class

There are tiny tips whenever you want to call your server class; always in client side you should use specific naming convention which is camel type, for instance if your hub class name is “MyHub”, you should instantiate your object from “myHub” or if you have “SendMessageToGroup”, you should call it from “sendMessageToGroup” so it should be like:

Test Case

To have the same result, you should have database as same as I have explained in the seventh step.

Case 1

Test Plan: If Client tries to login and there is no admin, then system shows an alarm.

Testing Steps
  1. Run project
  2. UserName: mahsa
  3. Password: 123
  4. Expected Output: System shows alarm

Case 2

Test Plan: There is at least on free admin and then one client login and then the first admin will be assigned to the first free client who needs help.

Testing Steps

  1. Run project
  2. UserName: admin1
  3. Password: 123
  4. Login {admin1 as first admin}

  5. Copy URL to another web browser

  6. UserName: mahsa
  7. Password: 123

  8. Login {mahsa as first client}
  9. If “mahsa” send message, so “admin1” will see it, because they are in the same group. When the first client true to login, then add to the first free admin.

  10. Copy URL to another web browser
  11. UserName: kashi
  12. Password: 123

  13. Login { kashi as second client}
  14. System shows alarm and says “there is no admin then system shows an alarm”

  15. Copy URL to another web browser
  16. UserName: admin2
  17. Password: 123
  18. “kashi” and “admin2” cannot see conversation between “admin1” and “mahsa”

Tips

Tip 1. Different UI for Admin and User and Show Waiting User for Admin

If you need a different UI for Admin from User, you should create different div for user and admin with different CSS and assign particular CSS to them by class attribute.

When you want to send Admin Message to Admin from hub class, send to different client method such as objHub.client.getMessagesAdmin and for user objHub.client.getMessagesUser.

In Chat.cshtml, implement these methods with different UI by different div "divMessageAdmin" and "divMessageUser", you should fill these divs with the proper message.

So please follow:

  1. Create different divs:
    <div class="Admin" id="divMessageAdmin"></div>
    <div Class="User" id="divMessageUser"></div>
  2. In Hub class -> SendMessageToGroup ->

    Check if user is Admin ->:

    Clients.Group(strgroup).getMessagesAdmin(userName, message);

Check if user is Ordinary User->:

Clients.Group(strgroup).getMessagesUser(userName, message);
  1. In Chat.cshtml:

    If User is Admin:

        objHub.client.getMessagesAdmin = function (userName, message) {
    
        $("#txtMessage").val('');
        $('#divMessageAdmin').append('<div><p>' +
        userName + ': ' + message + '</p></div>');
    
        var height = $('#divMessageAdmin')[0].scrollHeight;
        $('#divMessageAdmin').scrollTop(height);
    }
    

    If User is Ordinary User:

    objHub.client.getMessagesUser = function (userName, message) {
        $("#txtMessage").val('');
        $('#getMessagesUser').append('<div><p>' +
        userName + ': ' + message + '</p></div>');
    
        var height = $('#getMessagesUser')[0].scrollHeight;
        $('#getMessagesUser').scrollTop(height);
    }
    

    To see waiting users in Admin's UI:

  2. Inside Hub class -> Connect -> you have these lines of code:
    catch
              {
                  string msg = "All Administrators are busy, please be patient and try again";
                  // Return to Client 
                  Clients.Caller.NoExistAdmin();
              }

    Please send username for this user who has to wait for free admin:

    Clients.Caller.NoExistAdmin(username);
  3. In Chat.cshtml:
    objHub.client.NoExistAdmin = function (username) {
    var divNoExist = $('

    There is no Admin to respond... you try again later:

    '); $("#divChat").hide(); $("#divLogin").show(); $("#divWaitingUser").append('
    ' + userName + '
    '); $(divNoExist).hide(); $('#divalarm').prepend(divNoExist); 
        $(divNoExist).fadeIn(900).delay(9000).fadeOut(900); }

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here