Click here to Skip to main content
15,867,686 members
Articles / Web Development / HTML

Build a Silverlight Web Chatroom with Multiple Rooms and Private Chat - Part 2

Rate me:
Please Sign up or sign in to vote.
4.68/5 (13 votes)
16 Mar 2009CPOL14 min read 121.2K   4.1K   75   46
In Part 1, we built a simple web chat using Silverlight 2. We are going to add functionalities so that users are able to choose from multiple chat rooms as well as chat privately with other users.

Introduction

In Part 1, we built a simple web chat using Silverlight 2. Now we are going to add functionalities so that users are able to choose from multiple chat rooms as well as chat privately with other users. See the revised snapshot of the Silverlight Web Chat.

Silverlight Chatroom Part 2

Requirements

We will add a way for users to choose from a list of chat rooms to enter. A private chat between another user will also be established. The old requirements are still in effect which are listed below:

  • 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 super fast coding.

Database

We will use our existing database with an additional field on the PrivateMessage table as shown below. See Part 1 for the table descriptions. The additional PrivateMessage table description is shown below.

Database Structure

  • PrivateMessage: Contains private invitation information. When an invitation to chat privately is sent to another user, an entry is added here.

Newly Added XAML Files

Easily enough, because we started using XAML files based on their functionality in Part 1, we simply need to add two (2) new files: Rooms.xaml and PrivateChat.xaml.

  • Rooms.xaml: Shows a list of chat rooms listed in the Rooms table in our database. So to add new rooms to this web chat application, simply go to the Rooms table and add as many rooms as you like, no additional coding is needed.
  • PrivateChat.xaml: This is the private chat window that pops-up when a user invited to chat by another user accepts the invitation.

Login Changes

There is a very minor change in our login mechanism (Login.xaml.cs).

  1. When the user is authenticated, we save this user to the LoggedInUser table as shown in lines 81-82. Notice that the only information we're passing is the user ID, because this user has not chosen a room just yet. Line 82 (Login.xaml.cs) calls a new method in the ILinqChatService interface:
  2. C#
    39     [OperationContract]
    40     void LogInUser(int userID);
    

    As usual, this method is implemented in the LinqChatService service.

    C#
    157    void ILinqChatService.LogInUser(int userID)
    158    {
    159        // login the user
    160        LinqChatDataContext db = new LinqChatDataContext();
    161
    162         LoggedInUser loggedInUser = new LoggedInUser();
    163         loggedInUser.UserID = userID;
    164         db.LoggedInUsers.InsertOnSubmit(loggedInUser);
    165         db.SubmitChanges();
    166     }
    
  3. Rather than going straight to the only room that was available in Part 1, the user is redirected to the list of rooms page (Rooms.xaml) as shown in line 88.
  4. C#
     72    void proxy_UserExistCompleted(object sender,
                Silverlight2Chat.LinqChatReference.UserExistCompletedEventArgs e)
    73     {
    74         if (e.Error == null)
    75         {
    76             int userID = e.Result;
    77
    78             if (userID != -1)
    79             {
    80                 // save user to the login table
    81                 LinqChatReference.LinqChatServiceClient proxy =
                             new LinqChatReference.LinqChatServiceClient();
    82                 proxy.LogInUserAsync(userID);
    83
    84                 // go to the chatroom page
    85                 App app = (App)Application.Current;
    86                 app.UserID = userID;
    87                 app.UserName = TxtUserName.Text;
    88                 app.RedirectTo(new Rooms());
    89             }
    90             else
    91             {
    92                 TxtbNotfound.Visibility = Visibility.Visible;
    93             }
    94         }
    95     }
    

Choose a Room

The interface to Rooms.xaml is very straightforward. It contains two (2) controls: a TextBlock used to hold the title, and a StackPanel that holds a list of available rooms stacked vertically.

Choose a Room

XML
 1 <UserControl x:Class="Silverlight2Chat.Rooms"
 2     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4     Width="600" Height="340">
 5     <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
 6         <Grid.RowDefinitions>
 7             <RowDefinition Height="10" />       <!-- padding -->
 8             <RowDefinition Height="38" />       <!-- title -->
 9             <RowDefinition Height="10" />       <!-- padding -->
10             <RowDefinition Height="*" />        <!-- rooms -->
11             <RowDefinition Height="10" />       <!-- padding -->
12         </Grid.RowDefinitions>
13
14         <Grid.ColumnDefinitions>
15             <ColumnDefinition Width="10" />     <!-- padding -->
16             <ColumnDefinition Width="*" />      <!-- rooms -->
17             <ColumnDefinition Width="10" />     <!-- padding -->
18         </Grid.ColumnDefinitions>
19
20         <TextBlock Text="Choose a Room" Grid.Row="1"
              Grid.Column="1" FontSize="22" Foreground="Navy" />
21
22         <StackPanel x:Name="SpnlRoomList"
              Orientation="Vertical" Grid.Row="3"
              Grid.Column="1" />
23     </Grid>
24 </UserControl>

In the code file (Rooms.xaml.cs), we first check if the user is logged-in in line 26 by checking the user name. We could just as easily check the user ID instead of the user name. Then we get the chat rooms in line 29. As mentioned in Part 1, because we are trying to get values from the client service proxy (WCF Service), we call the "Completed" event of the client proxy before calling the "Async" event, lines 35-36. The proxy_GetRoomsCompleted event retrieves all the available rooms from the Room table and assigns them to a HyperlinkButton, shown in lines 39-55. Notice that line 50 calls the Click event of the HyperlinkButton. When a user clicks one of the rooms listed in the chat room list, the HyperlinkButton Click event simply "remembers" the room ID and room name (lines 62-64), then redirects (line 67) the user to the chat room page (Chatroom.xaml). For more information on the basics of a WCF service, please read Part 1.

