Feb
17
2013

One to one chat using Asp.net SignalR groups

Introduction

I have been experimenting a lot with SignalR these days. During this time, I tried to implement a fully functional chat client(web) with the help of SignalR.

I realized that many to many chat functionality is actually basic task with SignalR but it requires little tweak to make a basic one to one chat application.

In this short article, I will explain the one way to achieve it (If you know any other approach, please let us know in comment).

Background

I have seen a lot of people asking this questions in different asp.net related forums - How to make a one-one chat asp.net using SignalR. So, I thought of writing a full post to explain my method of doing it.

To filter the chat messages to particular pair of users, I would utilize the "Group" concept of SignalR.

Article Body

If you are completely new to the term "SignalR" then, I would suggest you to visit SignalR's page on GitHub.

In summary, SignalR is a Asp.net library which can be used to add real time functionality to your web page; including chatting, real time notifications of database or server operations and many more. You can check my old article to see the scenarios where SignalR can come to your rescue.

Lets start with the classic example of open public chat room and then we will move on to convert the same chat application for one-to-one communication.

First of all, we need to setup the basic SignalR enabled asp.net project. SignalR is part of official Visual Studio 2012.2 release. If you are using older version of Visual Studio then, you can get SignalR via Nuget.

In the package manager console, simply type following command and run

PM> Install-Package Microsoft.AspNet.SignalR

It will install SignalR and other required files in your project.

Next step - Add necessary configuration in Global.asax. Go to Application_Start event of Global.asax and add following line which is essential for SignalR to work

using System.Web.Routing;
using Microsoft.AspNet.SignalR;

//Inside Global class
public void Application_Start()
{
      RouteTable.Routes.MapHubs();
}

Now, In the root of web application, add a new class file named "chat.cs"

The class Chat of chat.cs would inherit from  Hub class and there would be only one method defined which will be called from JavaScript client. The complete class would look like this

using Microsoft.AspNet.SignalR;
using System.Web;

    [HubName("chatHub")]
    public class Chat : Hub
    {
        public void Send(string message)
        {
            if (Clients != null)
            {
                // Call the addMessage method on all clients
                Clients.All.addMessage(message);
            }
        }
   }

This completes the configuration on server side. Moving to client side code, create a new  aspx page and name it as Chat.aspx

From JavaScript code of chat.aspx, call a server side hub method "Send" and pass message string as a parameter. Also, create a separate method named "send" which will be in return called from hub.

