Build a Silverlight 2 Web Chat Application


Technologies Used: Silverlight 2, ASP.Net 3.5, WCF, C# 3.5, LINQ-to-SQL, MS SQL Server 2000/2005

Introduction:

Silverlight 2 is finally out and I'm thinking, the best way to learn it is to build a small web application using this wonderful technology. And that's exactly what we're going to do here. We're going to build a Web Chat using Silverlight 2. I'm also going to share some of the things I found out/learned with regards to this new technology. Here's a snapshot of the Silverlight 2 Web Chat application that we're going to build.

Silverlight 2 Chatroom

Requirements:

We will create a very simple web chat application using Silverlight 2 from scratch just for fun. This chat application will contain 2 Xaml user controls, the login control and the chatroom control. 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". You'll find out that all processing in Silverlight is done asynchronously.
  • We want to be able to monitor chat conversations using a database. We will use MS SQL Server to store conversations and user information.
  • Use of dynamic SQL using LINQ-to-SQL instead of stored procedures for a super fast coding.
The Fun Begins!

1. First, we need to build our database using MS SQL Server 2005/2008. For simplicity, we will go ahead and use the database that we built on the earlier blog, for the LINQ Web Chat Application in part 2. To see the Build a Web Chat Application Part 2 - Chat with Other Users Privately, click here.

Silverlight 2 Chat
  • 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. In Visual Studio, Create a new Silverlight Application project. You can do this by going to the File menu, select New, then click Project. In the New Project box, click Silverlight under the Project Types, then choose Silverlight Application under Templates, type in a name for this application. Then click ok.

On the next window, choose "Add a new ASP.Net Web Project...." and then click ok. In the solution explorer you will notice that a Silverlight application and an ASP.Net Web Project was created. We will use the generated web project to host our Silverlight 2 Chat application.

3. Let me talk about some of the things that were generated when we created this Silverlight application. In the Silverlight application you will notice that there are two (2) XAML files, App.xaml and Page.xaml. Xaml files, unlike ASP.Net webforms are user controls that needs to be hosted in an ASP.net Webform or Html page. All Xaml files, by default, can be hosted in a single web page. Of course you can also host one Xaml file/user control per web page, which I don't recommend.
  • App.xaml: Works kind-of-like the Global.asax in ASP.net. This is the very first control that is hit by a Silverlight application. Since Silverlight is a client-side technology, it does not have Sessions, nor can you do Response.Redirect to the next user control. This is where we can store variables/properties that can be available to other user controls, of course this is only possible if you're hosting the other user controls in the same ASP.Net page.

  • Page.xaml: By default, without changing any code, this is the user control that the generated ASP.Net web page will be hosting when you created your proeject. For the purposes of this tutorial, we will not use this user control. We will instead create our own and name them in accordance to their usage.

In the web application side, you will also notice the following files:
  • Default.aspx: This file is not used at all. So we will delete it in our project.

  • Chatroom.aspx: This is the actual page that will be hosting our Silverlight user control. You can set this as your start page. You will also notice that this file references a ".xap" file which is located in the ClientBin folder. When you look under the ClientBin folder, there's no .xap file when you first created your project. Build your web project, the .xap will now be under the ClientBin folder. The generated .xap file is the compiled Silverlight application.

  • Silverlight2ChatTestPage.html: Silverlight error page catcher. When a Silverlight error occurs, this page is shown as a pop up.

Here's a snapshot of the Silverlight 2 Chat project.

Silverlight 2 Chat Project

Now that you're familiar with a Silverlight application being hosted in an ASP.Net web page, we will now move on to the Web chat tutorial.

4. I created 2 user controls; Login.xaml and Chatroom.xaml, mainly so that I can show you how to move from one user control to next, and vice versa. I also wanted to show how to remember a user between user controls, somewhat a Session effect. Simple enough, you probably would have already guessed that these user controls are used to Login and Chat respectively.

5. You need to login before you can start chatting. The users must exist in your LinqChat User table. A room must also exist in the LinqChat Room table. This room must have a RoomID = 1, I hard-coded this in the Chatroom.xaml.cs. Of course you can have more than one room, but for the purposes of this tutorial, we only need one.

The XAML User Controls

In Silverlight 2 there are 3 basic XAML container controls; the Canvas, StackPanel, and the Grid control. In this project, we will be using the Grid and the StackPanel controls. The Grid works like an HTML table, rather than using TR for rows and TD for columns, it uses RowDefinitions and ColumnDefinitions respectively. The StackPanel can hold other XAML controls in a stack, either horizontally or vertically. For more information on these on controls please visit the silverlight website at http://www.silverlight.net.

Login.xaml

Login

Shown below is the XAML UI code that generates the simple login UI shown above. You will notice that all the XAML controls are contained in a Grid control. The positioning of the controls are very much self explanatory when you look at the green comments that I put there. There are a few things that I want to point out:

1. The error message controls are all "hidden", marked as Visibility="Collapsed". We control the visibility of these error messages in the code behind.

2. To mimic the ASP.Net validator controls I added the LostFocus and MouseEnter events in the User Name and Password controls.

