Отделенный код Chatroom.xaml.cs

1.    Когда пользователь перенаправляется на пользовательский управляющий элемент Chatroom.xaml из пользовательского управляющего элемента Login.xaml, проверяется вошел ли пользователь, путем проверки любого из значений, хранимых в App.xaml. Было решено проверить имя пользователя (строка 34). Если это значение пустое, то пользователь еще не вошел, и поэтому должен быть перенаправлен на страницу входа (строка 36).

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

2.    Так как Grid по имени "LayoutRoot" является корневым или главным контейнером для всех остальных управляющих элементов, событие LayoutRoot_Loaded вызывается при загрузке Grid. Оно имитирует событие Page_load страницы ASP.NET.

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

Как видно, при загрузке сетки происходит несколько вещей. Первое – активизация управляющего элемента TxtMessage, куда вводятся сообщения. Команда TxtMessage.Focus(), показанная выше в строке 48, не работает сама по себе. Надо сделать несколько вещей, чтобы активизировать этот управляющий элемент при загрузке сетки.

Первое, что надо сделать вместе с кодом TxtMessage.Focus(), - активизировать управляющий элемент Silverlight ASP.NET в главной веб-странице Chatroom.aspx. Как показано ниже, можно активизировать управляющий элемент Silverlight ASP.NET с помощью JavaScript.

<html xmlns="http://www.w3.org/1999/xhtml" style="height:100%;">
<head id="Head1" runat="server">
    <title>Silverlight 2 Chatroom</title>
    <script type="Text/javascript">
        window.onload = function ()
        {
            document.getElementById('Xaml1').focus();
        }
    </script>
</head>

<body style="height:100%;margin:0; padding:0; width: 100%;">
    <form id="form1" runat="server" style="height:100%;">
       <asp:ScriptManager ID="ScriptManager1" runat="server" />
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <div style="width: 100%; text-align:center; height: 100%;">
                    <asp:Silverlight ID="Xaml1" runat="server"
                       Source="~/ClientBin/Silverlight2Chat.xap"
                       MinimumVersion="2.0.31005.0"
                       Width="600" Height="100%" />
                </div>
            </ContentTemplate>
        </asp:UpdatePanel>
    </form>
</body>
</html>

3.    Асинхронный доступ к службе WCF с помощью посредника: Все следующие методы и/или события в файле отделенного кода Chatroom.xaml.cs обращаются к созданной ранее службе WCF:

o    InsertNewlyJoinedMessage()
o    GetUsers()
o    InsertMessage()
o    GetMessages()
o    BtnLogOut_Click

Обратите внимание, что вся обработка производится в приложении Silverlight асинхронно. При получении значений, поступающих из службы WCF в приложение Silverlight, надо создать обработчик события "Completed" для события Completed  посредника WCF и вызвать метод "Async" посредника WCF. В качестве примере рассмотрен метод GetUsers() в Chatroom.xaml.cs.

Строка 57 и строки 61-68 не нужны, если не извлекаются никакие значения из службы WCF, как при вставке значения в базу данных. Однако в данном случае извлекаются пользователи, и извлеченные значения присваиваются в управляющих элементах Silverlight. При завершении вызова GetUsersAsync вызывается обработчик события GetUsersCompleted. Извлеченные пользователи (или извлеченное значение) затем присваиваются аргументам события e.Result обработчика события GetUserCompleted. Тип возвращаемой переменной e.Result – динамический, зависящий от извлекаемого значения. При извлечении целого значения это будет тип int; при извлечении строкового значения это будет тип string, и т.д. В данном случае извлекается коллекция, поэтому она присваивается типу ObservableColletion.
Сигнатура метода GetUsersAsync в строке 58 идентична сигнатуре метода ILinqChatService.GetUsers в службе WCF; так вызывается метод службы WCF.

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

Метод GetUsers вызывается в службе WCF (LinqChatService.svc.cs). Он вызвал GetUsers, и не был создан обработчик события GetUsersCompleted или метод GetUsersAsync.

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