The complete code of chat.aspx is as below

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
    <link rel="Stylesheet" href="Styles/Site.css" />
    <script src="Scripts/jquery-1.6.4.js" type="text/javascript"></script>
    <script src="Scripts/json2.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.0-rc2.min.js" type="text/javascript"></script>
    <script src="signalr/hubs" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {

            // Proxy created on the fly
            var chat = $.connection.chatHub;

            // Declare a function on the chat hub so the server can invoke it
            chat.client.addMessage = function (message) {
                $('#divChatWindow').find('ul').append('<LI>' + message + '');
            };
            $("#broadcast").click(function () {
                // Call the chat method on the server
                chat.server.send($('#msg').val());
            });
            // Start the connection
            $.connection.hub.start();
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">

        <div id="divChatWindow" style="border: 1px solid #3366CC; float: left; width: 365px; display: none; margin-right: 10px">
            <div style="border: 1 solid blue;">
                <ul id="messages" style="width: 465px; height: 100px; font: normal 2 verdana"></ul>
            </div>
            <hr />
            <asp:TextBox runat="server" ID="msg" CssClass="ChatText" Width="280px" BorderColor="#666699"
                BorderStyle="Solid" BorderWidth="1px" Height="22px"></asp:TextBox>
            <asp:Button ID="btnChatSend" runat="server" CssClass="ChatSend" Text="Click me" OnClientClick="return false"
                BackColor="#99ccff" Font-Size="Smaller" BorderStyle="Solid" BorderWidth="1px"
                BorderColor="#0066FF" ForeColor="#336699" Height="24px" />
        </div>
    </form>
</body>
</html>

If you run this basic page, you can check the group(public) chat functionality wherein, every message is broadcasted to all other users. If you had trouble running till this point, check out this basic tutorial

Creating a one-one (private) chat functionality

Till this point, it's all fine. But, most of the time we also require to have one-to-one chat functionality. Wherein, user A and user B can communicate with each other while user C is not aware about their communication.

SignalR has very useful feature for this case, it's called SignalR "Groups". Logically this means, we create a named group and assign two users into same group. Then, the one-to-one chat text messages are broadcasted to only that group and not all clients. This way, the messages shared within the group are received by only that group's member.

Ok... So, how to implement it?

Some basic configuration stuff

We will somehow need to keep track of the users who are online and what is their connection Id. The connection Id of the current user can be accessed in every connection context of SignalR.

However, to hold the list of all logged in users and their corresponding connection Id's, lets create a modal class called "UserModal.cs". For the sake of clarity, I will create a new Folder called Modal in root of application and add the UserModal.cs class in that.

The class definition contains following fields

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebApplication1
{
    public class UserModal
    {
        public string connectionId=string.Empty;
        public string userName = string.Empty;
        public string userId = string.Empty;
        public bool newStatus = false;
        public string sessionId = string.Empty;
    }
}

We will also create another static modal class which has a List of UserModal object as a public variable. Also, the class offers two methods namely AddOnlineUser and RemoveOnlineUser. As name suggests, both the static methods should be called whenever a new user joins the chat or logs out of chatting.

The AddOnlineUser and RemoveOnlineUser are simply responsible for maintaining the global UserModal List object in synch with the actual list of online user's. Since, the List variable and class OnlineUsers are static, we maintain the single global user list.

Following is the complete code inside OnlineUser.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WebApplication1
{
    public static class OnlineUser
    {
        public static List<UserModal> userObj = new List<UserModal>();
        
        public static void AddOnlineUser(string strConnectionId, string strUserName,string strUserId,string strSessionId)
        {
            UserModal user = new UserModal();
            user.connectionId = strConnectionId;
            user.userName = strUserName;
            user.userId = strUserId;
            user.newStatus = true;
            user.sessionId = strSessionId;
            userObj.Add(user);
        }

        public static void RemoveOnlineUser(string strConnectionId, string strUserName,string strUserId)
        {
            var userRemove = (UserModal)userObj.Where(item => item.connectionId == strConnectionId && item.userName == strUserName);
            userObj.Remove(userRemove);            
        }
    }
}

We are now ready with some ground work. Before moving ahead with actually implementing one-one chat functionality, first imagine a simple chat application. If we want to create one-one chat then unlike public chat room, we need to identify the users who are available in chat.

That is, to be able to do one-one chat, we need to identify who these two users are (in public chat room, we could ignore the chatters login details since, every chat message is broadcasted to all users)

So, let's create a login page first. The login page contains a user name, password fields and a Login button. The login button click event will take care of following things

protected void btnLogin_Click(object sender, EventArgs e)
        {
            if (ValidateUser(txtUserName.Text, txtPassword.Text))
            {
                string user = txtUserName.Text;
                string userId = GetUserUniqueId(txtUserName.Text.Trim());
                Session["UserName"] = txtUserName.Text;
                Session["UserId"] = userId;
                FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, true);
                Response.Redirect("Chat.aspx");
            }
        }

It will validate the user's credential and also return a unique id of the login user. The unique id could be id field from user database table and is useful in further operations. As it is evident, we have created two session variables to hold current user's user name and unique user id. Finally, page is redirected to Chat.aspx where actual chat functionality is implemented.

For rest of this article, we will assume there only three users present in the system with user id's 111, 222, 333. We are making this assumption so that we can hard code these id's in our Javascript code. I am following this hard coding just to simplify the description and to make sure we do not get bogged down by minor details unrelated to SignalR

Update the online user's list

After validating user's credential and creating session values, user is redirected to Chat.aspx. The page load event of chat.aspx will update the online user's list with the new entry and also store user id in a hidden field (we will see the use of this hidden field later)

       protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                if (OnlineUser.userObj.Where(item => item.sessionId == HttpContext.Current.Request.Cookies["ASP.NET_SessionId"].Value.ToString()).Count() > 0)
                    OnlineUser.userObj.Remove(OnlineUser.userObj.Where(item => item.sessionId == HttpContext.Current.Request.Cookies["ASP.NET_SessionId"].Value.ToString()).FirstOrDefault());
                OnlineUser.AddOnlineUser("", HttpContext.Current.Session["UserName"].ToString(), HttpContext.Current.Session["UserId"].ToString(), HttpContext.Current.Request.Cookies["ASP.NET_SessionId"].Value.ToString());
                hdnUserId.Value = Session["UserId"].ToString();
                hdnUserName.Value = Session["UserName"].ToString();
            }
        }

Moving to our chat hub class called "chat.cs", whenever a new client is connected to a SignalR hub, a hub event is called which is named as OnConnected.

We will make use of this OnCpnnected event to notify all users about new user coming online. Also, in this event we will update the online users list. Remember, the in the users list object, we are updating SignalR connection id here which we will eventually require while setting up a one-one to chat