<UserControl x:Class="Silverlight2Chat.Login"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="510" Height="118">
    <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
        <Grid.RowDefinitions>
            <RowDefinition Height="10" />       <!-- padding -->
            <RowDefinition Height="26" />       <!-- username -->
            <RowDefinition Height="6" />        <!-- padding -->
            <RowDefinition Height="26" />       <!-- password -->
            <RowDefinition Height="10" />       <!-- padding -->
            <RowDefinition Height="30" />       <!-- button -->
            <RowDefinition Height="6" />        <!-- padding -->
        </Grid.RowDefinitions>
 
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10" />     <!-- padding -->
            <ColumnDefinition Width="80" />     <!-- labels -->
            <ColumnDefinition Width="10" />     <!-- padding -->
            <ColumnDefinition Width="200" />    <!-- controls -->
            <ColumnDefinition Width="10" />     <!-- padding -->
            <ColumnDefinition Width="*" />      <!-- error messages -->
            <ColumnDefinition Width="10" />     <!-- padding -->
        </Grid.ColumnDefinitions>
 
        <!-- labels -->
        <TextBlock Text="User Name:" Grid.Row="1" Grid.Column="1" FontSize="12" VerticalAlignment="Center" />
        <TextBlock Text="Password:" Grid.Row="3" Grid.Column="1" FontSize="12" VerticalAlignment="Center" />
 
        <!-- controls -->
        <TextBox x:Name="TxtUserName" Grid.Row="1" Grid.Column="3" FontSize="12" BorderThickness="2" 
                 LostFocus="TxtUserName_LostFocus" MouseEnter="TxtUserName_MouseEnter" />
 
        <PasswordBox x:Name="PbxPassword" Grid.Row="3" Grid.Column="3" FontSize="12" BorderThickness="2" 
                     LostFocus="PbxPassword_LostFocus" MouseEnter="PbxPassword_MouseEnter" />
 
        <Button x:Name="BtnLogin" Grid.Row="5" Grid.Column="3" Content="Login" FontSize="12" Click="BtnLogin_Click" />
 
        <!-- error messages -->
        <TextBlock x:Name="TxtbUserNameRequired" Text="User Name is Required!" Foreground="Red" 
                   Grid.Row="1" Grid.Column="5" FontSize="12" VerticalAlignment="Center" Visibility="Collapsed" />
 
        <TextBlock x:Name="TxtbPasswordRequired" Text="Password is Required!" Foreground="Red" 
                   Grid.Row="3" Grid.Column="5" FontSize="12" VerticalAlignment="Center" Visibility="Collapsed" />
 
        <TextBlock x:Name="TxtbNotfound" Text="Invalid Username or Password!" Foreground="Red" 
                   Grid.Row="5" Grid.Column="5" FontSize="12" VerticalAlignment="Center" Visibility="Collapsed" />
    </Grid>
</UserControl>


When the user clicks the Login button, a variable that tell's us that the user already clicked the Login button at least once is set to true. This will help us validate the username or password control from the client-side using the LostFocus and MouseEnter events, without the need for the user to re-click the Login button, therefore mimicking the ASP.Net validation controls before we actual validate the username and password on the server.

   24     private void BtnLogin_Click(object sender, RoutedEventArgs e)
   25     {
   26         _isLoginButtonClicked = true;
   27         ValidateUserName();
   28         ValidatePassword();
   29 
   30         if (!String.IsNullOrEmpty(TxtUserName.Text) && !String.IsNullOrEmpty(PbxPassword.Password))
   31         { 
   32             // validate user based on the username and password
   33             ValidateUser();
   34         }
   35     }


The ValidateUserName and ValidatePassword methods only checks if the User Name and Password boxes are not empty, then shows or hides the error message accordingly.

   49     private void ValidateUserName()
   50     {
   51         if (String.IsNullOrEmpty(TxtUserName.Text))
   52             TxtbUserNameRequired.Visibility = Visibility.Visible;
   53         else
   54             TxtbUserNameRequired.Visibility = Visibility.Collapsed;
   55     }
   56 
   57     private void ValidatePassword()
   58     {
   59         if (String.IsNullOrEmpty(PbxPassword.Password))
   60             TxtbPasswordRequired.Visibility = Visibility.Visible;
   61         else
   62             TxtbPasswordRequired.Visibility = Visibility.Collapsed;
   63     }


In the ValidateUser method, we're using a WCF (Windows Communication Foundation) service to validate the user name and password entered by the user. If the user is found in the database, the user is then redirected to the chatroom XAML, if not, an error messages is shown. We will go back on this in a little bit, but first let me talk about the WCF Service.

The Windows Communication Foundation (WCF) Service

WCF Service

Since Silverlight is a client-side technology, there are a few ways to handle database access, all of which uses some kind of AJAX technology, or JavaScript. I prefer using WCF over classic Web Service (.asmx) technology. To add a WCF Service, right-click on the web project, select Add new item, then select "WCF Service", as shown below. When you click Add, 3 files are added to the web project; a ".svc" file, the respective code file ".svc.cs", and an Interface file "I.....cs". An entry in the Web.config file is also inserted.

Interface