C#
20     public Rooms()
21     {
22         InitializeComponent();
23
24         App app = (App)Application.Current;
25
26         if (String.IsNullOrEmpty(app.UserName))
27             app.RedirectTo(new Login());
28
29         GetChatRooms();
30     }
31
32     private void GetChatRooms()
33     {
34         LinqChatReference.LinqChatServiceClient proxy =
             new LinqChatReference.LinqChatServiceClient();
35         proxy.GetRoomsCompleted += new
             EventHandler<Silverlight2Chat.LinqChatReference.
             GetRoomsCompletedEventArgs>(proxy_GetRoomsCompleted);
36         proxy.GetRoomsAsync();
37     }
38
39     void proxy_GetRoomsCompleted(object sender,
           Silverlight2Chat.LinqChatReference.GetRoomsCompletedEventArgs e)
40     {
41         if (e.Error == null)
42         {
43             ObservableCollection<LinqChatReference.RoomContract> rooms = e.Result;
44
45             foreach (var room in rooms)
46             {
47                 HyperlinkButton linkButton = new HyperlinkButton();
48                 linkButton.Name = room.RoomID.ToString();
49                 linkButton.Content = room.Name;
50                 linkButton.Click += new RoutedEventHandler(linkButton_Click);
51
52                 SpnlRoomList.Children.Add(linkButton);
53             }
54         }
55     }
56
57     void linkButton_Click(object sender, RoutedEventArgs e)
58     {
59         HyperlinkButton linkButton = sender as HyperlinkButton;
60
61         // assign the room
62         App app = (App)Application.Current;
63         app.RoomID = Convert.ToInt32(linkButton.Name);
64         app.RoomName = linkButton.Content.ToString();
65
66         // redirect
67         app.RedirectTo(new Chatroom());
68     }

Chatroom Page

As you probably have already noticed, there is also a very minor revision in the GUI (graphical user interface) of the Chatroom.xaml page. The GUI revision mostly has nothing to do with functionality. The logged-in user name was moved on top of the title in a StackPanel, and is now colored gray, line 25. A "Choose Other Room" button is added (line 30) on top of the Log Out button, also in a StackPanel. Notice that we are using a simple StackPanel (lines 43-44) for the list of users instead of an ItemsControl data template control as compared to what we used in Part 1, I will explain why later on in this article.

XML
 1     <UserControl x:Class="Silverlight2Chat.Chatroom"
 2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4         Width="600" Height="346">
 5         <Grid x:Name="LayoutRoot" Background="White"
                 ShowGridLines="False" Loaded="LayoutRoot_Loaded">
 6             <Grid.RowDefinitions>
 7                 <RowDefinition Height="10" />       <!-- padding -->
 8                 <RowDefinition Height="46" />       <!-- title -->
 9                 <RowDefinition Height="10" />       <!-- padding -->
10                 <RowDefinition Height="*" />        <!-- messages, userlist -->
11                 <RowDefinition Height="10" />       <!-- padding -->
12                 <RowDefinition Height="26" />       <!-- message text box, send button -->
13                 <RowDefinition Height="10" />       <!-- padding -->
14             </Grid.RowDefinitions>
15
16             <Grid.ColumnDefinitions>
17                 <ColumnDefinition Width="10" />     <!-- padding -->
18                 <ColumnDefinition Width="*" />      <!-- messages, message text box-->
19                 <ColumnDefinition Width="10" />     <!-- padding -->
20                 <ColumnDefinition Width="120" />    <!-- user list, send button-->
21                 <ColumnDefinition Width="10" />     <!-- padding -->
22             </Grid.ColumnDefinitions>
23
24             <StackPanel Orientation="Vertical" Grid.Row="1" Grid.Column="1">
25                 <TextBlock x:Name="TxtbLoggedInUser" FontSize="10"
                       Foreground="Gray" FontWeight="Bold"
                       Margin="0,0,0,4" />
26                 <TextBlock x:Name="TxtbRoomName"
                       FontSize="24" Foreground="Navy" />
27             </StackPanel>
28
29             <StackPanel Orientation="Vertical" Grid.Row="1" Grid.Column="3">
30                 <Button x:Name="BtnChooseRoom"
                       Content="Choose Other Room" FontSize="10"
                       Click="BtnChooseRoom_Click" Margin="0,0,0,4" />
31                 <Button x:Name="BtnLogOut" Content="Log Out"
                       FontSize="10" Click="BtnLogOut_Click" />
32             </StackPanel>
33
34             <ScrollViewer x:Name="SvwrMessages" Grid.Row="3" Grid.Column="1"
35                           HorizontalScrollBarVisibility="Hidden"
36                           VerticalScrollBarVisibility="Visible"
                             BorderThickness="2">
37                 <StackPanel x:Name="SpnlMessages" Orientation="Vertical" />
38             </ScrollViewer>
39
40             <ScrollViewer x:Name="SvwrUserList"
                       Grid.Row="3" Grid.Column="3"
41                     HorizontalScrollBarVisibility="Auto"
42                     VerticalScrollBarVisibility="Auto" BorderThickness="2">
43                 <StackPanel x:Name="SpnlUserList" Orientation="Vertical">
44                 </StackPanel>
45             </ScrollViewer>
46
47             <StackPanel Orientation="Horizontal"
                        Grid.Row="5" Grid.Column="1" >
48                 <TextBox x:Name="TxtMessage"
                        TextWrapping="Wrap" KeyDown="TxtMessage_KeyDown"
49                      ScrollViewer.VerticalScrollBarVisibility="Visible"
50                      ScrollViewer.HorizontalScrollBarVisibility="Disabled"
51                      Width="360"
52                      BorderThickness="2" Margin="0,0,10,0"/>
53
54                 <ComboBox x:Name="CbxFontColor" Width="80">
55                     <ComboBoxItem Content="Black" Foreground="White"
                             Background="Black" IsSelected="True" />