We finally broadcast the complete list of online users to all connected clients (This also helps in identifying the recently logged-off users and changing their status in chat list)

public override Task OnConnected()
{
       var newUsers = OnlineUser.userObj.Where(item => item.newStatus == true).Select(item => item.userId).ToList();            
       UserModal user = OnlineUser.userObj.Where(item => item.sessionId == HttpContext.Current.Request.Cookies["ASP.NET_SessionId"].Value.ToString()).SingleOrDefault();
       user.connectionId = Context.ConnectionId;
       return Clients.All.joined(Context.ConnectionId,newUsers);
}

If you notice, I have uses session id as a unique identifier to match the user who was logged in using Login.aspx page in the OnlineUser's list

User is already in the Chat.aspx since, this is where we redirected users after successful login. As soon chat.aspx page loads in user's browser, the OnConnected event would have been called (since, hub is started in chat.aspx's body load).

From OnConnected event, we have called a client method "joined" which will be executed in all connected clients browser. So, we need to create the "joined" Javascript method first... like below.

chat.client.joined = function (connectionId, userList) {
                $(userList).each(function (index, obj) {
                    if (obj == "111") {
                        $("#stat1").attr('src', 'images/online.png');
                        $("#stat1").addClass('online');
                    }
                    else if (obj == "222") {
                        $("#stat2").attr('src', 'images/online.png');
                        $("#stat2").addClass('online');
                    }
                    else if (obj == "333") {
                        $("#stat3").attr('src', 'images/online.png');
                        $("#stat3").addClass('online');
                    }
                });
            };

If you notice again, I have used hard-coded user id's (simply for maintaining the explanation simple) which could be easily adjusted for dynamic list of n number of users.

Also, I have used online and offline icons to indicate the user's online status. The user's online status will be displayed with the help of following html

<div style="border:thin solid #C0C0C0; font-family:Tunga; width:70px; font-weight: 500;">
            <asp:Image ID="stat1" runat="server" Width="18px" Height="18px" ImageUrl="~/Images/offline.png"/> 
            <a id="status1" class="UserItem" data-userid="111" href="#">Kedar</a>
            <br />
            <asp:Image ID="stat2" runat="server" Width="18px" Height="18px" ImageUrl="~/Images/offline.png"/> 
            <a id="status2" class="UserItem" data-userid="222" href="#">abc</a>
            <br />
            <asp:Image ID="stat3" runat="server" Width="18px" Height="18px" ImageUrl="~/Images/offline.png"/> 
            <a id="status3" class="UserItem" data-userid="333" href="#">xyz</a>
        </div>

With OnConnected event, we can notify the existing users about entry of new chatter. But how the newly connected user will get a list of already connected users?

This becomes easy, because at any given time, we would have a detail of online users available in the OnlineUser list object. So, let's create a new hub method which will simply return this list

public void GetAllOnlineStatus()
{
      Clients.Caller.OnlineStatus(Context.ConnectionId, OnlineUser.userObj.Select(item => item.userId).ToList());
}

From above code it is evident that, we will require a client side method called OnlineStatus

chat.client.OnlineStatus = function (connectionId, userList) {
                $("img[id^=stat]").attr('src', 'images/offline.png');
                $(userList).each(function (index, obj) {
                    if (obj == "111") {
                        $("#stat1").attr('src', 'images/online.png');
                        $("#status1").addClass('online');
                    }
                    else if (obj == "222") {
                        $("#stat2").attr('src', 'images/online.png');
                        $("#status2").addClass('online');
                    }
                    else if (obj == "333") {
                        $("#stat3").attr('src', 'images/online.png');
                        $("#status3").addClass('online');
                    }
                });
            };

However, the hub method GetAllOnlineStatus needs to be invoked from client side hub. This should be typically done as soon as user is connected to chat room; which means when hub connection is initiated from client side... like this

// Start the connection
            $.connection.hub.start( function () {
                chat.server.getAllOnlineStatus();
            });

Set up a one to one chat window (UI)

In one-one chat, the number of chat windows would be equivalent to number of different chatters we will be chatting with at a time. So, the chat window has to be dynamic. Hence, we will simply write a markup for one chat window and clone it as an when new chat is initiated

        <div id="divChatWindow" style="border: 1px solid #3366CC; float: left; width: 365px; display: none; margin-right: 10px">
            <div style="border: 1px solid blue;">
                <ul id="messages" style="width: 465px; height: 100px;font:normal 2 verdana";"></ul>
            </div>
            <hr />
            <asp:TextBox runat="server" ID="msg" CssClass="ChatText" Width="280px" BorderColor="#666699"
                BorderStyle="Solid" BorderWidth="1px" Height="22px"></asp:TextBox>
            <asp:Button ID="btnChatSend" runat="server" CssClass="ChatSend" Text="Submit" OnClientClick="return false"
                BackColor="#99ccff" Font-Size="Smaller" BorderStyle="Solid" BorderWidth="1px"
                BorderColor="#0066FF" ForeColor="#336699" Height="24px" />
        </div>