This interface file serves as a base interface for the WCF service. This is where we define all the methods that the WCF service is going to implement. You will also notice that the interface is marked as a ServiceContract. Each one of the methods are marked as an OperationContract.

   12     [ServiceContract]
   13     public interface ILinqChatService
   14     {
   15         [OperationContract]
   16         int UserExist(string username, string password);
   17 
   18         [OperationContract]
   19         List<MessageContract> GetMessages(int messageID, int roomID, DateTime timeUserJoined);
   20 
   21         [OperationContract]
   22         void InsertMessage(int roomID, int userID, int? toUserID, string messageText, string color);
   23 
   24         [OperationContract]
   25         List<UserContract> GetUsers(int roomID, int userID);
   26 
   27         [OperationContract]
   28         void LogOutUser(int userID, int roomID, string username);
   29     }


You will also notice that I created 2 public classes inside this interface. The classes simply defines the properties of the respective DataContract. Also, you will notice that the data members of the MessageContract class directly maps to the Message table, and the UserContract class data members directly maps to the User table in the database. Note: I only added the data members we will be using for the purposes of this tutorial. Also, because I'm a bit a lazy when I did this, I embedded these 2 classes inside the interface, I could have as easily made two separate public classes instead.

   31     [DataContract]
   32     public class MessageContract
   33     {
   34         [DataMember]
   35         public int MessageID;
   36 
   37         [DataMember]
   38         public string Text;
   39 
   40         [DataMember]
   41         public string UserName;
   42 
   43         [DataMember]
   44         public string Color;
   45     }
   46 
   47     [DataContract]
   48     public class UserContract
   49     {
   50         [DataMember]
   51         public int UserID;
   52 
   53         [DataMember]
   54         public string UserName;
   55     }


Implementing the Interface in the LinqChatService

The interface is implemented in the WCF service code behind file, "LinqChatService.svc.cs".  To implement the interface, first, we need to inherit the interface as shown below. The inheritance is coded by default.


   11     public class LinqChatService : ILinqChatService



Right-click on the inteface "ILinqChatService", select Implement Interface, then select Implement Interface Explicitly as shown below. This will generate the interface's members inside a region tag.

Implement an Interface

Interface Member Methods

Now that you know how to implement the members of an interface, we will now talk about the implementation of each of the member methods. Each of the member methods accesses the database through the use of LINQ-to-SQL. I also named each one of the methods sensibly enough based on their operation.

1. InsertMessage: We use this method to insert one message in the database at a time. This operation is called when a user types a message in the chat room and then hits the send button.

   13     void ILinqChatService.InsertMessage(int roomID, int userID, int? toUserID, string messageText, string color)
   14     {
   15         Message message = new Message();
   16         message.RoomID = roomID;
   17         message.UserID = userID;
   18         message.ToUserID = toUserID;
   19         message.Text = messageText;
   20         message.Color = color;
   21         message.TimeStamp = DateTime.Now;
   22 
   23         LinqChatDataContext db = new LinqChatDataContext();
   24         db.Messages.InsertOnSubmit(message);
   25 
   26         try
   27         {
   28             db.SubmitChanges();
   29         }
   30         catch (Exception)
   31         {
   32             throw;
   33         }
   34     }


2. GetMessages: We use this method to get the messages for a specific room from the time the logged-in user joined the room. This only gets the messages that have not been retrieved yet, this is the reason why we're passing the messageID of the last message that we retrieved from the previous calls on this method. The code timeUserJoined.AddSeconds(1) highlighted below restrains the messages retrieved 1 second past the the time the logged-in user joined the room, this is because, when a user joins the room, a message is inserted in the database saying "user joined the room". All the other chatters will see this message, except the logged-in user.

New User Joins Room

Another thing to note is the foreach loop. First thing that comes to mind is: why can't we just return a Generic list of type List<Message>? The interface does not understand complex types unless defined explictly as a DataContract. This is the reason why we created an almost exact copy of the Message table members and explicitly defined it as a MessageContract class, where each member of the DataContract is a DataMember.

   36     List<MessageContract> ILinqChatService.GetMessages(int messageID, int roomID, DateTime timeUserJoined)
   37     {
   38         LinqChatDataContext db = new LinqChatDataContext();
   39 
   40         var messages = (from m in db.Messages
   41                         where m.RoomID == roomID &&
   42                         m.MessageID > messageID &&
   43                         m.TimeStamp > timeUserJoined.AddSeconds(1)
   44                         orderby m.TimeStamp ascending
   45                         select new { m.MessageID, m.Text, m.User.Username, m.TimeStamp, m.Color });
   46 
   47         List<MessageContract> messageContracts = new List<MessageContract>();
   48 
   49         foreach (var message in messages)
   50         {
   51             MessageContract messageContract = new MessageContract();
   52             messageContract.MessageID = message.MessageID;
   53             messageContract.Text = message.Text;
   54             messageContract.UserName = message.Username;
   55             messageContract.Color = message.Color;
   56             messageContracts.Add(messageContract);
   57         }
   58 
   59         return messageContracts;
   60     }


3. GetUsers: This method gets all the users in a specific room. First it checks if the logged-in user is in the LoggedInUser table, if not, the user is inserted (line 75-82). The reason for this check is because I streamlined the inserting of the new user, when the user first enters the chatroom he/she is inserted into the LoggedInUser table. Every other call to get users just retrieves all the users from the database.

