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.
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.
Chat With Another User Privately
There are several things that happens when we send someone an invitation to chat privately.
-
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:
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>");
-
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 }
-
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:
-
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?
-
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.
-
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:
-
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 }
-
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();
.
.
-
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();
-
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 }
-
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.
As always, the code and the article are provided "As Is", there is absolutely no warranties. Use at your own risk.