Now, we will need create a chat window whenever user clicks on a another user's name who is in online state (we will certainly not support offline chat)

This is how we can do it

            $('.UserItem').click(function () {
                if ($(this).hasClass('online')) {
                    chat.server.createGroup($('#hdnUserId').val(), $(this).attr('data-userid'));
                    var chatWindow = $("#divChatWindow").clone(true);
                    $(chatWindow).attr('chatToId', $(this).attr('data-userid'));
                    $("#chatContainer").append(chatWindow);                 
                }
                return false;
            });

Apart of some usual JavaScript, the above code contains a call to a server side hub method called "createGroup". This is where we will actually user SignalR groups and support one-to-one chatting.

Also note that two parameters are being passed to createGroup method the first is user id of current user which we had stored in hidden field of page and second parameter is user id of the chatter we intend to start a one-one chat with.

One-to-One communication using Groups

The CreateGroup method on server side hub can be writen as...

        public void CreateGroup(string currentUserId, string toConnectTo)
        {
            string strGroupName = GetUniqueGroupName(currentUserId, toConnectTo);
            string connectionId_To = OnlineUser.userObj.Where(item => item.userId == toConnectTo).Select(item=>item.connectionId).SingleOrDefault();
            if (!string.IsNullOrEmpty(connectionId_To))
            {
                Groups.Add(Context.ConnectionId, strGroupName);
                Groups.Add(connectionId_To, strGroupName);
                Clients.Caller.setChatWindow(strGroupName, toConnectTo);
            }
        }

Note that, we are creating the new group using this line

Groups.Add(context.ConnectionId, strGroupName);

While creating new Group, we need to give it a name. Since we need to name the group dynamically, the name should be formed such that it will be unique for any given pair of users. However, we cannot simple append the user id's of both the users to form a group name.

Ex - user1user2

Because, when another user of group sends a message, it will form a group name as - user2user1

so, to generate a unique group name irrespective of who is sender and who is receiver in given pair, I have used a custom function GetUniqueGroupName. The function accepts both the user id's as a parameter and this is how a unique group name is devised

        private string GetUniqueGroupName(string currentUserId, string toConnectTo)
        {
            return (currentUserId.GetHashCode() ^ toConnectTo.GetHashCode()).ToString();
        }

The CreateGroup method simply creates a new group with unique group name and adds ConnectionId's of both users to the group and finally invokes a client side hub method setChatWindow for only calling user (the user who is initiating a one-one chat). The client hub setChatWindow method simply make the chat window visible.

That means, whenever user clicks on the name of any other chatter, a group will be created in background and both the user's connection id's are added into that group and then a chat window is made visible to user who is initiating a chat. Additionally, the group name is stored as the chat window div's attribute; so that, during rest of communication from this chat window group name can be sent to server hub for identifying intended recipient of chat messages.

A chat initiating user can now add text message in chat window and click submit. It will simply call a server hub method named "send" and pass the actual text message and group name as parameter to it

In return, the server hub method "Send" will invoke another client hub method addMessage... but only for the users present in the given group name

        // submit button click event
            $(".ChatSend").click(function () {
                strChatText = $('.ChatText', $(this).parent()).val();
                if (strChatText != '') {
                    var strGroupName = $(this).parent().attr('groupname');
                    if (typeof strGroupName !== 'undefined' && strGroupName !== false)
                        chat.server.send($("#hdnUserName").val() + ' : ' + strChatText, $(this).parent().attr('groupname'));
                    $('.ChatText', $(this).parent()).find('ul').append(strChatText);
                }
                return false;
            });


     // server hub method "send"
        public void Send(string message, string groupName)
        {
            if (Clients != null)
            {
                Clients.Group(groupName).addMessage(message,groupName);
            }
        }

We now only require defining a client hub method named "addMessage" which will receive the chat message and group name as a parameter. In addMessage, we will first check to see if there already exist a chat window in current page where groupname parameter value matches with chat window div's attribute value.