Just like the GetMessages method, we're assigning all the retrieved users to the UserContract Data Contract class (line 92-97).

   62     List<UserContract> ILinqChatService.GetUsers(int roomID, int userID)
   63     {
   64         LinqChatDataContext db = new LinqChatDataContext();
   65 
   66         // let's check if this authenticated user exist in the
   67         // LoggedInUser table (means user is logged-in to this room)
   68         var user = (from u in db.LoggedInUsers
   69                     where u.UserID == userID
   70                     && u.RoomID == roomID
   71                     select u).SingleOrDefault();
   72 
   73         // if user does not exist in the LoggedInUser table
   74         // then let's add/insert the user to the table
   75         if (user == null)
   76         {
   77             LoggedInUser loggedInUser = new LoggedInUser();
   78             loggedInUser.UserID = userID;
   79             loggedInUser.RoomID = roomID;
   80             db.LoggedInUsers.InsertOnSubmit(loggedInUser);
   81             db.SubmitChanges();
   82         }
   83 
   84         // get all logged in users to this room
   85         var loggedInUsers = from l in db.LoggedInUsers
   86                             where l.RoomID == roomID
   87                             orderby l.User.Username ascending
   88                             select new { l.User.Username };
   89 
   90         List<UserContract> userContracts = new List<UserContract>();
   91 
   92         foreach (var loggedInUser in loggedInUsers)
   93         {
   94             UserContract userContract = new UserContract();
   95             userContract.UserName = loggedInUser.Username;
   96             userContracts.Add(userContract);
   97         }
   98 
   99         return userContracts;
  100     }


4. UserExist: Checks if a user exist. If the user exist it returns the userID, if not it returns -1. This method is called from the Login.xaml code behind file to check if the username and password entered by the user exist in the database. Why am I returning the userID, and why just the userID? From the time the user logs in, the application remembers the userID and user name of that user, much like a Session effect. To minimize the data retrieved, all we really need at this point is the userID, since the user name was already provided through the username TextBox from the login page.

  102     int ILinqChatService.UserExist(string username, string password)
  103     {
  104         int userID = -1;
  105 
  106         LinqChatDataContext db = new LinqChatDataContext();
  107 
  108         var user = (from u in db.Users
  109                     where u.Username == username
  110                     && u.Password == password
  111                     select new { u.UserID }).SingleOrDefault();
  112 
  113         if (user != null)
  114             userID = user.UserID;
  115 
  116         return userID;
  117     }


5. LogOutUser: Simple, logs out the user. Lines 124-130 deletes the user from the LoggedInUser table. Lines 133-142 inserts a message in the Message table saying that the user left the room so that the other users can see this message when a user logs out. Again I streamlined the database access, rather than doing another query based on the userID to get the username of the user that is logging out, this method expects the username as highlighted below. This is the reason why we're remembering the username of the current user logged in to this chatroom, I'll talk about how this is done in a bit. This method is called when you click the logout button in the Chatroom.xaml UI.

Note: You can also easily log out a user when the user clicks the close button of a browser by catching the onunload event of the body tag. For more information on how to do this, please see my other article titled Build a Web Chat Application using ASP.Net 3.5, LINQ and AJAX .

  119     void ILinqChatService.LogOutUser(int userID, int roomID, string username)
  120     {
  121         // log out the user by deleting from the LoggedInUser table
  122         LinqChatDataContext db = new LinqChatDataContext();
  123 
  124         var loggedInUser = (from l in db.LoggedInUsers
  125                             where l.UserID == userID
  126                             && l.RoomID == roomID
  127                             select l).SingleOrDefault();
  128 
  129         db.LoggedInUsers.DeleteOnSubmit(loggedInUser);
  130         db.SubmitChanges();
  131 
  132         // insert user "left the room" text
  133         Message message = new Message();
  134         message.RoomID = roomID;
  135         message.UserID = userID;
  136         message.ToUserID = null;
  137         message.Text = username + " left the room.";
  138         message.Color = "Gray";
  139         message.TimeStamp = DateTime.Now;
  140 
  141         db.Messages.InsertOnSubmit(message);
  142         db.SubmitChanges();
  143     }


Web.config and WCF

When we added a WCF Service to our web application a few lines lines of code was also automatically added to the Web.config file in the system.ServiceModel tag. Although everything here is standardized, one thing I want to point out is the binding information in line 126. By default it will be coded as "wsHttpBinding", we need to change it to basicHttpBinding as shown and highlighted below.

  109     <system.serviceModel>
  110         <behaviors>
  111             <endpointBehaviors>
  112                 <behavior name="Silverlight2Chat.Web.Service1AspNetAjaxBehavior">
  113                     <enableWebScript />
  114                 </behavior>
  115             </endpointBehaviors>
  116             <serviceBehaviors>
  117                 <behavior name="Silverlight2Chat.Web.LinqChatServiceBehavior">
  118                     <serviceMetadata httpGetEnabled="true" />
  119                     <serviceDebug includeExceptionDetailInFaults="false" />
  120                 </behavior>
  121             </serviceBehaviors>
  122         </behaviors>
  123         <services>
  124             <service behaviorConfiguration="Silverlight2Chat.Web.LinqChatServiceBehavior"
  125                 name="Silverlight2Chat.Web.LinqChatService">
  126                 <endpoint address="" binding="basicHttpBinding" contract="Silverlight2Chat.Web.ILinqChatService">
  127                     <identity>
  128                         <dns value="localhost" />
  129                     </identity>
  130                 </endpoint>
  131                 <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
  132             </service>
  133         </services>
  134     </system.serviceModel>


