Build a Web Chat Application using ASP.Net 3.5, LINQ and AJAX (in C# 3.5)


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

Note: To see this article, and download the code in VB 9.0 click here.

Introduction:

A Java Programmer friend of mine once asked me if I have ever built a web chat application. I said "No". He then asked me if I were to build a really simple, monitored, bare minimum, working web chat application, how long do I think will it take (something to that effect). I said, "I don't know". A few days had passed, one lazy evening, I was surfing the web looking for ASP.Net web chat applications. It seems that I can't find one that is simple enough to implement. So I decided to just build something simple from scratch. When I say simple, that just means that: I'm am going to try to build this web chat as fast as possible, with very minimal planning or architecturing. So what you will read below are my notes after I got done building the web chat application. Here's an example of how the chatroom page looks like:

LINQ Chatroom

Requirements:

We will create a very simple web chat application using the latest ASP.Net 3.5 technologies from scratch just for fun. This chat application will contain 2 web pages, the login page and the chatroom page. Most of the tutorial will be focused on the chatroom page. Some of the things that I want to accomplish are as follows:
  • Must be accessible anywhere, and no need to download and install any components. This is why we're going to create a web chat.
  • Web chat must be "flicker-free". This is where AJAX will be very handy.
  • We want to be able to monitor chat conversations using a database. We will use MS SQL Server to store conversations and user information.
  • No use of frames. I read somewhere that frames are evil, and I kind of agree with that.
  • Use of dynamic SQL using LINQ-to-SQL instead of stored procedures for a super fast coding. Remember that we're doing this for fun.
  • No use of Application Variables. I've seen some examples on the web that uses application variables and I just don't like this idea.
The Fun Begins!

1. First, we need to build our database using MS SQL Server 2000/2005. Note that we will only create the bare minimum just to get the web chat application going. Listed below are the table names and their primary usage for our chat application.

LINQ Chatroom table structure
  • User: Contains user information. Feel free to add your own fields like address, city, and so on.
  • Message: Will hold the messages sent by the users while chatting.
  • Room: Contains information about different rooms. This means that you can have more than one room. But for the purposes of this tutorial, we will only use one room for now.
  • LoggedInUsers: Will hold users logged-in/chatting in the chatroom(s). In short, if a user enters in a room, we will save their information here, in this way, we can show the list of users chatting in a specific room.
2. Create an ASP.Net 3.5 website using Visual Studio 2008. For now, let's create this in C# 3.5. I should have the VB 9.0 version in a week (so check back for the VB version).

3. Create a Login page. Our chat application will require all users to be logged in. Creation of the registration page for a new user will not be discussed in this tutorial. Although you can see that the username and password are in plain text, in the real world I recommend using hashed values instead. The login page is really simple; if you’re authenticated then you will be redirected to room 1. Again, you can customize this so that your users can pick from a number of rooms.

4. Create the Chatroom page.

First let's talk about the main structure of the GUI (graphical user interface). As shown on the the picture, we have the following:

LINQ GUI structure
  1. Div tag for Messages: This will house/show/contain the messages that are sent by the chatters. No frames here.
  2. TextBox: This is where chatters will be typing their messages.
  3. Send Button: Sends the messages when clicked.
  4. Div tag for Chatters: Shows the chatters logged-in to the current room.
  5. Log-out Button: Logs out the user from the current room.
  6. Update Panel: This is not a visible part of the GUI, but is one of the most important part of the chat application. This panel will make sure that the messages we see is "Flicker-Free".
  7. Timer Control: This is also not visible in the GUI. The function of the timer control is to refresh our page every 7 seconds.
That's it for now, we will look at these components in more detail in a little bit.

Now let's look at the flow of the web application.
  1. All chatters must be authenticated. In short, everyone who wants to chat must login. The Default.aspx is our login page, all unauthenticated users will be redirected here. This is easily configured in our web.config file. See code below:

       50         <authentication mode="Forms">
       51             <forms name=".ASPXAUTH" loginUrl="Default.aspx"/>
       52         </authentication>
       53 
       54         <authorization>
       55             <deny users="?" />
       56         </authorization>


    Save the UserID in a Session before redirecting the user to the chat room page. For simplicity's sake, the user is then automatically redirected to "room 1" when authenticated. Note: You can redirect the user to a page where you have a list of chatrooms and have your user pick from the list before you redirect them to the chat room page, simply add rooms to the Room table, and list them in a page.

       13         protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
       14         {
       15             LinqChatDataContext db = new LinqChatDataContext();
       16 
       17             var user = (from u in db.Users
       18                         where u.Username == Login1.UserName
       19                         && u.Password == Login1.Password
       20                         select u).SingleOrDefault();
       21 
       22             if (user != null)
       23             {
       24                 e.Authenticated = true;
       25                 Session["ChatUserID"] = user.UserID;
       26                 Session["ChatUsername"] = user.Username;
       27             }
       28             else
       29             {
       30                 e.Authenticated = false;
       31             }
       32         }
       33 
       34         protected void Login1_LoggedIn(object sender, EventArgs e)
       35         {
       36             Response.Redirect("Chatroom.aspx?roomId=1");
       37         }


  2. The user then arrives in the chat room page. The first thing we check is the roomID from the query string. Again for simplicity's sake, the code below assumes that you passed an integer value for the roomID. You should, however, make sure that the roomID is not null and can be converted to an integer. The code below also shows that the roomID value is assigned to an invisible Label Control called lblRoomID. We could have just as easily saved this value in a Session variable, but this is my preference since we don't really need to know the roomID in other pages than the one we're currently in. We need to know the roomID value throughout the page like when we're retrieving messages for this room, or when we're inserting messages for this room, or when we're retrieving users for this room, or when we're logging users out of this room, etc., you get the idea right?

       16                 // for simplity's sake we're going to assume that a
       17                 // roomId was passed in the query string and that
       18                 // it is an integer
       19                 // note: in reality you would check if the roomId is empty
       20                 // and is an integer
       21                 string roomId = (string)Request["roomId"];
       22                 lblRoomId.Text = roomId;


  3. We want to tell all chatters that this user just logged in. So we add/insert this user to the LoggedInUser table specific to this room. Then we retrieve and show all the users for this room.

    The call to insert a message:

       26 this.InsertMessage(ConfigurationManager.AppSettings["ChatLoggedInText"] + " " + DateTime.Now.ToString());


    The insert message method: Notice in line 84 that I'm removing the less than characters "<", simply because it blows up the app and I'm in no mood to make my web app a little bit insecure by setting the ValidateRequest to false. I was in a hurry when I wrote this web chat, now I'm thinking I should have used the AJAX Control Toolkit's Filtered Text Box instead of a regular text box where you type your messages. Oh well, maybe in part 2.

       74         private void InsertMessage(string text)
       75         {
       76             LinqChatDataContext db = new LinqChatDataContext();
       77 
       78             Message message = new Message();
       79             message.RoomID = Convert.ToInt32(lblRoomId.Text);
       80             message.UserID= Convert.ToInt32(Session["ChatUserID"]);
       81 
       82             if (String.IsNullOrEmpty(text))
       83             {
       84                 message.Text = txtMessage.Text.Replace("<", "");
       85                 message.Color = ddlColor.SelectedValue;
       86             }
       87             else
       88             {
       89                 message.Text = text;
       90                 message.Color = "gray";
       91             }
       92 
       93             message.ToUserID = null;            // in the future, we will use this value for private messages
       94             message.TimeStamp = DateTime.Now;
       95             db.Messages.InsertOnSubmit(message);
       96             db.SubmitChanges();
       97         }


    In the web.config file:

       24     <appSettings>
       25         <add key="ChatLoggedInText" value="Just logged in!"/>
       26     </appSettings>


    Then we add/insert a message in the Message table saying that this user just logged in. We also need to retrieve the messages and show the updated messages in the messages box. And yes, we add or retrieve data specific to this room. Rather than I keep saying "specific to this room" all the time, I would like to say that everything we do in this chat room page is specific to this room, which simply means, we need to pass in the roomID.

      148         /// <summary>
      149         /// Get the last 20 messages for this room
      150         /// </summary>
      151         private void GetMessages()
      152         {
      153             LinqChatDataContext db = new LinqChatDataContext();
      154 
      155             var messages = (from m in db.Messages
      156                            where m.RoomID == Convert.ToInt32(lblRoomId.Text)
      157                            orderby m.TimeStamp descending
      158                            select m).Take(20).OrderBy(m => m.TimeStamp);
      159 
      160             if (messages != null)
      161             {
      162                 StringBuilder sb = new StringBuilder();
      163                 int ctr = 0;    // toggle counter for alternating color
      164 
      165                 foreach (var message in messages)
      166                 {
      167                     // alternate background color on messages
      168                     if (ctr == 0)
      169                     {
      170                         sb.Append("<div style='padding: 10px;'>");
      171                         ctr = 1;
      172                     }
      173                     else
      174                     {
      175                         sb.Append("<div style='background-color: #EFEFEF; padding: 10px;'>");
      176                         ctr = 0;
      177                     }
      178 
      179                     if (message.User.Sex.ToString().ToLower() == "m")
      180                         sb.Append("<img src='Images/manIcon.gif' style='vertical-align:middle' alt=''>  " + message.Text + "</div>");
      181                     else
      182                         sb.Append("<img src='Images/womanIcon.gif' style='vertical-align:middle' alt=''>  " + message.Text + "</div>");
      183                 }
      184 
      185                 litMessages.Text = sb.ToString();
      186             }
      187         }


  4. We also need to retrieve all the users logged in to this room so we can list them in the users div tag. Notice that users other than yourself will be clickable, this is because in part 2, I will show you how to send a private message to another chatter.

       99         private void GetLoggedInUsers()
      100         {
      101             LinqChatDataContext db = new LinqChatDataContext();
      102 
      103             // let's check if this authenticated user exist in the
      104             // LoggedInUser table (means user is logged-in to this room)
      105             var user = (from u in db.LoggedInUsers
      106                         where u.UserID == Convert.ToInt32(Session["ChatUserID"])
      107                         && u.RoomID == Convert.ToInt32(lblRoomId.Text)
      108                         select u).SingleOrDefault();
      109 
      110             // if user does not exist in the LoggedInUser table
      111             // then let's add/insert the user to the table
      112             if (user == null)
      113             {
      114                 LoggedInUser loggedInUser = new LoggedInUser();
      115                 loggedInUser.UserID = Convert.ToInt32(Session["ChatUserID"]);
      116                 loggedInUser.RoomID = Convert.ToInt32(lblRoomId.Text);
      117                 db.LoggedInUsers.InsertOnSubmit(loggedInUser);
      118                 db.SubmitChanges();
      119             }
      120 
      121             string userIcon;
      122             StringBuilder sb = new StringBuilder();
      123 
      124             // get all logged in users to this room
      125             var loggedInUsers = from l in db.LoggedInUsers
      126                                 where l.RoomID == Convert.ToInt32(lblRoomId.Text)
      127                                 select l;
      128 
      129             // list all logged in chat users in the user list
      130             foreach (var loggedInUser in loggedInUsers)
      131             {
      132                 // show user icon based on sex
      133                 if (loggedInUser.User.Sex.ToString().ToLower() == "m")
      134                     userIcon = "<img src='Images/manIcon.gif' style='vertical-align:middle' alt=''>  ";
      135                 else
      136                     userIcon = "<img src='Images/womanIcon.gif' style='vertical-align:middle' alt=''>  ";
      137 
      138                 if (loggedInUser.User.Username != (string)Session["ChatUsername"])
      139                         sb.Append(userIcon + "<a href=#>" + loggedInUser.User.Username + "</a><br>");
      140                 else
      141                     sb.Append(userIcon + "<b>" + loggedInUser.User.Username + "</b><br>");
      142             }
      143 
      144             // holds the names of the users shown in the chatroom
      145             litUsers.Text = sb.ToString();
      146         }


  5. So in simple terms this is what happens. You log-in, when authenticated you get redirected to the chatroom page, when you get to the chatroom page a message is shown on the screen to alert other chatters saying that you logged in.
Now that we know the flow of the web application, let's move on to the details of how certain things work. We will look at the events that happen when an event is triggered.

Sending a Message:
  1. You will notice that the TextBox control where you type your messages keeps the focus even after you hit the "Enter Key" or when you directly click the send button. This is done in two (2) places:

    In the BtnSend_Click event:

       58 ScriptManager1.SetFocus(txtMessage.ClientID);


    And in the Timer1_OnTick event:

       68 ScriptManager1.SetFocus(txtMessage);


  2. You will also notice that you don't have to directly click the "send button" to send a message, you can just hit the "enter key" from your keyboard and it gets the same effect as if you directly hit the send button. Code shown below.

    <form id="form1" defaultbutton="btnSend" defaultfocus="txtMessage" runat="server" >



  3. There are a few things that happen when you send a message. Here they are in order
    • The message is inserted in the Message table.
    • The messages are retrieved from the Message table.
    • Messages are then shown on the message box. Based on the sex of the chatter, a man or woman icon is shown along with messages.
    • Users are retrieved from the LoggedInUser table.
    • Users are then reconstructed and shown in the list of chatters box. You will notice that all the other users except you is shown as a link. This is because in the next installment of this tutorial we will add on to this code and make other users clickable so that you can chat privately. For now, it's just a placeholder for future code.

  4. You will also notice that the scroll bar on the div tag for the messages are always set to the bottom of the div, this is because we are calling a client-side function that sets the position of the div scroll to the bottom every single time the page re-loads.

    From the body tag:

    <body style="background-color: gainsboro;" onload="SetScrollPosition()" onunload="LogMeOut()">



    The client side function:

    function SetScrollPosition()

    {

          var div = document.getElementById('divMessages');

          div.scrollTop = 100000000000;

    }

Logging Out:

There are two (2) ways to log out. You can log out by clicking the logout button or by closing your browser.
  1. Clicking the Log-Out button: When the user clicks the log-out button, this user is then deleted from the LoggedInUser table. A message is inserted in the Message table saying that this user logged out.

      189         protected void BtnLogOut_Click(object sender, EventArgs e)
      190         {
      191             // log out the user by deleting from the LoggedInUser table
      192             LinqChatDataContext db = new LinqChatDataContext();
      193 
      194             var loggedInUser = (from l in db.LoggedInUsers
      195                                where l.UserID == Convert.ToInt32(Session["ChatUserID"])
      196                                && l.RoomID == Convert.ToInt32(lblRoomId.Text)
      197                                select l).SingleOrDefault();
      198 
      199             db.LoggedInUsers.DeleteOnSubmit(loggedInUser);
      200             db.SubmitChanges();
      201 
      202             // insert a message that this user has logged out
      203             this.InsertMessage("Just logged out! " + DateTime.Now.ToString());
      204 
      205             // clean the session
      206             Session.RemoveAll();
      207             Session.Abandon();
      208 
      209             // redirect the user to the login page
      210             Response.Redirect("Default.aspx");
      211         }


  2. Closing the Browser: My guess is that most users will probably NOT click the Logout button, and instead, just close their browser when they leave the chatroom. The only place where we can capture this is on the client-side. And of course my favorite script on the client side is JavaScript. We can capture this using the "onunload" function, by placing this on the body tag as shown below.

    <body style="background-color: gainsboro;" onload="SetScrollPosition()" onunload="LogMeOut()">



    The client-side JavaScript that calls back a server-side method:

    function LogMeOut()

    {

          LogOutUserCallBack();

    }



    We also need to call a server-side method to delete this user from the LoggedInUser table asynchronously from the client side. Of course there are many ways to do this. I used the CallBack feature of ASP.Net. To do this, we simply need to implement the "ICallbackEventHandler" explicitly in our code behind file, as shown below.

        8    public partial class Chatroom : System.Web.UI.Page, System.Web.UI.ICallbackEventHandler


    The ICallbackEventHandler have 2 members that you need to explictly implement. The "RaiseCallbackEvent" is the event that handles our callback from the client side to the server side code. This is where we delete the user from the LoggedInUser table as shown below.

      220         void  System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
      221         {
      222             _callBackStatus = "failed";
      223 
      224             // log out the user by deleting from the LoggedInUser table
      225             LinqChatDataContext db = new LinqChatDataContext();
      226 
      227             var loggedInUser = (from l in db.LoggedInUsers
      228                                 where l.UserID == Convert.ToInt32(Session["ChatUserID"])
      229                                 && l.RoomID == Convert.ToInt32(lblRoomId.Text)
      230                                 select l).SingleOrDefault();
      231 
      232             db.LoggedInUsers.DeleteOnSubmit(loggedInUser);
      233             db.SubmitChanges();
      234 
      235             // insert a message that this user has logged out
      236             this.InsertMessage("Just logged out! " + DateTime.Now.ToString());
      237 
      238             _callBackStatus = "success";
      239         }


    The "GetCallbackResult" is the method where you can return a string to a JavaScript (client-side) function. So from the RaiseCallbackEvent you can assign a variable some value that is returned from the GetCallbackResult method. For our purposes, we can just return "success" or "failed" value respectively. Code is shown below.

      215         string  System.Web.UI.ICallbackEventHandler.GetCallbackResult()
      216         {
      217             return _callBackStatus;
      218         }


    The beauty of this asynchronous/call-back processing is possible because on the Page_Load part of the code, we registered the scripts for callback.

       29                 // create a call back reference so we can log-out user when user closes the browser
       30                 string callBackReference = Page.ClientScript.GetCallbackEventReference(this, "arg", "LogOutUser", "");
       31                 string logOutUserCallBackScript = "function LogOutUserCallBack(arg, context) { " + callBackReference + "; }";
       32                 Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "LogOutUserCallBack", logOutUserCallBackScript, true);