If yes, that means the incoming chat messages belongs to a one-one chat window which is already in operation. If now, that means this is the first message received from this group. Hence, create and popup a new chat window and display the messsage in there.

          chat.client.addMessage = function (message, groupName) {
                if ($('div[groupname=' + groupName + ']').length == 0) {
                    var chatWindow = $("#divChatWindow").clone(true);
                    $(chatWindow).css('display', 'block');
                    $(chatWindow).attr('groupname', groupName);
                    $("#chatContainer").append(chatWindow);
                }
                $('div[groupname=' + groupName + ']').find('ul').append('<LI>' + message + '');
            };

This completes our complete code for creating one-one chat using SignalR. Finally, this is how it would work once you run and try to chat using different browser windows.

If you could not keep up with the whole lot of code snippets, you can download the sample project I created above and play around with it

SignalR_OneOneChat.zip (7.34 mb)

Conclusion

There are many features of SignalR which can be used based on asynchronous data requirement of your application. In this detailed article we tried to explore the use of groups and create a one-to-one chat application. There are many other features of SignalR which can be utilized in many other scenarios.

I will try to explain some other features of SignalR in future posts. Thanks for visiting my site!

Comments (15) -

Sabri

i'm getting an error in the method "public static void Remove OnlineUser:
Unable to cast object of type 'WhereListIterator`1[SignalR.Chat.Models.Client]' to type 'SignalR.Chat.Models.Client'.

this is the code that gives the error (line 1)

var userRemoval = (Client)userObj.Where(item => item.connectionId == strConnectionId && item.Name == strUserName);
userObj.Remove(userRemoval);

Sabri

I've got it, you forgot to type ".Select(item=>item);"
after your where clause.
it can't convert a where iterator to your userModal

Wendy

Is there any way to solve the casting problem?

Bhaskar

Hey bro there is no style sheet according to your above given code..it shows could not found the style sheet..

Pratik

Hi,

When using with dynamic users Fetch from DB and populate DOM link / button control does not work. When click on other user to open chat window thats where fails as the dynamic DOM controls not recognise by JS any idea how to work around this.

KedarRKulkarni

@Pratik

Even with dynamic list of users, the given JavaScript code should work. Since, we are using class name to reference the user name button/link.
Are you using any other method to open the chat window?

gear reviews

Hey there! Would you mind if I share your blog with my zynga group? There's a lot of people that I think would really appreciate your content. Please let me know. Many thanks

Feel free to surf to my web page:  gear reviews - www.thebackpackingclub.com/.../

weight loss foods

Great goods from you, man. I've understand your stuff previous to and you're just too great. I actually like what you have acquired here, really like what you are stating and the way in which you say it. You make it entertaining and you still care for to keep it smart. I cant wait to read far more from you. This is actually a tremendous web site.

my site ::  weight loss foods - http://adsboards.com

arthritis remedies

I am in fact glad to glance at this web site posts which includes lots of useful data, thanks for providing such information.

Also visit my webpage ::  arthritis remedies - http://arthritis.allhint.com

forex online trading

Magnificent beat ! I wish to apprentice while you amend your site, how could i subscribe for a blog website? The account aided me a acceptable deal. I had been a little bit acquainted of this your broadcast offered bright clear concept

Feel free to visit my homepage:  forex online trading - http://forex.cluesite.com

cellulite exercises

What's up i am kavin, its my first time to commenting anyplace, when i read this post i thought i could also create comment due to this sensible paragraph.

my site  cellulite exercises - http://cellulite.adsboards.com

healthy foods to eat

You made some good points there. I looked on the internet for additional information about the issue and found most people will go along with your views on this site.

Here is my weblog ...  healthy foods to eat - http://tipguide.net

council on the aging

Thank you for the auspicious writeup. It in reality was a enjoyment account it. Glance complex to far delivered agreeable from you! By the way, how can we keep up a correspondence?

Also visit my web site ::  council on the aging - http://findhint.com

online homeschooling programs

This is very interesting, You are a very skilled blogger. I have joined your feed and look forward to seeking more of your wonderful post. Also, I've shared your web site in my social networks!

Feel free to surf to my web blog;  online homeschooling programs - http://hintcenter.com

panic attack medication

Hello! I'm at work browsing your blog from my new apple iphone! Just wanted to say I love reading through your blog and look forward to all your posts! Keep up the outstanding work!

My website ::  panic attack medication - http://cluesite.com

Pingbacks and trackbacks (1)+

About Me

You are visiting personal website of Kedar (KK)

Please go here to know more about me

Disclaimer

The opinions expressed here represent my own and not those of my past or present employers.

The concept/code provided on this site may not work as described. If you are using any code provided on this site. Then, please test it thoroughly. I shall not be responsible for any issues arising in the code. 

Month List