56                     <ComboBoxItem Content="Red" Foreground="White" Background="Red" />
57                     <ComboBoxItem Content="Blue" Foreground="White" Background="Blue" />
58                 </ComboBox>
59             </StackPanel>
60
61             <Button x:Name="BtnSend" Content="Send"
                   Grid.Row="5" Grid.Column="3" Click="BtnSend_Click" />
62         </Grid>
63     </UserControl>

Most of the changes here were done to support private messages to other users as well as leaving this room to choose another room. With that said, we will dive right into the intricacies of chatting with other users privately.

  1. Getting the users: As I mentioned above, we changed the ItemsControl to a simple StackPanel that holds the list of users. One of the main reason is that, we want to be able to show all users as HyperlinkButton(s) besides the current logged-in user, which is you (lines 77-107). Simple enough, the idea is: you don't want to click your own name and accidentally chat with your own self.
  2. C#
     64     void proxy_GetUsersCompleted(object sender,
                 Silverlight2Chat.LinqChatReference.GetUsersCompletedEventArgs e)
     65     {
     66         if (e.Error == null)
     67         {
     68             ObservableCollection<LinqChatReference.UserContract> users = e.Result;
     69             SpnlUserList.Children.Clear();
     70
     71             foreach (var user in users)
     72             {
     73                 // show the current user as a non-clickable text only
     74                 // all other users should be hyperlinks
     75                 App app = (App)Application.Current;
     76
     77                 if (user.UserID == app.UserID)
     78                 {
     79                     TextBlock tb = new TextBlock();
     80                     tb.Text = user.UserName;
     81                     tb.Foreground = new SolidColorBrush(Colors.Black);
     82                     tb.FontWeight = FontWeights.Bold;
     83
     84                     SpnlUserList.Children.Add(tb);
     85                 }
     86                 else
     87                 {
     88                     HyperlinkButton hb = new HyperlinkButton();
     89                     hb.Content = user.UserName;
     90
     91                     // build the absolute url
     92                     Uri url = System.Windows.Browser.HtmlPage.Document.DocumentUri;
     93                     string link = url.OriginalString;
     94                     int lastSlash = link.LastIndexOf('/') + 1;
     95                     link = link.Remove(lastSlash, link.Length - lastSlash) +
     96                         "Chatroom.aspx?fromuserid=" + app.UserID.ToString() +
     97                         "&fromusername=" + app.UserName +
     98                         "&touserid=" + user.UserID +
     99                         "&tousername=" + user.UserName +
    100                         "&isinvited=false";
    101
    102                     // build the hyperlink
    103                     hb.TargetName = "_blank";
    104                     hb.NavigateUri = new Uri(link);
    105
    106                     SpnlUserList.Children.Add(hb);
    107                 }
    108             }
    109         }
    110     }
    

    To chat with someone privately, I want to be able to open a new browser when I click a user on the user list. Currently, Silverlight's HyperlinkButton control can only be assigned an absolute URL. So to open up a new browser, we manipulate the current URL in the browser (lines 92-100 above) to build a link for each of the users. Notice in line 96 above that when this user is clicked, it will open the "Chatroom.aspx" ASP.NET page. This is because, like we talked about in Part 1, all the XAML user controls are going to be hosted by only one (1) ASP.NET page, and that page is the Chatroom.aspx page. Because a new browser means a new instance of the chat application, the very first XAML user control that we encounter is the App.xaml. So now, how do we load the PrivateChat.xaml user control? We certainly don't want to enter a username and password each time we click another user to chat privately with them.

    That is why we have a few querystring key/value pairs that are assigned to each user link seen in lines 96-100 above, like "fromusername", "touserid", etc. We will use the respective values to sign-in the user automatically in the App.xaml user control. The "isinvited" key in line 100 simply states that you are the one inviting the other user to chat privately, and not the one being invited, isinvited=false;. I'll talk more about how we use this value later on.

    Let's jump to the App.xaml user control to see how this process works. You will notice that we added a few public properties to hold and store our application-wide values. By looking at the highlighted code below, you can guess right away that the querystring key/value pairs shown in the HyperlinkButton (above), in the users list inside the Chatroom.xaml user control, are directly related.

    C#
    17     public int UserID { get; set; }
    18     public string UserName { get; set; }
    19     public int ToUserID { get; set; }
    20     public string ToUserName { get; set; }
    21     public DateTime TimeUserJoined { get; set; }
    22     public int RoomID { get; set; }
    23     public string RoomName { get; set; }
    24     public bool IsInvited { get; set; }
    25     public DateTime TimeUserSentInviation { get; set; }
    

    Lines 64-69 below retrieve the key/value pairs from the querystring and then assign them to the public properties of the App.xaml user control. To load the PrivateChat user control instead of the Login user control, we simply check at least one value from the URL indicating that this user has opened a private chat window. In this case, we're just checking the "fromusername" key shown in line 61. Lines 43-53 determine what XAML user control to load.

    C#
    38     private void Application_Startup(object sender, StartupEventArgs e)
    39     {
    40         this.RootVisual = rootGrid;
    41
    42         // check if it's a private chat request
    43         if (IsPrivateChatRequest())
    44         {
    45             // open private chat instead of login page
    46             rootGrid.Children.Add(new PrivateChat());
    47         }
    48         else
    49         {
    50             // start at the login page
    51             // this.RootVisual = rootGrid;
    52             rootGrid.Children.Add(new Login());
    53         }
    54     }
    55
    56     private bool IsPrivateChatRequest()
    57     {
    58         Uri uri = System.Windows.Browser.HtmlPage.Document.DocumentUri;
    59         IDictionary<string, string> queryString =
                  System.Windows.Browser.HtmlPage.Document.QueryString;
    60
    61         if (uri.ToString().Contains("fromusername"))
    62         {
    63             // set all the app wide variables
    64             App app = (App)Application.Current;
    65             app.UserID = Convert.ToInt32(queryString["fromuserid"]);
    66             app.UserName = queryString["fromusername"];
    67             app.ToUserID = Convert.ToInt32(queryString["touserid"]);
    68             app.ToUserName = queryString["tousername"];
    69             app.IsInvited = Convert.ToBoolean(queryString["isinvited"]);
    70
    71             try
    72             {
    73                 app.TimeUserSentInviation =
                        Convert.ToDateTime(queryString["timeusersentinvitation"]);
    74             }
    75             catch { }
    76
    77             return true;
    78         }
    79         else
    80         {
    81             return false;
    82         }
    83     }
    
  3. Responding to a private chat invitation. Now back to the Chatroom.xaml user control. We just talked about the process of opening a private chat window so that you can invite another user to chat with you privately. At this point, you have not sent an invitation yet, you just opened the private chat window. You send an invitation to chat as soon as you send your first message to the other user. Let me talk about this process later when we get to the PrivateChat user control part of the article. For now, I will talk about how the other user (the one you're inviting to chat privately) can respond to your invitation.
  4. We check the private message invitations from other users to you on the TimerControl's Tick event. In this event, we call the GetPrivateMessages method to check for any invitations. This of course calls our WCF service via a client proxy as discussed above.

    C#
    261     void TimerTick(object sender, EventArgs e)
    262     {
    263         GetMessages();
    264         GetUsers();
    265         GetPrivateMessages();
    266     }
    267
    268     private void GetPrivateMessages()
    269     {
    270         // get the private message invitations sent to me by other chatters
    271         LinqChatReference.LinqChatServiceClient proxy =
                     new LinqChatReference.LinqChatServiceClient();
    272         proxy.GetPrivateMessageInvitesCompleted += new
                  EventHandler<Silverlight2Chat.LinqChatReference.
                  GetPrivateMessageInvitesCompletedEventArgs>(
                  proxy_GetPrivateMessageInvitesCompleted);
    273         proxy.GetPrivateMessageInvitesAsync(_userID);
    274     }
    

    The implementation of getting the private message invitations is shown below, and can be found along with the other implementations of the ILinqChatService interface, in the LinqChatService.svc service.

    C#
    275     List<PrivateMessageContract> ILinqChatService.GetPrivateMessageInvites(int toUserID)
    276     {
    277         List<PrivateMessageContract> pmContracts = new List<PrivateMessageContract>();
    278         LinqChatDataContext db = new LinqChatDataContext();
    279
    280         var pvtMessages = from pm in db.PrivateMessages
    281                           where pm.ToUserID == toUserID
    282                           select new { pm.PrivateMessageID, pm.UserID,
                    pm.User.Username, pm.ToUserID, pm.TimeUserSentInvitation };
    283
    284         if (pvtMessages.Count() > 0)
    285         {
    286             foreach(var privateMessage in pvtMessages)
    287             {
    288                 PrivateMessageContract pmc = new PrivateMessageContract();
    289                 pmc.PrivateMessageID = privateMessage.PrivateMessageID;
    290                 pmc.UserID = privateMessage.UserID;
    291                 pmc.UserName = privateMessage.Username;
    292                 pmc.ToUserID = privateMessage.ToUserID;
    293                 pmc.TimeUserSentInvitation = privateMessage.TimeUserSentInvitation;
    294
    295                 pmContracts.Add(pmc);
    296             }
    297         }
    298
    299         return pmContracts;
    300     }
    

    Each invitation will cause to show a Silverlight Popup control as shown in the image below.

    Invitation to Chat Privately

    The code that builds this Popup control for each invitation and causes the pop-up to show can be found in the GetPrivateMessage Completed event shown below. I know what you're thinking, "it takes this much code to show a very simple pop-up control?", the answer is Yes. What's even more amazing is each of the buttons, the Close and Chat Now buttons have their own Click event code, so that makes the code even longer, line 346 and 357. The code shown below simply adds a TextBlock control, a dummy Chat Now Button control, a Close Button control, and a Chat Now HyperlinkButton control to the Grid control which is a child of the Popup control. The Popup control is set to show or set to be visible as shown in line 285.

    The dummy Chat Now button control is used as a "background" control to the real Chat Now HyperlinkButton control so that we can make the HyperlinkButton control look like a button. This is just my preference, you could have as easily assigned the HyperlinkButton control's Content property to show a button, or even design the HyperlinkButton to show like a button. You can see that both the Button and HyperlinkButton are located in the same spot, lines 316 and 328.

    C#
    276     void proxy_GetPrivateMessageInvitesCompleted(object sender,
                 Silverlight2Chat.LinqChatReference.GetPrivateMessageInvitesCompletedEventArgs e)
    277     {
    278         ObservableCollection<LinqChatReference.PrivateMessageContract> invitations = e.Result;
    279
    280         foreach (var invitation in invitations)
    281         {
    282             Popup popUp = new Popup();
    283             Grid grid = new Grid();
    284             popUp.Child = grid;
    285             popUp.IsOpen = true;
    286             popUp.Name = "PopUpInvitation" + invitation.PrivateMessageID.ToString();
    287
    288             // add popup to the root grid
    289             LayoutRoot.Children.Add(popUp);
    290
    291             grid.Width = 200;
    292             grid.Height = 100;
    293             grid.HorizontalAlignment = HorizontalAlignment.Center;
    294
    295             // pop-up border
    296             Border border = new Border();
    297             border.BorderBrush = new SolidColorBrush(Colors.Black);
    298             border.BorderThickness = new Thickness(2);
    299             border.CornerRadius = new CornerRadius(8);
    300             border.Background = new SolidColorBrush(Colors.White);
    301
    302             // pop-up text
    303             App app = (App)Application.Current;
    304
    305             TextBlock textBlock = new TextBlock();
    306             textBlock.Text = app.UserName  + " wants to chat privately.";
    307             textBlock.HorizontalAlignment = HorizontalAlignment.Center;
    308             textBlock.VerticalAlignment = VerticalAlignment.Top;
    309             textBlock.Margin = new Thickness(8);
    310
    311             // accept button - background only
    312             Button btnAccept = new Button();
    313             btnAccept.Width = 100;
    314             btnAccept.Height = 24;
    315             btnAccept.HorizontalAlignment = HorizontalAlignment.Left;
    316             btnAccept.VerticalAlignment = VerticalAlignment.Bottom;
    317             btnAccept.Margin = new Thickness(8);
    318
    319             // accept hyperlink - put on top of the accept button
    320             HyperlinkButton hpBtn = new HyperlinkButton();
    321             hpBtn.Name = "HbtnChatNow" + invitation.PrivateMessageID.ToString();
    322             hpBtn.Width = 100;
    323             hpBtn.Height = 22;
    324             hpBtn.Content = "     Chat Now    ";
    325             hpBtn.Foreground = new SolidColorBrush(Colors.Green);
    326             hpBtn.Background = new SolidColorBrush(Colors.Transparent);
    327             hpBtn.HorizontalAlignment = HorizontalAlignment.Left;
    328             hpBtn.VerticalAlignment = VerticalAlignment.Bottom;
    329             hpBtn.Margin = new Thickness(8);
    330
    331             // build the absolute url
    332             Uri url = System.Windows.Browser.HtmlPage.Document.DocumentUri;
    333             string link = url.OriginalString;
    334             int lastSlash = link.LastIndexOf('/') + 1;
    335             link = link.Remove(lastSlash, link.Length - lastSlash) +
    336                 "Chatroom.aspx?fromuserid=" + app.UserID.ToString() +
    337                 "&fromusername=" + app.UserName +
    338                 "&touserid=" + invitation.UserID +
    339                 "&tousername=" + invitation.UserName +
    340                 "&isinvited=true" +
    341                 "&timeusersentinvitation=" + invitation.TimeUserSentInvitation.ToString();
    342
    343             // build the hyperlink
    344             hpBtn.TargetName = "_blank";
    345             hpBtn.NavigateUri = new Uri(link);
    346             hpBtn.Click += new RoutedEventHandler(hpBtn_Click);
    347
    348             // close button
    349             Button btnClose = new Button();
    350             btnClose.Name = "BtnClose" + invitation.PrivateMessageID.ToString();
    351             btnClose.Width = 50;
    352             btnClose.Height = 24;
    353             btnClose.Content = "Close";
    354             btnClose.HorizontalAlignment = HorizontalAlignment.Right;
    355             btnClose.VerticalAlignment = VerticalAlignment.Bottom;
    356             btnClose.Margin = new Thickness(8);
    357             btnClose.Click += new RoutedEventHandler(btnClose_Click);
    358
    359             // add to grid
    360             grid.Children.Add(border);
    361             grid.Children.Add(textBlock);
    362             grid.Children.Add(btnAccept);
    363             grid.Children.Add(hpBtn);
    364             grid.Children.Add(btnClose);
    365
    366             // delete private message invation from database
    367             LinqChatReference.LinqChatServiceClient proxy = new LinqChatReference.LinqChatServiceClient();
    368             proxy.DeletePrivateMessageAsync(invitation.PrivateMessageID);
    369         }
    370     }
    

    The Close button and the Chat Now HyperlinkButton pretty much do the same thing when you click on them as shown in their respective Click events below. They simply get the name of the Popup control to close and then close them by doing popUp.IsOpen = false. But the Chat Now HyperlinkButton also opens up the PrivateChat.xaml user control in the browser, much the same way we open up a new browser when we invite someone to chat privately with us as shown earlier in this article. This is because we use pretty much the same code as shown in lines 332-341 above, the only differences are: the value on the "isinvited" which is "true" in line 340 (signifies that you're the one being invited to chat privately), and another key/value pair is added in line 341, "timeusersentinvitation", which signifies the time the other user sent you the invitation to chat privately. We will use the value from "timeusersentinvitation" to get all the private messages from the time the other user sent you an invitation. I will talk more about this later.

    C#
    372     void hpBtn_Click(object sender, RoutedEventArgs e)
    373     {
    374         // get the name of the pop-up to close
    375         // based from the name of the hyperlink button
    376         HyperlinkButton button = sender as HyperlinkButton;
    377         string privateMessageID = button.Name.Replace("HbtnChatNow", "");
    378
    379         Popup popUp = (Popup)LayoutRoot.FindName("PopUpInvitation" + privateMessageID);
    380         popUp.IsOpen = false;
    381     }
    382
    383     void btnClose_Click(object sender, RoutedEventArgs e)
    384     {
    385         // get the name of the pop-up to close
    386         // based from the name of the button
    387         Button button = sender as Button;
    388         string privateMessageID = button.Name.Replace("BtnClose", "");
    389
    390         Popup popUp = (Popup) LayoutRoot.FindName("PopUpInvitation" + privateMessageID);
    391         popUp.IsOpen = false;
    392     }
    

PrivateChat User Control

Private Chat Window

As you can see, this control looks just like the Chatroom user control, except that some controls are omited such as the title, log-out button, etc. And because of the similarities, the way we send messages or receive messages works the same way, except we're only sending them to one user. Shown below is the PrivateChat user control GUI code.

XML
 1     <UserControl x:Class="Silverlight2Chat.PrivateChat"
 2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4         Width="440" Height="280">
 5         <Grid x:Name="LayoutRoot" Background="White"
                    ShowGridLines="False" Loaded="LayoutRoot_Loaded">
 6             <Grid.RowDefinitions>
 7                 <RowDefinition Height="10" />       <!-- padding -->
 8                 <RowDefinition Height="*" />        <!-- messages -->
 9                 <RowDefinition Height="10" />       <!-- padding -->
10                 <RowDefinition Height="26" />       <!-- message text box, send button -->
11                 <RowDefinition Height="10" />       <!-- padding -->
12             </Grid.RowDefinitions>
13
14             <Grid.ColumnDefinitions>
15                 <ColumnDefinition Width="10" />     <!-- padding -->
16                 <ColumnDefinition Width="*" />      <!-- messages, message text box-->
17                 <ColumnDefinition Width="10" />     <!-- padding -->
18             </Grid.ColumnDefinitions>
19
20             <ScrollViewer x:Name="SvwrMessages" Grid.Row="1" Grid.Column="1"
21                           HorizontalScrollBarVisibility="Hidden"
22                           VerticalScrollBarVisibility="Visible" BorderThickness="2">
23                 <StackPanel x:Name="SpnlMessages" Orientation="Vertical" />
24             </ScrollViewer>
25
26             <StackPanel Orientation="Horizontal" Grid.Row="3" Grid.Column="1" >
27                 <TextBox x:Name="TxtMessage"
                        TextWrapping="Wrap" KeyDown="TxtMessage_KeyDown"
28                      ScrollViewer.VerticalScrollBarVisibility="Visible"
29                      ScrollViewer.HorizontalScrollBarVisibility="Disabled"
30                      Width="280"
31                      BorderThickness="2" Margin="0,0,10,0"/>
32
33                 <ComboBox x:Name="CbxFontColor" Width="60" Margin="0,0,10,0">
34                     <ComboBoxItem Content="Black" Foreground="White"
                          Background="Black" IsSelected="True" />
35                     <ComboBoxItem Content="Red" Foreground="White" Background="Red" />
36                     <ComboBoxItem Content="Blue" Foreground="White" Background="Blue" />
37                 </ComboBox>
38
39                 <Button x:Name="BtnSend" Content="Send" Click="BtnSend_Click" Width="60" />
40             </StackPanel>
41
42         </Grid>
43     </UserControl>
  1. Invite another user to chat privately. When you click one of the users listed in the Chatroom.xaml page, this private chat window appears. To invite someone to chat privately, you first need to send at least one message to the other user. Lines 201-205.
  2. C#
    185     private void BtnSend_Click(object sender, RoutedEventArgs e)
    186     {
    187         SendMessage();
    188     }
    189
    190     private void SendMessage()
    191     {
    192         if (!String.IsNullOrEmpty(TxtMessage.Text))
    193         {
    194             InsertMessage();
    195             GetPrivateMessages();
    196
    197             // send an invitation to chat only if you're the one that
    198             // is sending the invitation, this means that you clicked
    199             // one of the users in the user list in the main chat
    200             // and did not click the invitation to chat pop up
    201             if (_isInvitationToChatSent == false && _isInvited == false)
    202             {
    203                 InviteOtherUserToChat();
    204                 _isInvited = true;
    205             }
    206         }
    207     }
    

    The method called in line 203 above is shown below. Because we are not expecting anything to be returned from the WCF service shown below, we need not call the Completed event of the InsertPrivateMessageInvite object. In short, we only need to call the "Async" method of this client proxy.

    C#
    214     private void InviteOtherUserToChat()
    215     {
    216         LinqChatReference.LinqChatServiceClient proxy =
                     new LinqChatReference.LinqChatServiceClient();
    217         proxy.InsertPrivateMessageInviteAsync(_fromUserID, _toUserID);
    218     }
    

    The Async WCF call in line 217 above calls the WCF service method found in LinqChatService.cs. Notice that the async call above and the method below have the same signature. The service below inserts an entry in the PrivateMessage table.

    C#
    243     void ILinqChatService.InsertPrivateMessageInvite(int userID, int toUserID)
    244     {
    245         // first check if an invitation to chat has already
    246         // been sent to the particular user "toUserID"
    247         LinqChatDataContext db = new LinqChatDataContext();
    248
    249         var count = (from pvtm in db.PrivateMessages
    250                      where pvtm.UserID == userID &&
    251                      pvtm.ToUserID == toUserID
    252                      select new { pvtm.PrivateMessageID }).Count();
    253
    254         if (count == 0)
    255         {
    256             // no invitation was found
    257             PrivateMessage pm = new PrivateMessage();
    258             pm.UserID = userID;
    259             pm.ToUserID = toUserID;
    260             pm.TimeUserSentInvitation = DateTime.Now;
    261
    262             db.PrivateMessages.InsertOnSubmit(pm);
    263
    264             try
    265             {
    266                 db.SubmitChanges();
    267             }
    268             catch (Exception)
    269             {
    270                 throw;
    271             }
    272         }
    273     }
    
  3. Sending private messages. In the chat room, we're getting all the messages displayed from all the users that are chatting. Here, we're only supposed to get messages between the two users privately chatting with each other. This is simple enough to solve. In the Chatroom.xaml user control, when we insert a message in the Message table, "_toUserID" is set to null. Here we simply set that value to the user that we're trying to chat with. The _toUserID value is set in this user control's constructor, which is retrieved from App.xaml's public properties.
  4. C#
    19     DispatcherTimer timer;
    20     private bool _isTimerStarted;
    21     private bool _isWithBackground = false;
    22     private int _lastMessageId = 0;
    23     private int _fromUserID;
    24     private int _toUserID;
    25     private bool _isInvited;
    26     private bool _isInvitationToChatSent = false;
    27     private DateTime _timeUserSentInvitation;
    28
    29     public PrivateChat()
    30     {
    31         InitializeComponent();
    32
    33         App app = (App)Application.Current;
    34
    35         if (String.IsNullOrEmpty(app.UserName))
    36         {
    37             app.RedirectTo(new Login());
    38         }
    39         else
    40         {
    41             _fromUserID = app.UserID;
    42             _toUserID = app.ToUserID;
    43             _isInvited = app.IsInvited;
    44
    45             if (_isInvited)
    46                 _timeUserSentInvitation = app.TimeUserSentInviation;
    47             else
    48                 _timeUserSentInvitation = DateTime.Now;
    49         }
    50     }
    

    This makes it very simple to send the message to the other user.

    C#
    68     private void InsertMessage()
    69     {
    70         LinqChatReference.LinqChatServiceClient proxy =
                  new LinqChatReference.LinqChatServiceClient();
    71         proxy.InsertMessageAsync(null, _fromUserID, _toUserID,
                 TxtMessage.Text, CbxFontColor.SelectionBoxItem.ToString());
    72     }
    
  5. Getting private messages. Just like when we're sending messages to another user privately, we need to populate the _fromUserID and _toUserID variables to retrieve just the messages meant for exclusive chat users. Another piece of information we need to populate from the very first time the "inviting" user sent an invitation to chat privately is assigned to the _timeUserSentInvitation variable. This way we will only retrieve the messages from that time on and above.
  6. C#
    74     private void GetPrivateMessages()
    75     {
    76         LinqChatReference.LinqChatServiceClient proxy =
                   new LinqChatReference.LinqChatServiceClient();
    77         proxy.GetPrivateMessagesCompleted += new
                 EventHandler<Silverlight2Chat.LinqChatReference.
                 GetPrivateMessagesCompletedEventArgs>(proxy_GetPrivateMessagesCompleted);
    78         proxy.GetPrivateMessagesAsync(_timeUserSentInvitation,
                 _lastMessageId, _fromUserID, _toUserID);
    79     }
    

    The GetPrivateMessagesCompleted event here does the same thing as the GetMessagesCompleted event in the Chatroom.xaml user control so I'm not going to spend any time explaining it. For more information on the Completed event, see Part 1. The Async proxy code above calls the WCF service GetPrivateMessages located in LinqChatService.svc.cs. One thing I would like to stress out is the way we built the query, especially the code shown in lines 68-69. We are retrieving all messages sent by either user chatting privately which is "sent to" either user chatting privately.

    C#
    62     List<MessageContract> ILinqChatService.GetPrivateMessages(DateTime
                   timeUserSentInvitation, int messageID, int fromUserId, int toUserId)
    63     {
    64         LinqChatDataContext db = new LinqChatDataContext();
    65
    66         var messages = (from m in db.Messages
    67                         where
    68                         ((m.UserID == fromUserId && m.ToUserID == toUserId) ||
    69                         (m.ToUserID == fromUserId && m.UserID == toUserId)) &&
    70                         m.TimeStamp >= timeUserSentInvitation &&
    71                         m.MessageID > messageID
    72                         orderby m.TimeStamp ascending
    73                         select new { m.MessageID, m.Text,
                                            m.User.Username, m.TimeStamp, m.Color });
    74
    75         List<MessageContract> messageContracts = new List<MessageContract>();
    76
    77         foreach (var message in messages)
    78         {
    79             MessageContract messageContract = new MessageContract();
    80             messageContract.MessageID = message.MessageID;
    81             messageContract.Text = message.Text;
    82             messageContract.UserName = message.Username;
    83             messageContract.Color = message.Color;
    84             messageContracts.Add(messageContract);
    85         }
    86
    87         return messageContracts;
    88     }
    
  7. Resizing the private chat window. Silverlight 2 unfortunately does not have a functionality to open a new window in a specified size at the time of this writing. Remember that we're hosting all the XAML user controls using just one ASP.NET page. So to dynamically resize this ASP.NET page only when the PrivateChat.xaml user control is loaded, we need some kind of indication, somehow passing the information from a XAML user control to an ASP.NET page. Luckily enough, we can write some hack in JavaScript to do just this. Using JavaScript, we can examine at least one querystring key/value pair from the Chatroom.aspx page. In this case, we are going to check for the "fromusername" key. If the key is there, we resize the window (Chatroom.aspx) to 640 x 460.
  8. JavaScript
    9 <script type="Text/javascript">
    10 window.onload = function() 
    11 {
    12 ResizeWindow();
    13 document.getElementById('Xaml1').focus();
    14 }
    15 
    16 function ResizeWindow() 
    17 {
    18 var fromusername = GetQueryString("fromusername");
    19 
    20 if (fromusername != null) 
    21 {
    22 // resize the screen
    23 window.resizeTo(640, 460);
    24 } 
    25 }
    26 
    27 function GetQueryString(variable) 
    28 {
    29 var query = window.location.search.substring(1);
    30 var vars = query.split("&");
    31 
    32 for (var i = 0; i < vars.length; i++) 
    33 {
    34 var pair = vars[i].split("=");
    35 
    36 if (pair[0] == variable)
    37 return pair[1];
    38 }
    39 }
    40 </script>

Choosing Other Rooms

You need to leave your current room to enter another room. You can click the button with the text "Choose Other Room" located in the Chatroom.xaml user control. The Click event is shown below.

C#
406     private void BtnChooseRoom_Click(object sender, RoutedEventArgs e)
407     {
408         timer.Stop();
409         App app = (App)Application.Current;
410
411         // leave the room to choose another room
412         LinqChatReference.LinqChatServiceClient proxy =
               new LinqChatReference.LinqChatServiceClient();
413         proxy.LeaveRoomAsync(_userID, _roomId, app.UserName);
414
415         // redirect to the rooms page
416         app.RedirectTo(new Rooms());
417     }

The proxy client method above calls the WCF service method shown below. The only difference with this method and the Log Out method is that, here we are only setting RoomID = null in the LoggedInUser table, whereas in the Log Out method, we are totally deleting this user from the LoggedInUser table.

C#
194     void ILinqChatService.LeaveRoom(int userID, int roomID, string username)
195     {
196         // leave the room by setting room id = null
197         LinqChatDataContext db = new LinqChatDataContext();
198
199         var loggedInUser = (from l in db.LoggedInUsers
200                             where l.UserID == userID
201                             && l.RoomID == roomID
202                             select l).SingleOrDefault();
203
204         loggedInUser.RoomID = null;
205         db.SubmitChanges();
206
207         // insert user "left the room" text
208         Message message = new Message();
209         message.RoomID = roomID;
210         message.UserID = userID;
211         message.ToUserID = null;
212         message.Text = username + " left the room.";
213         message.Color = "Gray";
214         message.TimeStamp = DateTime.Now;
215
216         db.Messages.InsertOnSubmit(message);
217         db.SubmitChanges();
218     }

Because this is a web chat application, you could of course open several browsers, log-in in each browser, and then enter different rooms at the same time.

Last Words

I hope that you learned something out of this article. I did not discuss the basics of WCF (Windows Communication Foundation), nor did I discuss the formatting of the messages in the chat room because these and others can be found in Part 1. The article is meant for learning the processes of establishing a monitored private chat using Silverlight 2 and MS SQL Server. There are a lot of things that you can improve here. For example, to improve performance, you can change all calls to the database using Stored Procedures instead of dynamic SQL. Rather than being too chatty, you can also combine calls to the database to make one trip instead of several trips, like the methods found in the TimerTick event of the Chatroom.xaml user control.

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

Note: The original article can be found here: http://www.junnark.com/Articles/Build-a-Silverlight-Web-Chatroom-with-Multiple-Rooms-and-Private-Chat-Part-2.aspx.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
None.

Comments and Discussions

 
QuestionCannot insert the value NULL into column 'RoomID', table 'LinqChat.dbo.LoggedInUser'; column does not allow nulls. INSERT fails. Pin
Member 114622075-Jun-15 0:02
Member 114622075-Jun-15 0:02 
QuestionI am new Pin
David.Moore8724-Feb-14 3:18
David.Moore8724-Feb-14 3:18 
QuestionNothing is happening after sigin Pin
ashish_asd12330-Jan-14 19:06
ashish_asd12330-Jan-14 19:06 
Questionthe error Pin
Seraph_summer1-Jun-12 22:57
Seraph_summer1-Jun-12 22:57 
Questionhow can I add user and password into table Pin
Seraph_summer1-Jun-12 10:34
Seraph_summer1-Jun-12 10:34 
Questionabout Silverlight 2 Web Chat Application Pin
dani50226-Mar-12 8:38
dani50226-Mar-12 8:38 
AnswerRe: about Silverlight 2 Web Chat Application Pin
Seraph_summer1-Jun-12 10:49
Seraph_summer1-Jun-12 10:49 
QuestionAdd video and voice to chat Pin
coder13pp5-Jul-11 23:33
coder13pp5-Jul-11 23:33 
GeneralMy vote of 1 Pin
Vincees29-Jul-10 22:19
Vincees29-Jul-10 22:19 
GeneralHelp a newbe Pin
Blackapril27-Jun-10 21:35
Blackapril27-Jun-10 21:35 
QuestionHow to get Session ["userName"] from asp.net page into Silverlight web chat Pin
beibei20086-May-10 0:03
beibei20086-May-10 0:03 
GeneralThanks for the great application. Pin
dotnetrich7-Nov-09 10:15
dotnetrich7-Nov-09 10:15 
Hi.

Thanks for this great application. If I try to login I got this problem. (this line int _result = ((int)(base.EndInvoke("UserExist", _args, result))); )

An error occurred while trying to make a request to URI 'http://localhost:41885/LinqChatService.svc'. This could be due to attempting to access a service in a cross-domain way without a proper cross-domain policy in place, or a policy that is unsuitable for SOAP services. You may need to contact the owner of the service to publish a cross-domain policy file and to ensure it allows SOAP-related HTTP headers to be sent. This error may also be caused by using internal types in the web service proxy without using the InternalsVisibleToAttribute attribute. Please see the inner exception for more details.

Do you know the solution?
GeneralRe: Thanks for the great application. Pin
dotnetrich7-Nov-09 11:24
dotnetrich7-Nov-09 11:24 
GeneralOnline Chess Pin
Adam Berent17-Apr-09 4:01
Adam Berent17-Apr-09 4:01 
GeneralRe: Online Chess Pin
Adam Berent17-Jul-09 10:37
Adam Berent17-Jul-09 10:37 
GeneralRe: Online Chess Pin
mehdi Saghari25-Aug-11 23:10
mehdi Saghari25-Aug-11 23:10 
GeneralRe: Online Chess Pin
Seraph_summer1-Jun-12 10:47
Seraph_summer1-Jun-12 10:47 
GeneralThis is Beautiful Pin
Vimalsoft(Pty) Ltd18-Mar-09 20:37
professionalVimalsoft(Pty) Ltd18-Mar-09 20:37 
GeneralRe: This is Beautiful Pin
gerardo_00125-Apr-09 20:26
gerardo_00125-Apr-09 20:26 
GeneralRe: This is Beautiful Pin
Vimalsoft(Pty) Ltd27-Apr-09 20:01
professionalVimalsoft(Pty) Ltd27-Apr-09 20:01 
GeneralSample is interesting but not practical Pin
Bill SerGio, The Infomercial King17-Mar-09 2:49
Bill SerGio, The Infomercial King17-Mar-09 2:49 
GeneralRe: Sample is interesting but not practical Pin
defwebserver17-Mar-09 5:31
defwebserver17-Mar-09 5:31 
GeneralRe: Sample is interesting but not practical Pin
alien700017-Mar-09 6:32
alien700017-Mar-09 6:32 
GeneralRe: Sample is interesting but not practical Pin
junnark17-Mar-09 7:21
junnark17-Mar-09 7:21 
GeneralTerrible sample in my opinion Pin
Bill SerGio, The Infomercial King17-Mar-09 9:38
Bill SerGio, The Infomercial King17-Mar-09 9:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.