4.    Отправка и прием сообщений: Сообщения отправляются при нажатии клавиши «Ввод клавиатуры» или при нажатии кнопки «Отправить». При нажатии клавиши возврата каретки на клавиатуре или кнопки «Отправить» выполняются еще две вещи вместе с сохранением сообщения в базе данных: извлекаются сообщения из базы данных и извлекаются пользователи из базы данных.

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

Таймер устанавливается при загрузке главной Grid(сетка).

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

Каждые 3 секунды, если не нажимается клавиша «Ввод клавиатуры», вызывается событие срабатывания таймера, чтобы извлечь сообщения и извлечь пользователей из базы данных.

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

Таймер останавливается при каждом вводе сообщения и затем возобновляется при нажатии клавиши «Ввод». Это позволяет остановить таймер на основании обновления управляющего элемента TxtMessage при вводе с клавиатуры.

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

5.    Установка полосы прокрутки внизу сообщений: Для установки полосы прокрутки внизу сообщений передается максимальное значение двойной точности в член ScrollToVerticalOffset управляющего элемента XAML просмотрщика прокрутки.

179     private void SetScrollBarToBottom()
180     {
181         // установить полосу прокрутки внизу
182         SvwrMessages.UpdateLayout();
183         SvwrMessages.ScrollToVerticalOffset(double.MaxValue);
184     }

6.    Демонстрация сообщений в управляющем элементе просмотра прокрутки: Пожалуй, это самая интересная часть руководства. На нее ушло больше всего времени при программировании приложения чата Silverlight 2. Получение всех сообщений и присвоение их в управляющий элемент ListBox XAML не оправдывает себя. Должна быть возможность делать разный цвет или оттенок для пользователя и сообщения в одной и той же строке. Также нужен чередующийся фон для каждого сообщения. Это достигается путем написания дополнительного кода для списка пользователей. При этом не приходится извлекать все сообщения для текущего раздела; извлекаются только еще не извлеченные сообщения, потому что при добавлении сообщения в управляющий элемент панели с помощью данного метода пользовательский интерфейс запоминает все, что было добавлено, и не надо добавлять их снова.

В строке 109 создается экземпляр горизонтальной панели-пачки, который будет добавлен в основную панель-пачку в строке 167. Это было сделано программно, так что можно чередовать цвет фона, как показано в строках 155-116. Также добавляется TextBlock, который будет хранить выделенное жирным имя пользователя в строке 130, и TextBox, который будет хранить сообщение в строке 164, для указанной панели-пачки. Это было весьма просто.

Почему используется TextBox (строка 133) вместо TextBlock для сообщения рядом с именем пользователя? В Silverlight есть следующая ошибка: при использовании события KeyDown поля сообщения возвраты каретки кодируются в сообщениях, вводимых при нажатии кнопки «Ввод». Это значит, что сообщения разбиваются на две строки в разных местах. Допустим, введено "Привет, как твои дела?" в текстовое поле сообщения, и нажата кнопка ввода. Сообщение "Привет, как твои дела?" разобьется на две строки при присвоении TextBlock значения Text поля сообщения, следовательно, оно должно читаться так:

Hello how are
you doing?

или так:

Hello 
how are you doing?

Нет управляющего элемента, куда вставляется символ перевода строки; он и не нужен. Выяснилось, что присвоение значения Text поля сообщения другому TextBox устраняет эту проблему.

100     void proxy_GetMessagesCompleted(object sender, 
             Silverlight2Chat.LinqChatReference.GetMessagesCompletedEventArgs e)