Remembering Information and Moving from One XAML User Control to the Next

As I mentioned earlier, Silverlight is a client-side technology. This means that we cannot use Session Objects to remember things nor can we use the famous Response.Redirect command to go to the next page/XAML user control. Although you can host one XAML file per ASP.Net WebForm, Silverlight is not designed this way, you should flip or move from one XAML file to the next hosted by the same ASP.Net WebForm or Html. By default, when you create a Silverlight application, a Page.xaml file is generated. This is also the default XAML user control that is used or hosted when you run your Silverlight application. We don't really need the Page user control, so I deleted it. Instead, I added 2 user controls; Login.xaml and Chatroom.xaml. We will be moving back and forth from the Login.xaml to the Chatroom.xaml and vice versa using the App.xaml web user control.

App.xaml is the Silverlight application-wide user control. This where we can set-up which user control will be shown or called first in our application. This is also where we can remember things like the userID and user name. It also works like the Web.config where you can store your application-wide resources.

1. Setting Login.xaml as the default XAML user control: In the Application_Startup event of the App.xaml simply change "Page()" to "Login()" as shown in line 32.

   28     private void Application_Startup(object sender, StartupEventArgs e)
   29     {
   30         // start at the login page
   31         this.RootVisual = rootGrid;
   32         rootGrid.Children.Add(new Login());
   33     }


2. To move from the Login.xaml user control to the Chatroom.xaml and vice versa, I created a method called RedirectTo in the App.xaml user control that accepts a User Control where we want to be redirected to. What we're doing here is removing the current user control and adding the user control that we want to show to the user. This method is called from the login xaml after a user logs in the chatroom and from the chatroom xaml when a user logs out. Code is shown below.

   69     public void RedirectTo(UserControl usercontrol)
   70     {
   71         App app = (App)Application.Current;
   72         app.rootGrid.Children.Clear();
   73         app.rootGrid.Children.Add(usercontrol);
   74     }


To redirect a user from the Login.xaml to the Chatroom.xaml, we do the following from the Login.xaml user control:

   81     App app = (App)Application.Current;
   82     app.UserID = userID;
   83     app.UserName = TxtUserName.Text;
   84     app.RedirectTo(new Chatroom());


3. To remember a value from one XAML User Control to the next, we need to create a public property for each of the values that we want remembered from the App.xaml user control.

   76     public int UserID { get; set; }
   77 
   78     public string UserName { get; set; }
   79 
   80     public DateTime TimeUserJoined { get; set; }


Now that these properties are created we simply need to assign the values we want remembered from either the Login.xaml or the Chatroom.xaml. Code shown below from the Login.xaml.

   81     App app = (App)Application.Current;
   82     app.UserID = userID;
   83     app.UserName = TxtUserName.Text;


Chatroom.xaml UI

There are a few things that I want to point out about the chatroom UI and all of them are highlighted below. You will notice that both the Messages and the User List are using Stack Panel controls, but the data binding are different. The User List is using the DataTemplate tag which is a lot simpler, and binding all the UserName as a Hyperlink. On the other hand, the messages are bound in the code behind. This is because the messages needs to show a more complex set of controls. As you have noticed in the snapshot of this chatroom, the messages can be of different colors, the names and the message right beside them don't need to have the same colors. I will discuss more on this in a little bit.

A KeyDown event for the message TextBox (where the user types their messages) is also supplied so that we can check if the user hit the "enter key" of the keyboard. When the user hits the enter key, the message is sent. Again, I will talk more about this a bit later.

