使用 Silverlight 构建带有多个房间和私聊的 Silverlight Web 聊天室 - 第二部分
在第一部分中,我们构建了一个简单的Silverlight 2 Web聊天室。现在我们将添加功能,使用户能够从多个聊天室中选择,以及与其他用户进行私人聊天。
引言
在第一部分中,我们构建了一个简单的Silverlight 2 Web聊天室。现在我们将添加功能,使用户能够从多个聊天室中选择,以及与其他用户进行私人聊天。看看Silverlight Web聊天室的修订快照。
要求
我们将添加一种方式供用户从聊天室列表中选择进入。还将建立与其他用户的私人聊天。旧的要求仍然有效,如下所示:
- 必须可以随时随地访问,并且无需下载和安装任何组件。这就是为什么我们要创建一个Web聊天室。
- Web聊天室必须“无闪烁”。您会发现Silverlight中的所有处理都是异步完成的。
- 我们希望能够使用数据库监控聊天对话。我们将使用 MS SQL Server 存储对话和用户信息。
- 使用LINQ-to-SQL的动态SQL,而不是存储过程,以实现超快速编码。
数据库
我们将使用现有的数据库,并在PrivateMessage表中添加一个附加字段,如下所示。有关表描述,请参阅第一部分。下面是附加的PrivateMessage表描述。
- PrivateMessage:包含私人邀请信息。当向另一个用户发送私人聊天邀请时,此处会添加一条记录。
新添加的XAML文件
很简单,因为我们在第一部分中已经开始使用基于其功能的XAML文件,所以我们只需要添加两个(2)个新文件:Rooms.xaml 和 PrivateChat.xaml。
- Rooms.xaml:显示数据库中Rooms表中列出的聊天室列表。因此,要为此Web聊天应用程序添加新房间,只需转到Rooms表并添加任意数量的房间,无需额外编码。
- PrivateChat.xaml:这是当被另一用户邀请聊天并接受邀请时弹出的私人聊天窗口。
登录更改
我们的登录机制(Login.xaml.cs)有一个非常小的改动。
- 当用户通过身份验证后,我们将该用户保存到LoggedInUser表中,如第81-82行所示。请注意,我们传递的唯一信息是用户ID,因为该用户尚未选择房间。第82行(Login.xaml.cs)调用
ILinqChatService
接口中的一个新方法。 - 用户不会直接进入第一部分中唯一可用的房间,而是被重定向到房间列表页面(Rooms.xaml),如第88行所示。
39 [OperationContract]
40 void LogInUser(int userID);
像往常一样,此方法在LinqChatService
服务中实现。
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 }
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 }
选择房间
Rooms.xaml的界面非常简单。它包含两个(2)个控件:一个用于显示标题的TextBlock
,以及一个容纳垂直堆叠的可用房间列表的StackPanel
。
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>
在代码文件(Rooms.xaml.cs)中,我们首先在第26行通过检查用户名来检查用户是否已登录。我们也可以同样检查用户ID而不是用户名。然后在第29行获取聊天室。如第一部分所述,因为我们尝试从客户端服务代理(WCF服务)获取值,所以我们在调用“Async”事件之前调用客户端代理的“Completed”事件,第35-36行。proxy_GetRoomsCompleted
事件从Room表中检索所有可用房间,并将它们分配给一个HyperlinkButton
,显示在第39-55行。请注意,第50行调用了HyperlinkButton
的Click
事件。当用户单击聊天室列表中列出的某个房间时,HyperlinkButton
的Click
事件仅“记住”房间ID和房间名称(第62-64行),然后将用户重定向到聊天室页面(Chatroom.xaml)。有关WCF服务基础知识的更多信息,请阅读第一部分。
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.xaml页面的GUI(图形用户界面)也有一个非常小的修订。GUI修订主要与功能无关。登录用户名已移至标题上方,位于StackPanel
中,现在是灰色的(第25行)。在“注销”按钮上方添加了一个“选择其他房间”按钮(第30行),也位于StackPanel
中。请注意,我们正在使用一个简单的StackPanel
(第43-44行)来显示用户列表,而不是像第一部分中使用的ItemsControl
数据模板控件,我将在本文稍后解释原因。
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>
这里的绝大多数更改都是为了支持与其他用户的私人消息以及离开当前房间以选择其他房间。因此,我们将直接深入探讨与其他用户进行私人聊天的细节。
- 获取用户:如上所述,我们将
ItemsControl
更改为简单的StackPanel
,用于显示用户列表。主要原因之一是,我们希望除了当前登录用户(即您)之外,所有用户都显示为HyperlinkButton
(第77-107行)。很简单,想法是:您不希望单击自己的名字而不小心与自己聊天。 - 响应私人聊天邀请。现在回到Chatroom.xaml用户控件。我们刚才讨论了打开私人聊天窗口以邀请他人私密聊天的过程。此时,您尚未发送邀请,只是打开了私人聊天窗口。您发送的第一个消息发送给其他用户时,就会发出邀请。稍后在本文的
PrivateChat
用户控件部分,我将讨论这个过程。目前,我将讨论被邀请者(您邀请的其他人)如何响应您的邀请。
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 }
要私密地与某人聊天,我希望在单击用户列表中的某个用户时打开一个新的浏览器。目前,Silverlight的HyperlinkButton
控件只能分配绝对URL。因此,要打开一个新浏览器,我们需要在浏览器中操作当前URL(上面的第92-100行)来为每个用户构建一个链接。请注意,上面的第96行显示,当单击此用户时,它将打开“Chatroom.aspx”ASP.NET页面。这是因为,正如我们在第一部分中所讨论的,所有XAML用户控件都将由一个(1)ASP.NET页面托管,该页面就是Chatroom.aspx页面。由于新浏览器意味着聊天应用程序的新实例,所以我们遇到的第一个XAML用户控件是App.xaml。那么,如何加载PrivateChat.xaml用户控件呢?我们当然不希望每次单击另一个用户进行私人聊天时都输入用户名和密码。
这就是为什么我们在上面第96-100行中看到的一些查询字符串键/值对被分配给每个用户链接。例如“fromusername”、“touserid”等。我们将使用相应的值来自动登录App.xaml用户控件中的用户。“isinvited
”键(第100行)仅表示您是邀请另一个用户私密聊天的人,而不是被邀请的人,isinvited=false;
。我稍后会详细介绍我们如何使用此值。
让我们跳转到App.xaml用户控件,看看这个过程是如何工作的。您会注意到,我们添加了一些公共属性来保存和存储我们的应用程序级值。通过查看下面的突出显示的的代码,您可以立即猜到,在用户列表中的Chatroom.xaml用户控件的HyperlinkButton
(上面)中看到的查询字符串键/值对是直接相关的。
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; }
下面的第64-69行从查询字符串中检索键/值对,然后将它们分配给App.xaml用户控件的公共属性。要加载PrivateChat
用户控件而不是Login
用户控件,我们只需检查URL中的至少一个值,表明该用户已打开私人聊天窗口。在这种情况下,我们只是检查第61行中的“fromusername”键。第43-53行确定加载哪个XAML用户控件。
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 }
我们在TimerControl
的Tick
事件中检查来自其他用户的私人消息邀请。在此事件中,我们调用GetPrivateMessages
方法来检查任何邀请。这当然会像上面讨论的那样,通过客户端代理调用我们的WCF服务。
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 }
下面显示了获取私人消息邀请的实现,它与ILinqChatService
接口的其他实现一起,在LinqChatService.svc服务中。
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 }
每次邀请都会导致显示一个Silverlight Popup
控件,如下图所示。
构建此Popup
控件的每一项邀请并导致弹出的代码可以在下面的GetPrivateMessage
Completed
事件中找到。我知道您在想,“显示一个非常简单的弹出控件需要这么多代码吗?”答案是肯定的。更令人惊讶的是,每个按钮(关闭和立即聊天)都有自己的Click事件代码,因此代码会更长(第346行和第357行)。下面显示的只是在Popup
控件的子控件Grid
控件中添加了一个TextBlock
控件,一个虚拟的聊天按钮控件,一个关闭按钮控件和一个聊天HyperlinkButton
控件。Popup
控件设置为显示或设置为可见,如第285行所示。
虚拟的聊天按钮控件用作实际的聊天HyperlinkButton
控件的“背景”控件,以便我们可以使HyperlinkButton
控件看起来像一个按钮。这只是我的偏好,您也可以轻松地将HyperlinkButton
控件的Content
属性设置为显示一个按钮,甚至设计HyperlinkButton
以显示成按钮。您可以看到,Button
和HyperlinkButton
位于同一位置(第316行和第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 // 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 }
单击关闭按钮和立即聊天HyperlinkButton
时,它们的功能几乎相同,如它们各自的Click
事件所示。它们只是获取要关闭的Popup
控件的名称,然后通过执行popUp.IsOpen = false
来关闭它们。但是,立即聊天HyperlinkButton
也会在浏览器中打开PrivateChat.xaml用户控件,这与我们之前打开新浏览器邀请某人私密聊天的方式非常相似。这是因为我们使用了与上面第332-341行所示几乎相同的代码,唯一的区别是:“isinvited
”的值(第340行中为“true
”,表示您是被邀请私密聊天的人),并且在第341行添加了另一个键/值对,“timeusersentinvitation
”,它表示其他人向您发送私人聊天邀请的时间。我们将使用“timeusersentinvitation
”的值来获取自对方发送聊天邀请以来的所有私人消息。我稍后会详细介绍。
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用户控件
如您所见,此控件看起来与Chatroom用户控件相同,除了省略了某些控件,例如标题、注销按钮等。并且由于相似性,发送消息或接收消息的方式相同,只是它们只发送给一个人。下面显示的是PrivateChat用户控件的GUI代码。
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>
- 邀请其他人进行私人聊天。当您单击Chatroom.xaml页面上的某个用户时,此私人聊天窗口将出现。要邀请某人进行私人聊天,您首先需要向该用户发送至少一条消息。第201-205行。
- 发送私人消息。在聊天室中,我们获取所有用户正在聊天的消息。在这里,我们只应获取两个私密聊天用户之间的消息。这很容易解决。在Chatroom.xaml用户控件中,当我们向Message表中插入消息时,“
_toUserID
”设置为null
。在这里,我们只需将该值设置为我们正在尝试聊天的用户。_toUserID
值在此用户控件的构造函数中设置,该值从App.xaml的公共属性中检索。 - 获取私人消息。就像发送私人消息给其他人一样,我们需要填充
_fromUserID
和_toUserID
变量,以便只检索用于独占聊天用户的消息。我们从“邀请”用户首次发送私人聊天邀请时需要填充的另一条信息被赋给了_timeUserSentInvitation
变量。这样,我们将只检索从那时开始的消息。 - 调整私人聊天窗口大小。在撰写本文时,Silverlight 2不幸地没有在指定大小打开新窗口的功能。请记住,我们仅使用一个ASP.NET页面托管所有XAML用户控件。因此,要在PrivateChat.xaml用户控件加载时动态调整此ASP.NET页面的大小,我们需要某种指示,某种将信息从XAML用户控件传递到ASP.NET页面的方法。幸运的是,我们可以编写一些JavaScript技巧来完成这项工作。使用JavaScript,我们可以检查Chatroom.aspx页面上至少一个查询字符串键/值对。在这种情况下,我们将检查“fromusername”键。如果该键存在,我们将窗口(Chatroom.aspx)的大小调整为640 x 460。
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 }
上面第203行调用的方法如下所示。因为我们不期望WCF服务(如下所示)返回任何内容,所以我们不需要调用InsertPrivateMessageInvite
对象的Completed
事件。简而言之,我们只需要调用此客户端代理的“Async
”方法。
214 private void InviteOtherUserToChat()
215 {
216 LinqChatReference.LinqChatServiceClient proxy =
new LinqChatReference.LinqChatServiceClient();
217 proxy.InsertPrivateMessageInviteAsync(_fromUserID, _toUserID);
218 }
上面第217行的Async
WCF调用会调用LinqChatService.cs中找到的WCF服务方法。请注意,上面的异步调用和下面的方法具有相同的签名。下面的服务将在PrivateMessage表中插入一条记录。
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 }
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 }
这使得发送消息给其他人非常简单。
68 private void InsertMessage()
69 {
70 LinqChatReference.LinqChatServiceClient proxy =
new LinqChatReference.LinqChatServiceClient();
71 proxy.InsertMessageAsync(null, _fromUserID, _toUserID,
TxtMessage.Text, CbxFontColor.SelectionBoxItem.ToString());
72 }
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 }
这里的GetPrivateMessagesCompleted
事件的作用与Chatroom.xaml用户控件中的GetMessagesCompleted
事件相同,因此我将不花时间解释它。有关Completed
事件的更多信息,请参阅第一部分。上面的Async代理代码调用LinqChatService.svc.cs中找到的WCF服务GetPrivateMessages
。我想强调的一点是构建查询的方式,特别是第68-69行中的代码。我们正在检索私密聊天用户之间发送的(“发送给”)任何一方的消息。
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 }
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>
选择其他房间
您需要离开当前房间才能进入另一个房间。您可以单击Chatroom.xaml用户控件中带有“选择其他房间”文本的按钮。下面显示了Click
事件。
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 }
上面的代理客户端方法调用下面的WCF服务方法。该方法与注销方法唯一的区别在于,这里我们只在LoggedInUser表中设置RoomID = null
,而在注销方法中,我们完全从LoggedInUser表中删除该用户。
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 }
由于这是一个Web聊天应用程序,您当然可以打开多个浏览器,在每个浏览器中登录,然后同时进入不同的房间。
最后的话
希望您从本文中学到了东西。我没有讨论WCF(Windows Communication Foundation)的基础知识,也没有讨论聊天室消息的格式,因为这些以及其他内容可以在第一部分中找到。本文旨在学习使用Silverlight 2和MS SQL Server建立受监控的私人聊天的过程。有很多方面可以改进。例如,为了提高性能,您可以将所有数据库调用更改为使用存储过程而不是动态SQL。与其过于啰嗦,您还可以将数据库调用合并成一次操作,而不是多次操作,例如在Chatroom.xaml用户控件的TimerTick
事件中找到的方法。
一如既往,代码和文章按“原样”提供,绝对没有任何保证。请自行承担使用风险。
注意:原文可以在这里找到:http://www.junnark.com/Articles/Build-a-Silverlight-Web-Chatroom-with-Multiple-Rooms-and-Private-Chat-Part-2.aspx。