101     {
102         if (e.Error == null)
103         {
104             ObservableCollection<LinqChatReference.MessageContract>
                                                             messages = e.Result;
105
106             foreach (var message in messages)
107             {
108                 // добавить горизонтальную панель-пачку
109                 StackPanel sp = new StackPanel();
110                 sp.Orientation = Orientation.Horizontal;
111                 sp.HorizontalAlignment = HorizontalAlignment.Left;
112                 sp.Width = SpnlMessages.ActualWidth;
113
114                 // поместить чередующийся фон
115                 if (!_isWithBackground)
116                     sp.Background = new SolidColorBrush(
                             System.Windows.Media.Color.FromArgb(100, 235, 235, 235));
117
118                 // добавить TextBlock для хранения имени пользователя для панели-пачки
119                 TextBlock name = new TextBlock();
120                 name.Text = message.UserName + ": ";
121                 name.FontSize = 12.0;
122                 name.FontWeight = FontWeights.Bold;
123                 name.Padding = new Thickness(4, 8, 0, 8);
124
125                 if (message.Color == "Gray")
126                     name.Foreground = new SolidColorBrush(Colors.Gray);
127                 else
128                     name.Foreground = new SolidColorBrush(Colors.Black);
129
130                 sp.Children.Add(name);
131
132                 // добавить TextBox для хранения сообщения пользователя для панели-пачки
133                 TextBox text = new TextBox();
134                 text.BorderBrush = new SolidColorBrush(Colors.Transparent);
135                 text.FontSize = 12.0;
136                 text.Text = message.Text.Trim();
137                 text.VerticalAlignment = VerticalAlignment.Top;
138                 text.Width = SpnlMessages.ActualWidth - name.ActualWidth;
139                 text.TextWrapping = TextWrapping.Wrap;
140                 text.Margin = new Thickness(0, 4, 4, 0);
141                 text.IsReadOnly = true;
142
143                 // изменить цвет текста исходя из выбранного пользователем цвета
144                 if(message.Color == "Red")
145                     text.Foreground = new SolidColorBrush(Colors.Red);
146                 else if (message.Color == "Blue")
147                     text.Foreground = new SolidColorBrush(Colors.Blue);
148                 else if (message.Color == "Gray")
149                     text.Foreground = new SolidColorBrush(Colors.Gray);
150                 else
151                     text.Foreground = new SolidColorBrush(Colors.Black);
152
153                 // поместить чередующийся фон
154                 if (!_isWithBackground)
155                 {
156                     text.Background = new SolidColorBrush(
                            System.Windows.Media.Color.FromArgb(100, 235, 235, 235));
157                     _isWithBackground = true;
158                 }
159                 else
160                 {
161                     _isWithBackground = false;
162                 }
163
164                 sp.Children.Add(text);
165
166                 // добавить горизонтальную панель-пачку к основной панели-пачке
167                 SpnlMessages.Children.Add(sp);
168
169                 // запомнить идентификатор последнего сообщения
170                 _lastMessageId = message.MessageID;
171             }
172
173             SetScrollBarToBottom();
174             TxtMessage.Text = String.Empty;
175             TxtMessage.Focus();
176         }
177     }

7.    Выход (разрегистрация): При нажатии кнопки выхода останавливается таймер (строка 227). Далее удаляется пользователь из таблицы LoggedInUser путем вызова службы WCF в строке 229-230. Обработчик события Completed не вызывается, так как не извлекаются никакие значения из базы данных. И наконец, пользователь перенаправляется на пользовательский управляющий элемент входа XAML.

Как сказано ранее, можно разрегистрировать пользователя, когда он нажимает кнопку закрытия браузера, путем перехвата события unload тега body в главной странице ASP.NET.

225     private void BtnLogOut_Click(object sender, RoutedEventArgs e)
226     {
227         timer.Stop();
228
229         LinqChatReference.LinqChatServiceClient proxy =
                      new LinqChatReference.LinqChatServiceClient();
230         proxy.LogOutUserAsync(_userID, _roomId, TxtbLoggedInUser.Text); 
231
232         // перенаправить на страницу входа
233         App app = (App)Application.Current;
234         app.RedirectTo(new Login());
235     }