<UserControl x:Class="Silverlight2Chat.Chatroom"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="600" Height="340">
    <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False" Loaded="LayoutRoot_Loaded">
        <Grid.RowDefinitions>
            <RowDefinition Height="10" />       <!-- padding -->
            <RowDefinition Height="38" />       <!-- title -->
            <RowDefinition Height="10" />       <!-- padding -->
            <RowDefinition Height="*" />        <!-- messages, userlist -->
            <RowDefinition Height="10" />       <!-- padding -->       
            <RowDefinition Height="26" />       <!-- message text box, send button -->
            <RowDefinition Height="10" />       <!-- padding -->
        </Grid.RowDefinitions>
 
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10" />     <!-- padding -->
            <ColumnDefinition Width="*" />      <!-- messages, message text box-->
            <ColumnDefinition Width="10" />     <!-- padding -->
            <ColumnDefinition Width="120" />    <!-- user list, send button-->
            <ColumnDefinition Width="10" />     <!-- padding -->
        </Grid.ColumnDefinitions>
 
        <TextBlock Text="Silverlight 2 Chat" Grid.Row="1" Grid.Column="1" FontSize="22" Foreground="Navy" />
 
        <StackPanel Orientation="Vertical" Grid.Row="1" Grid.Column="3">
            <TextBlock x:Name="TxtbLoggedInUser" FontSize="10" Foreground="Navy" FontWeight="Bold" HorizontalAlignment="Center" />
            <Button x:Name="BtnLogOut" Content="Log Out" FontSize="10" Click="BtnLogOut_Click" />
        </StackPanel>
 
        <ScrollViewer x:Name="SvwrMessages" Grid.Row="3" Grid.Column="1" 
                      HorizontalScrollBarVisibility="Hidden" 
                      VerticalScrollBarVisibility="Visible" BorderThickness="2">
            <StackPanel x:Name="SpnlMessages" Orientation="Vertical" />
        </ScrollViewer>
 
        <ScrollViewer x:Name="SvwrUserList" Grid.Row="3" Grid.Column="3" 
                      HorizontalScrollBarVisibility="Auto" 
                      VerticalScrollBarVisibility="Auto" BorderThickness="2">
            <StackPanel x:Name="SpnlUserList" Orientation="Vertical">
                <ItemsControl x:Name="ItmcUserList">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <HyperlinkButton Content="{Binding UserName}" />
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>   
            </StackPanel>
        </ScrollViewer>
 
        <StackPanel Orientation="Horizontal" Grid.Row="5" Grid.Column="1" >
            <TextBox x:Name="TxtMessage" TextWrapping="Wrap" KeyDown="TxtMessage_KeyDown"  
                 ScrollViewer.VerticalScrollBarVisibility="Visible" 
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 Width="360"
                 BorderThickness="2" Margin="0,0,10,0"/>  
 
            <ComboBox x:Name="CbxFontColor" Width="80">
                <ComboBoxItem Content="Black" Foreground="White" Background="Black" IsSelected="True" />
                <ComboBoxItem Content="Red" Foreground="White" Background="Red" />
                <ComboBoxItem Content="Blue" Foreground="White" Background="Blue" />
            </ComboBox>
        </StackPanel>
 
        <Button x:Name="BtnSend" Content="Send" Grid.Row="5" Grid.Column="3" Click="BtnSend_Click" />
    </Grid>
</UserControl>


Chatroom.xaml.cs Code Behind

1. When the user is redirected to the Chatroom.xaml user control from the Login.xaml user control is; We check if user is logged-in by checking any of the values we stored in App.xaml, I chose to check for the user name (line 34). If this value is empty, then the user have not logged-in yet, and therefore needs to be redirected to the login page (line 36).

   32     App app = (App)Application.Current;
   33 
   34     if (String.IsNullOrEmpty(app.UserName))
   35     {
   36         app.RedirectTo(new Login());
   37     }
   38     else
   39     {
   40         _userID = app.UserID;
   41         _timeUserJoined = DateTime.Now;
   42         TxtbLoggedInUser.Text = app.UserName;
   43     }


2. Since we're using a Grid named "LayoutRoot" as our root or main container for all the other controls, the LayoutRoot_Loaded event is called when the Grid is loaded. And yes, it mimics the Page_load event of an ASP.Net page.

   46     private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
   47     {            
   48         TxtMessage.Focus();
   49         InsertNewlyJoinedMessage();
   50         GetUsers();
   51         SetTimer();
   52     }


As you can see, a few things happen when the grid is loaded. One thing in particular that I want to discuss here is the focusing of the TxtMessage TextBox control, which is where you type your messages. The TxtMessage.Focus() command shown above in line 48 does not work by itself alone, there are a few things that we need to do to focus on this control when the grid is loaded.

The first thing we need to do along with the TxtMessage.Focus() code is to put a focus on the Silverlight ASP.Net Control in the hosting web page, which is the Chatroom.aspx. As shown below, we can focus on the Silverlight ASP.Net control using JavaScript.

<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Register Assembly="System.Web.Silverlight" Namespace="System.Web.UI.SilverlightControls" TagPrefix="asp" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" style="height:100%;">
<head id="Head1" runat="server">
    <title>Silverlight 2 Chatroom</title>
    <script type="Text/javascript">
        window.onload = function ()
        {
            document.getElementById('Xaml1').focus();
        }
    </script>
</head>
<body style="height:100%;margin:0; padding:0; width: 100%;">
    <form id="form1" runat="server" style="height:100%;">
       <asp:ScriptManager ID="ScriptManager1" runat="server" />
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <div style="width: 100%; text-align:center; height: 100%;">
                    <asp:Silverlight ID="Xaml1" runat="server" Source="~/ClientBin/Silverlight2Chat.xap" MinimumVersion="2.0.31005.0" Width="600" Height="100%" />
                </div>
            </ContentTemplate>
        </asp:UpdatePanel>
    </form>
</body>
</html>


3. Using a Proxy to Access WCF Service Asyncrhonously: The following methods and/or events in the Chatroom.xaml.cs code behind file all accesses the WCF Service that we created earlier.
  • InsertNewlyJoinedMessage()
  • GetUsers()
  • InsertMessage()
  • GetMessages()
  • BtnLogOut_Click event
