Build a Web Chat Application Part 2 - Chat with Other Users Privately (in VB 9.0)


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

As promised. Here's the Visual Basic 9.0 version of the Part 2 of the chat application. To see this article and code in C# 3.5, click here.

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:

  177    If message.User.Sex.ToString().ToLower() = "m" Then
  178        sb.Append("<img src='Images/manIcon.gif' style='vertical-align:middle' alt=''>  <span style='color: black; font-weight: bold;'>" & _
  179                  message.User.Username & ":</span>  " & message.Text + "</div>")
  180    Else
  181        sb.Append("<img src='Images/womanIcon.gif' style='vertical-align:middle' alt=''>  <span style='color: black; font-weight: bold;'>" & _
  182                   message.User.Username & ":</span>  " & message.Text + "</div>")
  183    End If


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:

      139    sb.Append(userIcon & "<a href=# onclick=""window.open('ChatWindow.aspx?FromUserId=" & Session("ChatUserID") & _
      140            "&ToUserId=" & loggedInUser.User.UserID & "&Username=" & loggedInUser.User.Username & _
      141            "','','width=400,height=200,scrollbars=no,toolbars=no,titlebar=no,menubar=no'); isLostFocus = 'true';"">" & _
      142            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.

       49    ' This is where we send a private chat invitation to other chatters
       50    Private Sub InsertPrivateMessage()
       51        ' if the private message is sent to this user already, 
       52        ' don't send it again
       53        If String.IsNullOrEmpty(lblMessageSent.Text) Then
       54            ' if any private message is found based on the 
       55            ' from user id, or the to user id, then this 
       56            ' private message will not be inserted
       57            Dim privateMessage As PrivateMessage = New PrivateMessage()
       58            privateMessage.UserID = Convert.ToInt32(lblFromUserId.Text)
       59            privateMessage.ToUserID = Convert.ToInt32(lblToUserId.Text)
       60 
       61            Dim db As LinqChatDataContext = New LinqChatDataContext()
       62            db.PrivateMessages.InsertOnSubmit(privateMessage)
       63            db.SubmitChanges()
       64 
       65            ' make sure to assign any value to this label
       66            ' to confirm that a message is sent to this user
       67            lblMessageSent.Text = ConfigurationManager.AppSettings("ChatWindowMessageSent")
       68        End If
       69    End Sub


  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:

      213     ''' <summary>
      214     ''' Check if anyone invited me to chat privately
      215     ''' </summary>
      216     Private Sub GetPrivateMessages()
      217         Dim db As LinqChatDataContext = New LinqChatDataContext()
      218 
      219         Dim privateMessage = (From pm In db.PrivateMessages _
      220                               Where pm.ToUserID = Convert.ToInt32(Session("ChatUserID")) _
      221                               Select pm).FirstOrDefault()
      222 
      223         If Not privateMessage Is Nothing Then
      224             lblChatNowUser.Text = privateMessage.User.Username
      225             btnChatNow.OnClientClick = _
      226                 "window.open('ChatWindow.aspx?FromUserId=" & Session("ChatUserID") & _
      227                 "&ToUserId=" & privateMessage.UserID & "&Username=" & privateMessage.User.Username & _
      228                 "&IsReply=yes','','width=400,height=200,scrollbars=no,toolbars=no,titlebar=no,menubar=no'); isLostFocus = 'true';"
      229 
      230             pnlChatNow.Visible = True
      231 
      232             db.PrivateMessages.DeleteOnSubmit(privateMessage)
      233             db.SubmitChanges()
      234         End If
      235     End Sub


    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:

      237     Protected Sub BtnChatNow_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnChatNow.Click
      238         pnlChatNow.Visible = False
      239     End Sub
      240 
      241     Protected Sub BtnCancel_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnCancel.Click
      242         pnlChatNow.Visible = False
      243     End Sub


    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:

   49    ' This is where we send a private chat invitation to other chatters
   50    Private Sub InsertPrivateMessage()
   51        ' if the private message is sent to this user already, 
   52        ' don't send it again
   53        If String.IsNullOrEmpty(lblMessageSent.Text) Then
   54            ' if any private message is found based on the 
   55            ' from user id, or the to user id, then this 
   56            ' private message will not be inserted
   57            Dim privateMessage As PrivateMessage = New PrivateMessage()
   58            privateMessage.UserID = Convert.ToInt32(lblFromUserId.Text)
   59            privateMessage.ToUserID = Convert.ToInt32(lblToUserId.Text)
   60 
   61            Dim db As LinqChatDataContext = New LinqChatDataContext()
   62            db.PrivateMessages.InsertOnSubmit(privateMessage)
   63            db.SubmitChanges()
   64 
   65            ' make sure to assign any value to this label
   66            ' to confirm that a message is sent to this user
   67            lblMessageSent.Text = ConfigurationManager.AppSettings("ChatWindowMessageSent")
   68        End If
   69    End Sub


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:

      245     Private Sub FocusThisWindow()
      246         form1.DefaultButton = "btnSend"
      247         form1.DefaultFocus = "txtMessage"
      248         Session("DefaultWindow") = "MainWindow"
      249     End Sub


    From the ChatWindow.aspx:

      126         Private Sub FocusThisWindow()
      127             Dim chatWindowToFocus As String = lblFromUserId.Text & "_" & lblToUserId.Text
      128 
      129             If Session("DefaultWindow").ToString() = chatWindowToFocus Then
      130                 form1.DefaultButton = "btnSend"
      131                 form1.DefaultFocus = "txtMessage"
      132             End If
      133         End Sub


  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:

       13     Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
       14         If Not IsPostBack Then
                      .
                      .
       28             Me.FocusThisWindow()
                      .
                      .


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

       13         Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
                      .
                      .
       27                 ' focus this window
       28                 Dim chatWindowToFocus As String = lblFromUserId.Text & "_" & lblToUserId.Text
       29                 Session("DefaultWindow") = chatWindowToFocus
       30                 Me.FocusThisWindow()
                      .
                      .


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

       39         Protected Sub BtnSend_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnSend.Click
                          .
                          .
       45                 Me.FocusThisWindow()
                          .
                          .


  4. In the tick event of the Timer Control.

    From the Chatroom.aspx page:

       65     Protected Sub Timer1_OnTick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Timer1.Tick
       66         Me.GetLoggedInUsers()
       67         Me.GetMessages()
       68         Me.GetPrivateMessages()
       69 
       70         If Not Session("DefaultWindow") Is Nothing Then
       71             If Session("DefaultWindow").ToString() = "MainWindow" Then
       72                 Me.FocusThisWindow()
       73             End If
       74         End If
       75     End Sub


    From the ChatWindow.aspx page:

      118         Protected Sub Timer1_OnTick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Timer1.Tick
      119             Me.GetPrivateMessages()
      120 
      121             If Not Session("DefaultWindow") Is Nothing Then
      122                 Me.FocusThisWindow()
      123             End If
      124         End Sub


  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.

       35    ' create a call back reference so that we can refocus to this window when the cursor is placed in the message text box
       36    Dim focusWindowCallBackReference As String = Page.ClientScript.GetCallbackEventReference(Me, "arg", "FocusThisWindow", "")
       37    Dim focusThisWindowCallBackScript As String = "function FocusThisWindowCallBack(arg, context) { " + focusWindowCallBackReference + "; }"
       38    Page.ClientScript.RegisterClientScriptBlock(Me.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:

      255     ''' <summary>
      256     ''' We're doing 2 things here now so we want to validate whether we're trying to "LogOut" or "FocusThisWindow"
      257     ''' based on the eventArgument parameter that is passed via a JavaScript callback method
      258     ''' </summary>
      259     Public Sub RaiseCallbackEvent(ByVal eventArgument As String) Implements System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent
      260         _callBackStatus = "failed"
      261 
      262         If Not String.IsNullOrEmpty(eventArgument) Then
      263             ' put back the focus on this window
      264             If eventArgument = "FocusThisWindow" Then
      265                 Me.FocusThisWindow()
      266             End If
      267         End If
      268 
      269         If Not String.IsNullOrEmpty(eventArgument) Then
      270             If eventArgument = "LogOut" Then
      271                 ' log out the user by deleting from the LoggedInUser table
      272                 Dim db As LinqChatDataContext = New LinqChatDataContext()
      273 
      274                 Dim loggedInUser = (From l In db.LoggedInUsers _
      275                                     Where l.UserID = Convert.ToInt32(Session("ChatUserID")) _
      276                                     And l.RoomID = Convert.ToInt32(lblRoomId.Text) _
      277                                     Select l).SingleOrDefault()
      278 
      279                 db.LoggedInUsers.DeleteOnSubmit(loggedInUser)
      280                 db.SubmitChanges()
      281 
      282                 ' insert a message that this user has logged out
      283                 Me.InsertMessage("Just logged out! " + DateTime.Now.ToString())
      284             End If
      285         End If
      286 
      287         _callBackStatus = "success"
      288     End Sub


    From the ChatWindow.aspx code file:

      139    Public Sub RaiseCallbackEvent(ByVal eventArgument As String) Implements System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent
      140        If Not String.IsNullOrEmpty(eventArgument) Then
      141            If eventArgument = "FocusThisWindow" Then
      142                Dim chatWindowToFocus As String = lblFromUserId.Text & "_" & lblToUserId.Text
      143                Session("DefaultWindow") = chatWindowToFocus
      144                Me.FocusThisWindow()
      145            End If
      146        End If
      147    End Sub


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: Saturday, August 9, 2008