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.
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.
- 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).
- 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:
39 [OperationContract]
40 void LogInUser(int userID);
As usual, this method is implemented in the LinqChatService
service.
157 void ILinqChatService.LogInUser(int userID)
158 {
159 160 LinqChatDataContext db = new LinqChatDataContext();
161
162 LoggedInUser loggedInUser = new LoggedInUser();
163 loggedInUser.UserID = userID;
164 db.LoggedInUsers.InsertOnSubmit(loggedInUser);
165 db.SubmitChanges();
166 }
- 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.
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 81 LinqChatReference.LinqChatServiceClient proxy =
new LinqChatReference.LinqChatServiceClient();
82 proxy.LogInUserAsync(userID);
83
84 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.
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" /> -->
8 <RowDefinition Height="38" /> -->
9 <RowDefinition Height="10" /> -->
10 <RowDefinition Height="*" /> -->
11 <RowDefinition Height="10" /> -->
12 </Grid.RowDefinitions>
13
14 <Grid.ColumnDefinitions>
15 <ColumnDefinition Width="10" /> -->
16 <ColumnDefinition Width="*" /> -->
17 <ColumnDefinition Width="10" /> -->
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.
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 62 App app = (App)Application.Current;
63 app.RoomID = Convert.ToInt32(linkButton.Name);
64 app.RoomName = linkButton.Content.ToString();
65
66 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.
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" /> -->
8 <RowDefinition Height="46" /> -->
9 <RowDefinition Height="10" /> -->
10 <RowDefinition Height="*" /> -->
11 <RowDefinition Height="10" /> -->
12 <RowDefinition Height="26" /> -->
13 <RowDefinition Height="10" /> -->
14 </Grid.RowDefinitions>
15
16 <Grid.ColumnDefinitions>
17 <ColumnDefinition Width="10" /> -->
18 <ColumnDefinition Width="*" /> -->
19 <ColumnDefinition Width="10" /> -->
20 <ColumnDefinition Width="120" /> -->
21 <ColumnDefinition Width="10" /> -->
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.
- 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.
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 74 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 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 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.
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.
38 private void Application_Startup(object sender, StartupEventArgs e)
39 {
40 this.RootVisual = rootGrid;
41
42 43 if (IsPrivateChatRequest())
44 {
45 46 rootGrid.Children.Add(new PrivateChat());
47 }
48 else
49 {
50 51 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 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 }
- 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.
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.
261 void TimerTick(object sender, EventArgs e)
262 {
263 GetMessages();
264 GetUsers();
265 GetPrivateMessages();
266 }
267
268 private void GetPrivateMessages()
269 {
270 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.
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.
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.
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 289 LayoutRoot.Children.Add(popUp);
290
291 grid.Width = 200;
292 grid.Height = 100;
293 grid.HorizontalAlignment = HorizontalAlignment.Center;
294
295 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 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 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 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 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 344 hpBtn.TargetName = "_blank";
345 hpBtn.NavigateUri = new Uri(link);
346 hpBtn.Click += new RoutedEventHandler(hpBtn_Click);
347
348 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 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 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.
372 void hpBtn_Click(object sender, RoutedEventArgs e)
373 {
374 375 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 386 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
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.
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" /> -->
8 <RowDefinition Height="*" /> -->
9 <RowDefinition Height="10" /> -->
10 <RowDefinition Height="26" /> -->
11 <RowDefinition Height="10" /> -->
12 </Grid.RowDefinitions>
13
14 <Grid.ColumnDefinitions>
15 <ColumnDefinition Width="10" /> -->
16 <ColumnDefinition Width="*" /> -->
17 <ColumnDefinition Width="10" /> -->
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>
- 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.
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 198 199 200 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.
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.
243 void ILinqChatService.InsertPrivateMessageInvite(int userID, int toUserID)
244 {
245 246 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 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 }
- 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.
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.
68 private void InsertMessage()
69 {
70 LinqChatReference.LinqChatServiceClient proxy =
new LinqChatReference.LinqChatServiceClient();
71 proxy.InsertMessageAsync(null, _fromUserID, _toUserID,
TxtMessage.Text, CbxFontColor.SelectionBoxItem.ToString());
72 }
- 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.
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.
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 }
- 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.
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 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.
406 private void BtnChooseRoom_Click(object sender, RoutedEventArgs e)
407 {
408 timer.Stop();
409 App app = (App)Application.Current;
410
411 412 LinqChatReference.LinqChatServiceClient proxy =
new LinqChatReference.LinqChatServiceClient();
413 proxy.LeaveRoomAsync(_userID, _roomId, app.UserName);
414
415 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.
194 void ILinqChatService.LeaveRoom(int userID, int roomID, string username)
195 {
196 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 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.