Take note that all processing done in a Silverlight application is done Asynchronously. When you are trying to get values coming from a WCF Service into a Silverlight application, you will need to create a "Completed" event handler for the WCF proxy's Completed event as well as call the "Async" method of the WCF proxy. As an example, let's look at the GetUsers() method in the Chatroom.xaml.cs.

Line 57 and lines 61-68 are really not needed if you're not going to retrieve any value from the WCF Service, like when we insert a value in the database. However in this case, we're retrieving users and assigning the retrieved values in Silverlight controls. For this example, when the GetUsersAsync call has been completed, the GetUsersCompleted event handler is called. The retrieved users (or retrieved value) are then assigned to the e.Result event arguments of the GetUserCompleted event handler. The e.Result return type is dynamic based on the value being retrieved. So if we're just retrieving an int value, then it will be an int type, if we're retrieving a string value then it will be a string type and so on. For this specific example, we're retrieving a collection that's why were assigning it to an ObservableColletion type.

Note that the GetUsersAsync method in line 58 has the same signature as the ILinqChatService.GetUsers method in the WCF Service, this is how we call the WCF Service method.

   54     private void GetUsers()
   55     {
   56         LinqChatReference.LinqChatServiceClient proxy = new LinqChatReference.LinqChatServiceClient();
   57         proxy.GetUsersCompleted += new EventHandler<Silverlight2Chat.LinqChatReference.GetUsersCompletedEventArgs>(proxy_GetUsersCompleted);
   58         proxy.GetUsersAsync(_roomId, _userID);
   59     }
   60 
   61     void proxy_GetUsersCompleted(object sender, Silverlight2Chat.LinqChatReference.GetUsersCompletedEventArgs e)
   62     {
   63         if (e.Error == null)
   64         {
   65             ObservableCollection<LinqChatReference.UserContract> users = e.Result;
   66             ItmcUserList.ItemsSource = users;
   67         }
   68     }


Here's the GetUsers method that we are calling in the WCF Service (LinqChatService.svc.cs). Notice that it's called GetUsers and we did not create a GetUsersCompleted event handler or a GetUsersAsync method

   62      List<UserContract> ILinqChatService.GetUsers(int roomID, int userID)


4. Sending and Receiving Messages: You send messages when you click the enter key of your keyboard or when you hit the send button. When you click either the carriage return key or your keyboard or the send button, two other things are executed along with saving your message to the database, you get the messages from the database, as well as get the users from the database.

  209     private void SendMessage()
  210     {
  211         if(!String.IsNullOrEmpty(TxtMessage.Text))
  212         {
  213             InsertMessage();
  214             GetMessages();
  215             GetUsers();
  216         }
  217     }


Remember that we set up the timer when the main Grid loads.

   77     private void SetTimer()
   78     {
   79         timer = new DispatcherTimer();
   80         timer.Interval = new TimeSpan(0, 0, 0, 3, 0);
   81         timer.Tick += new EventHandler(TimerTick);
   82         timer.Start();
   83 
   84         _isTimerStarted = true;
   85     }


Every 3 seconds, when you don't hit the enter key of your keyboard, the timer tick event is called to get the messages and get the users from the database.

  219     void TimerTick(object sender, EventArgs e)
  220     {
  221         GetMessages();
  222         GetUsers();
  223     }


The timer is stopped every single time you type in a message, and then resumes when you hit the enter key. This is done so that we can stop the timer to refresh our TxtMessage Box control while we're typing.

  186     private void TxtMessage_KeyDown(object sender, KeyEventArgs e)
  187     {
  188         if (e.Key == Key.Enter)
  189         {
  190             SendMessage();
  191             timer.Start();
  192             _isTimerStarted = true;
  193         }
  194         else
  195         {
  196             if (_isTimerStarted)
  197             {
  198                 timer.Stop();
  199                 _isTimerStarted = false;
  200             }
  201         }
  202     }


5. Setting the Scroll Bar to the Bottom of the Messages: To set the scroll bar in the bottom of the messages we simply supply the highest double value int the ScrollToVerticalOffset member of the Scroll Viewer XAML control.

  179     private void SetScrollBarToBottom()
  180     {
  181         // set the scroll bar to the bottom
  182         SvwrMessages.UpdateLayout();
  183         SvwrMessages.ScrollToVerticalOffset(double.MaxValue);
  184     }


6. Showing the Messages in the Scroll View Control: Perhaps this is one of the most exciting part of this tutorial. While I was coding the Silverlight 2 Chat application this is where I spent most of my time in. Getting all the messages and assigning them all in a ListBox XAML control just does not work for me. First I want to be able to have a different color or different shade for the user and the message which is in the same line. I also wanted an alternating background for each of the message. This is accomplished by writting a little bit more code than we did with the Users list. The good thing about doing it this way is that; we don't have to retrieve all the messages for the current room, instead, we just retrieve the ones that we have not retrieved yet, because when you add a message to the panel control using this method, the UI remembers all that has been added, and there's no need to add them again.

In line 109, we're instantiating a horizontal stack panel which will be added to the base stack panel in line 167. We did this programmatically so that we can alternate the background color as shown in lines 155-116. We're also adding a TextBlock which will hold the bold username in line 130, and a TextBox which will hold the message in line 164, to this stack panel. That was simple enough.