Message Retrieval and Page Refresh:

Let me talk about how we retrieve the messages from the database. There are two (2) primary ways on the way we retrieve the messages from the database to show to chatters.
  1. When we send a message: Every single time you hit that send button or the enter-key, a message is inserted in the Message table. In your chat window, you will also automatically retrieve the messages from the Message table. So, for example you're chatting with someone else, and the other chatter sent a message a second ago, that message will appear on top of message you just sent, and it will look like the other user just sent you a message. If the other user constantly sends messages, to that user, it will look like he/she is getting a response back asap, and there's no dry time when chatting.

  2. Every 7 seconds on the Timer Tick Event: If for some reason you just never send messages, the back-up is the Timer Control. The timer control will automatically refresh your window every 7 seconds to show the updated chat messages. First you would think that this is just too long. Not really, because would really just retrieve messages only when you don't send any messages for 7 seconds. The cool thing about this is; the timer counter will reset to zero (0) every single time you hit the send button or the enter key. This makes the performance of your web app a little faster.

  3. Lastly, the combination of the above mentioned retrieval process will give you better performance rather than refreshing your browser every second. It will also present the users a seemless and faster message retrieval.
Last Words:

Building this Web Chat Application was indeed fun for me, and most of all fast (about 2 hours) because of the use of LINQ. As a matter of fact, it took me a lot longer, way, way longer to write this article than to actually build the chat application in Visual Studio 2008. Of course you can improve on this chat application by adding to it, the article presented here is just to give you a taste of how fun and fast you would be able to build a chat application using LINQ and AJAX. I'm planning to write a second part to this article which will show you how you can, just as easily add a way to communicate (im other chatters privately) with other chatters privately. I will also provide the code to Visual Basic (VB 9.0) in the next week or so.


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, June 18, 2008