Build a Web Chat Application Part 2 - Chat with Other Users Privately (in C# 3.5)


Technologies Used: ASP.Net 3.5, AJAX, JavaScript, C# 3.5, LINQ-to-SQL, MS SQL Server 2000/2005

Note: As usual in about a week, I will have this article shown in VB 9.0 code. So check back later.

Introduction:

In Part 1 we built a very simple, flicker-free, frame-less, web chat application using ASP.Net 3.5, LINQ-to-SQL, AJAX and MS SQL Server, with your choice of either C# 3.5 or VB 9.0 in about 2 hours. As promised, we will add functionality to our previous chatroom application so that users will have the ability to chat with each other privately. You can read and download the code of Part 1 in C# 3.5 here, or in VB 9.0 here.

Here's a snapshot of the Chatroom area.

Revised LINQ Chatroom Part 2

I noticed while coding for part 2 that the names (in black) of the chatters didn't show in the chatroom messages, so I added them real quickly. Although I have no doubt that you could have as easily done so, but here's the code revision anyway:

  201    if (message.User.Sex.ToString().ToLower() == "m")
  202        sb.Append("<img src='Images/manIcon.gif' style='vertical-align:middle' alt=''> <span style='color: black; font-weight: bold;'>" + 
  203            message.User.Username + ":</span>  " + message.Text + "</div>");
  204    else
  205        sb.Append("<img src='Images/womanIcon.gif' style='vertical-align:middle' alt=''> <span style='color: black; font-weight: bold;'>" + 
  206            message.User.Username + ":</span>  " + message.Text + "</div>");


Database Revision/Addition

We will need a table to hold information regarding users that sends invitations to other chatters, so they can chat privately. As shown below, I added a table called PrivateMessage.

Revised Database Structure

Chat With Another User Privately

There are several things that happens when we send someone an invitation to chat privately.

  1. To start sending another chatter an invitation to chat, we simply click the chatter's user name from the list of the users, shown in a box to the right, on the Chatroom.aspx page. This should open another window, the ChatWindow.aspx. A new window opens because of the additional code shown below:

    The ChatWindow.aspx opens or pops up:

    Private Chat Window

    Using the code below:

      155    sb.Append(userIcon + "<a href=# onclick=\"window.open('ChatWindow.aspx?FromUserId=" + Session["ChatUserID"] +
      156            "&ToUserId=" + loggedInUser.User.UserID + "&Username=" + loggedInUser.User.Username +
      157            "','','width=400,height=200,scrollbars=no,toolbars=no,titlebar=no,menubar=no'); isLostFocus = 'true';\">" +
      158            loggedInUser.User.Username + "</a><br>");


  2. To send an invitation, we first need to send a message to the other chatter on the newly popped-up window (ChatWindow.aspx), just like other chat applications out there. When you send a message to let's say "John Doe" from this window for the very first time, information is saved in the PrivateMessage table. So, just to make it clear, all messages sent after the first one to "John Doe" will not be saved in the PrivateMessage table.

       53         // This is where we send a private chat invitation to other chatters
       54         private void InsertPrivateMessage()
       55         {
       56             // if the private message is sent to this user already, 
       57             // don't send it again
       58             if (String.IsNullOrEmpty(lblMessageSent.Text))
       59             {
       60                 // if any private message is found based on the 
       61                 // from user id, or the to user id, then this 
       62                 // private message will not be inserted
       63                 PrivateMessage privateMessage = new PrivateMessage();
       64                 privateMessage.UserID = Convert.ToInt32(lblFromUserId.Text);
       65                 privateMessage.ToUserID = Convert.ToInt32(lblToUserId.Text);
       66 
       67                 LinqChatDataContext db = new LinqChatDataContext();
       68                 db.PrivateMessages.InsertOnSubmit(privateMessage);
       69                 db.SubmitChanges();
       70 
       71                 // make sure to assign any value to this label
       72                 // to confirm that a message is sent to this user
       73                 lblMessageSent.Text = ConfigurationManager.AppSettings["ChatWindowMessageSent"];
       74             }
       75         }


  3. The information saved in the PrivateMessage table is then retrieved from the main chatroom page (Chatroom.aspx) every seven (7) seconds or whenever you hit the send button, checking if another user(s) have sent you an invitation to chat privately. If an entry is found, the hidden panel server control's visible attribute is then set to "true", showing the invitation to chat privately.

    Here's the code that checks if an invitation to chat privately is sent to you:

      237         /// <summary>
      238         /// Check if anyone invited me to chat privately
      239         /// </summary>
      240         private void GetPrivateMessages()
      241         {
      242             LinqChatDataContext db = new LinqChatDataContext();
      243 
      244             var privateMessage = (from pm in db.PrivateMessages
      245                                   where pm.ToUserID == Convert.ToInt32(Session["ChatUserID"])
      246                                   select pm).SingleOrDefault();
      247 
      248             if (privateMessage != null)
      249             {
      250                 lblChatNowUser.Text = privateMessage.User.Username;
      251                 btnChatNow.OnClientClick = 
      252                     "window.open('ChatWindow.aspx?FromUserId=" + Session["ChatUserID"] + 
      253                     "&ToUserId=" + privateMessage.UserID + "&Username=" + privateMessage.User.Username + 
      254                     "&IsReply=yes','','width=400,height=200,scrollbars=no,toolbars=no,titlebar=no,menubar=no'); isLostFocus = 'true';";
      255 
      256                 pnlChatNow.Visible = true;
      257 
      258 
      259                 db.PrivateMessages.DeleteOnSubmit(privateMessage);
      260                 db.SubmitChanges();
      261             }
      262         }


    Here's a snapshot of the panel server control showing that someone has invited you to chat privately:

    Invitation to chat privately

  4. You will notice that there are two buttons on the invitation box, the "Chat Now" and "Cancel" buttons. Whether you click the Chat Now or the Cancel button, the entry on the PrivateMessage table for you and the user that invited you will be deleted (Line 259 and 260 shown above) and the panel's visible attribute will be set to "false", and this is primarily what the Cancel button does. On the other hand, if you click the Chat Now button, a new window will pop-up (Lines 251-254 shown above) and you can now start chatting privately with the user that invited you to chat. Code is shown below from the Chatroom.aspx code file:

      264         protected void BtnChatNow_Click(object sender, EventArgs e)
      265         {
      266             pnlChatNow.Visible = false;
      267         }
      268 
      269         protected void BtnCancel_Click(object sender, EventArgs e)
      270         {
      271             pnlChatNow.Visible = false;
      272         }


    As usual, I was in a hurry to finish this code. One suggestion would be, when the user clicks the Cancel button, send the other user who sent the invitation a message saying that you declined the invitation to chat privately. This can easily be done on the Cancel button's click event. Maybe I'll add this in Part 3.

Difference Between Chatting Privately and Chatting Corporately

Other than the obvious missing list of users when chatting privately, the main difference code-wise is; we're sending the message to a specific user. Remember in Part 1 that the ToUserID property of the Message object/table is not being filled or passed, now it's still optional to assign a value in this property. But, if you want to chat privately with someone, the ToUserID must be assigned. See code below from the ChatWindow.aspx code file:

   53         // This is where we send a private chat invitation to other chatters
   54         private void InsertPrivateMessage()
   55         {
   56             // if the private message is sent to this user already, 
   57             // don't send it again
   58             if (String.IsNullOrEmpty(lblMessageSent.Text))
   59             {
   60                 // if any private message is found based on the 
   61                 // from user id, or the to user id, then this 
   62                 // private message will not be inserted
   63                 PrivateMessage privateMessage = new PrivateMessage();
   64                 privateMessage.UserID = Convert.ToInt32(lblFromUserId.Text);
   65                 privateMessage.ToUserID = Convert.ToInt32(lblToUserId.Text);
   66 
   67                 LinqChatDataContext db = new LinqChatDataContext();
   68                 db.PrivateMessages.InsertOnSubmit(privateMessage);
   69                 db.SubmitChanges();
   70 
   71                 // make sure to assign any value to this label
   72                 // to confirm that a message is sent to this user
   73                 lblMessageSent.Text = ConfigurationManager.AppSettings["ChatWindowMessageSent"];
   74             }
   75         }


Focusing The Current Window

In Part 1 we focused the Send button by setting the defaultbutton attribute of the form tag to the ID of the Send button. This made a continous focus on the Send button so that when we hit the "Enter key" on our keyboard, the message is sent, there's no need to manually click the Send button with your mouse to send a message. Interestingly enough, we cannot do this when another window opens up as a child or children window because the focus will keep returning back to the parent window. Or if we also set the defaultbutton attribute of the form tag to the ID of the Send button in the child window (ChatWindow.aspx), the focus will go back and forth from the Chatroom.aspx to the ChatWindow.aspx, and vice versa.

So why does this happen?
  1. The window will refocus the Send button, therefore focusing back on the window itself every single time there's a post back. The post back occurs every 7 seconds on the timer tick event.

  2. The parent and child windows share Sessions. You will notice in Part 1 that if you open two or more instances of the Chatroom.aspx window, the focus does not go back and fort these windows even though the defaultbutton attribute is set for the Send button. This is because the windows are instances of the browser, and therefore does not share Session with each other.

The solution:

We need to be able to focus the Message TextBox (where you type your messages) and the Send button dynamically and on demand. To do this, we need to be able to tell which window is currently in focus, is it the main chat room (Chatroom.aspx)? Or is it one of the opened private chat windows (ChatWindow.aspx)? Listed below are the events from which we need to set the focus of our message text box and the send button:

  1. Shown below is the reusable method that we will call to set the focus from different events in our code. Why is the code longer in the ChatWindow.aspx code file? Because we're assuming that each Chatroom.aspx that is open are separate instances of the browser, therefore, each Chatroom.aspx window that is open only need one marker, "MainWindow". On the other hand, each ChatWindow.aspx that is open shares the same Session as the parent, and are not a separate instances of the browser, this is why we need to know which of these ChatWindows is supposed to receive the focus.

    From the Chatroom.aspx:

      274         private void FocusThisWindow()
      275         {
      276             form1.DefaultButton = "btnSend";
      277             form1.DefaultFocus = "txtMessage";
      278             Session["DefaultWindow"] = "MainWindow";
      279         }


    From the ChatWindow.aspx:

      141         private void FocusThisWindow()
      142         {
      143             string chatWindowToFocus = lblFromUserId.Text + "_" + lblToUserId.Text;
      144 
      145             if (Session["DefaultWindow"].ToString() == chatWindowToFocus)
      146             {
      147                 form1.DefaultButton = "btnSend";
      148                 form1.DefaultFocus = "txtMessage";
      149             }
      150         }


  2. Every single time a window loads, or opens, whether it be the Chatroom.aspx or the ChatWindow.aspx, we need to set the focus on the Page Load event under the !IsPostBack validation.

    From the Chatroom.aspx, we simply call the method:

       12         protected void Page_Load(object sender, EventArgs e)
       13         {
       14             if (!IsPostBack)
       15             {
                      .
                      .
       29                 this.FocusThisWindow();
                      .
                      .


    But from the ChatWindow.aspx, we need to remember which chat window must receive the focus:

       12         protected void Page_Load(object sender, EventArgs e)
       13         {
                      .
                      .
       16             if (!IsPostBack)
       17             {
                      .
                      .
       30                 string chatWindowToFocus = lblFromUserId.Text + "_" + lblToUserId.Text;
       31                 Session["DefaultWindow"] = chatWindowToFocus;
       32                 this.FocusThisWindow();
                      .
                      .


  3. In the OnClick event of the Send button. Both Chatroom.aspx and ChatWindow.aspx code file have similar code here.

       58         protected void BtnSend_Click(object sender, EventArgs e)
       59         {
       60             if (txtMessage.Text.Length > 0)
       61             {
                          .
                          .
       66                 this.FocusThisWindow();


  4. In the tick event of the Timer Control.

    From the Chatroom.aspx page:

       70         protected void Timer1_OnTick(object sender, EventArgs e)
       71         {
                      .
                      .
       76             if (Session["DefaultWindow"] != null)
       77             {
       78                 if (Session["DefaultWindow"].ToString() == "MainWindow")
       79                 {
       80                     this.FocusThisWindow();
       81                 }
       82             }
       83         }


    From the ChatWindow.aspx page:

      131         protected void Timer1_OnTick(object sender, EventArgs e)
      132         {
                      .
                      .
      135             if (Session["DefaultWindow"] != null)
      136             {
      137                 this.FocusThisWindow();
      138             }
      139         }


  5. And lastly, in the onclick event of the Message TextBox control. The only catch here is that; the Message Text Box control does not have a server side onclick event. So we'll have to do this from the client-side, and from the client, we will call a server-side method. In Part 1, we implemented a callback feature so that when a chatter closes his browser, we're still able to log that user out by catching a client-side event that calls to a server-side method, where the actual logging out is done. We will do a similar code here.

    From the TextBox, we will call a client-side function, shown below:

    <asp:TextBox Id="txtMessage" onkeyup="ReplaceChars()" onclick="FocusMe()" onfocus="SetToEnd(this)" runat="server" MaxLength="100" Width="500px" />



    Here's the client-side function that is called from the onclick event of the Text Box control. Notice that it's calling another method. This is a call-back method called asynchronously with the client-side function, which we will wire-up from the server side.

    function FocusMe()

    {

    FocusThisWindowCallBack('FocusThisWindow');

    }



    In the code file (code-behind), because we're implementing the ICallbackEventHandler (see Part 1 for explanation), we can easily register scripts for the call back functionality.

       36         // create a call back reference so that we can refocus to this window when the cursor is placed in the message text box
       37         string focusWindowCallBackReference = Page.ClientScript.GetCallbackEventReference(this, "arg", "FocusThisWindow", "");
       38         string focusThisWindowCallBackScript = "function FocusThisWindowCallBack(arg, context) { " + focusWindowCallBackReference + "; }";
       39         Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "FocusThisWindowCallBack", focusThisWindowCallBackScript, true);


    Now, the magic happens in the ICallbackEventHandler's RaiseCallbackEvent method. The control was passed here when we called the FocusThisWindowCallBack('FocusThisWindow') from the client-side. The "eventArgument" works as as comma-delimited string variable, so if you passed more than one string as a parameter from the client-side function, all will be caught by this variable. Note that all callback methods from the client-side will pass through the RaiseCallbackEvent. With that said, in Part 1, we did not pass any parameter(s) from the Log Out callback method, because there's only one method that will pass through here. But now both the Log Out and the Message Text Box control's client-side onclick event will use the RaiseCallbackEvent. In short, we will differentiate who did the call back by checking the eventArgument's value.

    From the Chatroom.aspx code file:

      288         /// <summary>
      289         /// We're doing 2 things here now so we want to validate whether we're trying to "LogOut" or "FocusThisWindow"
      290         /// based on the eventArgument parameter that is passed via a JavaScript callback method
      291         /// </summary>
      292         void  System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
      293         {
      294             _callBackStatus = "failed";
      295 
      296             if (!String.IsNullOrEmpty(eventArgument))
      297             {
      298                 // put back the focus on this window
      299                 if (eventArgument == "FocusThisWindow")
      300                 {
      301                     this.FocusThisWindow();
      302                 }
      303             }
      304 
      305             if (!String.IsNullOrEmpty(eventArgument))
      306             {
      307                 if (eventArgument == "LogOut")
      308                 {
      309                     // log out the user by deleting from the LoggedInUser table
      310                     LinqChatDataContext db = new LinqChatDataContext();
      311 
      312                     var loggedInUser = (from l in db.LoggedInUsers
      313                                         where l.UserID == Convert.ToInt32(Session["ChatUserID"])
      314                                         && l.RoomID == Convert.ToInt32(lblRoomId.Text)
      315                                         select l).SingleOrDefault();
      316 
      317                     db.LoggedInUsers.DeleteOnSubmit(loggedInUser);
      318                     db.SubmitChanges();
      319 
      320                     // insert a message that this user has logged out
      321                     this.InsertMessage("Just logged out! " + DateTime.Now.ToString());
      322                 }
      323             }
      324 
      325             _callBackStatus = "success";
      326         }


    From the ChatWindow.aspx code file:

      159         void System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
      160         {
      161             if (!String.IsNullOrEmpty(eventArgument))
      162             {
      163                 if (eventArgument == "FocusThisWindow")
      164                 {
      165                     string chatWindowToFocus = lblFromUserId.Text + "_" + lblToUserId.Text;
      166                     Session["DefaultWindow"] = chatWindowToFocus;
      167                     this.FocusThisWindow();
      168                 }
      169             }
      170         }


Last Words:

Adding features to this web chat application using ASP.Net 3.5, LINQ-to-SQL, and ASP.Net AJAX surely made coding a whole lot faster, and therefore fun. Adding this private imming or chatting functionality took about an hour to complete. Doing this in ASP.Net 2.0 would have taken at least 3 times longer to complete. So kudos to the ASP.Net team. I'm planning to write a Part 3, but I'm not sure what to write about or what functionalities to add, so if you have any idea of the functionalities that I can add, please email me.


Code Download: Click here to download the code

As always, the code and the article are provided "As Is", there is absolutely no warranties. Use at your own risk.

Happy Coding!!!

Date Created: Wednesday, July 16, 2008