So why am I using a TextBox (line 133) instead of a TextBlock for the message beside the Username? I noticed that there was a bug with Silverlight and I read up and sure enough I was right. When using the KeyDown event of the message box, carriage returns are then encoded in the messages that you type when you hit the enter key. What that means is that; the messages are cut in 2 lines in various places. For example: Let's say you typed "Hello how are you doing?" in the message text box, and then hit the enter key, the message "Hello how are you doing?" will be cut in two lines when you assign the Text value of the message box to a TextBlock, so it should read like this:

Hello how are
you doing?


or this:

Hello
how are you doing?


We simply don't have any control where the newline character is inserted, not that we want it to be inserted anyway. Because of this bug, I noticed that assigning the Text value of the message box to another TextBox fixes this problem.

  100     void proxy_GetMessagesCompleted(object sender, Silverlight2Chat.LinqChatReference.GetMessagesCompletedEventArgs e)
  101     {
  102         if (e.Error == null)
  103         {
  104             ObservableCollection<LinqChatReference.MessageContract> messages = e.Result;
  105 
  106             foreach (var message in messages)
  107             {
  108                 // add a horizontal stack panel
  109                 StackPanel sp = new StackPanel();
  110                 sp.Orientation = Orientation.Horizontal;
  111                 sp.HorizontalAlignment = HorizontalAlignment.Left;
  112                 sp.Width = SpnlMessages.ActualWidth;
  113 
  114                 // put an alternating background
  115                 if (!_isWithBackground)
  116                     sp.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(100, 235, 235, 235));
  117 
  118                 // add a TextBlock to hold the user's name to the stack panel
  119                 TextBlock name = new TextBlock();
  120                 name.Text = message.UserName + ": ";
  121                 name.FontSize = 12.0;
  122                 name.FontWeight = FontWeights.Bold;
  123                 name.Padding = new Thickness(4, 8, 0, 8);
  124 
  125                 if (message.Color == "Gray")
  126                     name.Foreground = new SolidColorBrush(Colors.Gray);
  127                 else
  128                     name.Foreground = new SolidColorBrush(Colors.Black);
  129 
  130                 sp.Children.Add(name);
  131 
  132                 // add a TextBox to hold the user's message to the stack panel
  133                 TextBox text = new TextBox();
  134                 text.BorderBrush = new SolidColorBrush(Colors.Transparent);
  135                 text.FontSize = 12.0;
  136                 text.Text = message.Text.Trim();
  137                 text.VerticalAlignment = VerticalAlignment.Top;
  138                 text.Width = SpnlMessages.ActualWidth - name.ActualWidth;
  139                 text.TextWrapping = TextWrapping.Wrap;
  140                 text.Margin = new Thickness(0, 4, 4, 0);
  141                 text.IsReadOnly = true;
  142 
  143                 // change text color based on the user's chosen color
  144                 if(message.Color == "Red")
  145                     text.Foreground = new SolidColorBrush(Colors.Red);
  146                 else if (message.Color == "Blue")
  147                     text.Foreground = new SolidColorBrush(Colors.Blue);
  148                 else if (message.Color == "Gray")
  149                     text.Foreground = new SolidColorBrush(Colors.Gray);
  150                 else
  151                     text.Foreground = new SolidColorBrush(Colors.Black);
  152 
  153                 // put an alternating background
  154                 if (!_isWithBackground)
  155                 {
  156                     text.Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(100, 235, 235, 235));
  157                     _isWithBackground = true;
  158                 }
  159                 else
  160                 {
  161                     _isWithBackground = false;
  162                 }
  163 
  164                 sp.Children.Add(text);
  165 
  166                 // add the horizontal stack panel to the base stack panel
  167                 SpnlMessages.Children.Add(sp);
  168 
  169                 // remember the last message id
  170                 _lastMessageId = message.MessageID;
  171             }
  172 
  173             SetScrollBarToBottom();
  174             TxtMessage.Text = String.Empty;
  175             TxtMessage.Focus();
  176         }
  177     }


7. Logging Out: As soon as you click the logout button we stop the timer (line 227). Then we delete the user in the LoggedInUser table by calling the WCF Service in line 229-230. Notice that we did not need to call a Completed event handler since we're not retrieving any values in the database. And lastly, we redirect the user to the Login XAML user control.

As I mentioned earlier, you can also log-out the user when they hit the close button of the browser by catching the unload event of the body tag in the hosting ASP.Net page.

  225     private void BtnLogOut_Click(object sender, RoutedEventArgs e)
  226     {
  227         timer.Stop();
  228 
  229         LinqChatReference.LinqChatServiceClient proxy = new LinqChatReference.LinqChatServiceClient();
  230         proxy.LogOutUserAsync(_userID, _roomId, TxtbLoggedInUser.Text);  
  231 
  232         // redirect to the login page
  233         App app = (App)Application.Current;
  234         app.RedirectTo(new Login());
  235     }


Last Words:

Building this Silverlight 2 Web Chat Application was a fun learning experience for me. On my next blog/tutorial, I will be talking about how to chat with someone privately. As you may have already noticed, the user links are left empty, of course I did this for a purpose. So watch out for my next blog.


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: Thursday, December 4, 2008