Создание приложения веб-чата на Silverlight 2

ОГЛАВЛЕНИЕ

Silverlight 2 сейчас доступен всем, и лучший способ его изучить – создать маленькое веб-приложение с помощью этой чудесной технологии. Именно это и будет сделано здесь. Будет создан веб-чат с помощью Silverlight 2. Также будут рассказаны некоторые вещи, выясненные о данной новой технологии. Ниже показан снимок приложения веб-чата на Silverlight 2, которое будет создано.

•    Скачать исходники - 595 KB

Требования

Будет с нуля создано очень простое приложение веб-чата с помощью Silverlight 2 из спортивного интереса. Приложение чата будет содержать два пользовательских управляющих элемента XAML: управляющий элемент входа в чат и управляющий элемент раздела чата. Большая часть руководства посвящена странице раздела чата. Надо достичь следующего:
•    Чат должен быть доступен везде и без необходимости скачивать и устанавливать компоненты. Поэтому будет создан веб-чат.
•    Веб-чат не должен мерцать. Вы узнаете, что вся обработка в Silverlight производится асинхронно.
•    Должна быть возможность контролировать разговоры в чате с помощью базы данных. MS SQL Server будет использоваться для хранения разговоров и информации о пользователе.
•    Использование динамического SQL с помощью LINQ-для-SQL вместо хранимых процедур для очень быстрого программирования.

Веселье начинается

1.    Сначала надо создать базу данных с помощью MS SQL Server 2005/2008. Для простоты используется следующая база данных:

       o    Пользователь: Содержит информацию о пользователе. Не стесняйтесь добавить свои собственные поля, такие как адрес, город, и т.д.
       o    Сообщение: будет хранить сообщения, отправленные пользователями во время беседы.
       o    Раздел: Содержит информацию о разных разделах чата. Может быть несколько разделов. Но в данном руководстве пока будет использоваться только один раздел.
       o    Вошедшие пользователи: будет хранить пользователей, вошедших/общающихся в разделе(ах) чата. При входе пользователя в раздел информация будет сохраняться тут; таким образом, можно показать список пользователей, общающихся в конкретном разделе.

2.    В Visual Studio создайте новый проект приложения Silverlight. Чтобы сделать это, перейдите в меню «Файл», выберите «Новый», затем щелкните по «Проекту». В поле «Новый проект» щелкните по Silverlight в «Типах проекта», затем выберите «Приложение Silverlight» в «Шаблонах», введите имя приложения. Затем щелкните по OK.

В следующем окне выберите "Добавить новый веб-проект ASP.NET...." и нажмите OK. В проводнике решений появятся созданные приложение Silverlight и веб-проект ASP.NET Web Project. В сгенерированном веб-проекте будет размещаться приложение чата Silverlight 2.

3.    При создании приложения Silverlight были сгенерированы некоторые вещи. В приложении Silverlight есть два файла XAML, App.xaml и Page.xaml. Файлы XAML, в отличие от веб-форм ASP.NET, являются пользовательскими управляющими элементами, размещаемыми в веб-форме ASP.NET или на странице HTML. Все файлы XAML по умолчанию размещаются на одной веб-странице. Также можно размещать файл или пользовательский управляющий элемент XAML на каждой веб-странице, что не рекомендуется.
      o    App.xaml: Работает подобно Global.asax в ASP.NET. Это самый первый управляющий элемент, которого достигает приложение Silverlight. Поскольку Silverlight является клиентской технологией, то не имеет сессий и не позволяет осуществить Response.Redirect для следующего пользовательского управляющего элемента. Тут хранятся переменные и свойства, доступные другим пользовательским управляющим элементам. Это возможно только при размещении других пользовательских управляющих элементов на одной и той же странице ASP.NET.
      o    Page.xaml: По умолчанию, без изменения кода, это пользовательский управляющий элемент, размещаемый в сгенерированной веб-странице ASP.NET при создании проекта. В данном руководстве этот пользовательский управляющий элемент не применяется. Зато будет создан собственный пользовательский управляющий элемент и назван в соответствии с использованием.

На стороне веб-приложения имеются следующие файлы:
      o    Default.aspx: Этот файл вообще не применяется, поэтому удаляется из проекта.
      o    Chatroom.aspx: Фактическая страница, в которой будет размещаться пользовательский управляющий элемент Silverlight. Ее можно назначить начальной страницей. Этот файл ссылается на файл ".xap", расположенный в папке ClientBin. Сразу после создания проекта в папке ClientBin нет файла .xap. После сборки веб-проекта .xap будет в папке ClientBin. Сгенерированный файл .xap является скомпилированным приложением Silverlight.
      o    Silverlight2ChatTestPage.html: Ловушка для ошибочной страницы Silverlight. Когда происходит ошибка Silverlight, на экране показывается эта страница.
Ниже приведен снимок проекта чата Silverlight 2:

Теперь, когда вы знаете приложение Silverlight, размещаемое в веб-странице ASP.NET, продолжается руководство по веб-чату.
4.    Были созданы два пользовательских управляющих элемента: Login.xaml и Chatroom.xaml, чтобы показать, как перейти от одного пользовательского управляющего элемента к другому и обратно. Также показано, как помнить пользователя между пользовательскими управляющими элементами наподобие сессии. Эти пользовательские управляющие элементы служат для входа в чат и для общения.

5.    Надо войти в чат, прежде чем начать общаться. Пользователи должны существовать в таблице пользователей чата LinqChat User. Раздел тоже должен существовать в таблице разделов чата LinqChat Room. Этот раздел должен иметь идентификатор RoomID = 1, жёстко заданный в Chatroom.xaml.cs. Конечно, может быть несколько разделов, но в руководстве используется всего один.


Пользовательские управляющие элементы XAML

В Silverlight 2 есть три базовых контейнерных управляющих элементов XAML; Canvas, StackPanel и Grid. В этом проекте используются управляющие элементы Grid и StackPanel. Grid работает как таблица HTML; вместо использования TR для строк и TD для столбцов она использует RowDefinitions и ColumnDefinitions соответственно. StackPanel может хранить другие управляющие элементы XAML в пачке горизонтальной или вертикально. Дополнительные сведения об этих управляющих элементах смотрите на сайте Silverlight: http://www.silverlight.net.

Login.xaml

Ниже показан код пользовательского интерфейса XAML, генерирующий простую форму входа, показанную выше. Все управляющие элементы XAML заключены в управляющий элемент Grid. Расположение управляющих элементов ясно из зеленых комментариев. Надо отметить несколько вещей:
1.    Все управляющие элементы сообщений об ошибках скрытые, помеченные как Visibility="Collapsed". Видимость сообщений об ошибках контролируется в отделенном коде.
2.    Для имитации управляющих элементов проверки правильности ASP.NET были добавлены события LostFocus и MouseEnter в управляющие элементы имени пользователя и пароля.

<UserControl x:Class="Silverlight2Chat.Login"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="510" Height="118">
    <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
        <Grid.RowDefinitions>
            <RowDefinition Height="10" />       <!-- отступ -->
            <RowDefinition Height="26" />       <!—имя пользователя -->
            <RowDefinition Height="6" />        <!-- отступ -->
            <RowDefinition Height="26" />       <!-- пароль -->
            <RowDefinition Height="10" />       <!-- отступ -->
            <RowDefinition Height="30" />       <!-- кнопка -->
            <RowDefinition Height="6" />        <!-- отступ -->
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10" />     <!-- отступ -->
            <ColumnDefinition Width="80" />     <!-- надписи -->
            <ColumnDefinition Width="10" />     <!-- отступ -->
            <ColumnDefinition Width="200" />    <!—управляющие элементы -->
            <ColumnDefinition Width="10" />     <!-- отступ -->
            <ColumnDefinition Width="*" />      <!—сообщения об ошибках -->
            <ColumnDefinition Width="10" />     <!-- отступ -->
        </Grid.ColumnDefinitions>

        <!-- надписи -->
        <TextBlock Text="User Name:" Grid.Row="1"
          Grid.Column="1" FontSize="12"
          VerticalAlignment="Center" />
        <TextBlock Text="Password:" Grid.Row="3"
          Grid.Column="1" FontSize="12"
          VerticalAlignment="Center" />

        <!-- управляющие элементы -->
        <TextBox x:Name="TxtUserName" Grid.Row="1"
           Grid.Column="3" FontSize="12" BorderThickness="2"
           LostFocus="TxtUserName_LostFocus" MouseEnter="TxtUserName_MouseEnter" />

        <PasswordBox x:Name="PbxPassword" Grid.Row="3"
            Grid.Column="3" FontSize="12" BorderThickness="2"
            LostFocus="PbxPassword_LostFocus"
            MouseEnter="PbxPassword_MouseEnter" />

        <Button x:Name="BtnLogin" Grid.Row="5"
            Grid.Column="3" Content="Login"
            FontSize="12" Click="BtnLogin_Click" />

        <!-- сообщения об ошибках -->
        <TextBlock x:Name="TxtbUserNameRequired" Text="User Name is Required!"
           Foreground="Red" Grid.Row="1" Grid.Column="5"
           FontSize="12" VerticalAlignment="Center" Visibility="Collapsed" />
 
        <TextBlock x:Name="TxtbPasswordRequired"
           Text="Password is Required!" Foreground="Red"
           Grid.Row="3" Grid.Column="5" FontSize="12"
           VerticalAlignment="Center" Visibility="Collapsed" />
 
        <TextBlock x:Name="TxtbNotfound" Text="Invalid Username or Password!"
           Foreground="Red" Grid.Row="5" Grid.Column="5"
           FontSize="12" VerticalAlignment="Center"
           Visibility="Collapsed" />
    </Grid>
</UserControl>

Когда пользователь нажимает кнопку «Войти», переменная, сообщающая, что пользователь уже нажал кнопку «Войти» хотя бы один раз, устанавливается в истину. Это помогает проверить правильность управляющего элемента имени пользователя или пароля на стороне клиента с помощью событий LostFocus и MouseEnter, без необходимости для пользователя повторно нажимать кнопку «Войти», тем самым имитируя управляющие элементы проверки правильности ASP.NET перед фактической проверкой правильности имени пользователя и пароля на сервере.

24     private void BtnLogin_Click(object sender, RoutedEventArgs e)
25     {
26         _isLoginButtonClicked = true;
27         ValidateUserName();
28         ValidatePassword();
29
30         if (!String.IsNullOrEmpty(TxtUserName.Text) &&
               !String.IsNullOrEmpty(PbxPassword.Password))
31         {
32             // проверить правильность пользователя на основе имени пользователя и пароля
33             ValidateUser();
34         }
35     }

Методы ValidateUserName и ValidatePassword только проверяют, являются ли поля имени пользователя и пароля пустыми, затем соответственно показывают или скрывают сообщение об ошибке.

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

В методе ValidateUser используется служба WCF (Windows Communication Foundation – служба обмена данными между приложениями) для проверки правильности имени пользователя и пароля, введенных пользователем. Если пользователь найден в базе данных, то пользователь перенаправляется в раздел чата XAML; если нет – показывается сообщение об ошибке. К этому стоит вернуться позже, но сначала надо рассмотреть службу WCF.


Служба обмена данными между приложениями (WCF)

Так как Silverlight является клиентской технологией, есть несколько способов управления доступом к базе данных, все из которых использует своего рода AJAX или JavaScript. Предпочтительно использовать WCF вместо классической технологии веб-службы (.asmx). Чтобы добавить службу WCF, щелкните правой кнопкой мыши по веб-проекту, выберите «Добавить новый элемент», затем выберите "Служба WCF", как показано ниже. При нажатии «Добавить» в веб-проект добавляются три файла: файл ".svc", соответствующий файл кода ".svc.cs" и файл интерфейса "I.....cs". Также вставляется запись в файл Web.config.

Интерфейс

Файл интерфейса служит основным интерфейсом для службы WCF. В нем определяются все методы, реализуемые службой WCF. Интерфейс помечен как ServiceContract. Каждый из методов помечен как OperationContract.

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

Внутри этого интерфейса было создано два открытых класса. Классы определяют свойства соответствующего DataContract. Члены данных класса MessageContract прямо увязаны с таблицей «Сообщение», а члены данных класса UserContract прямо увязаны с таблицей «Пользователь» в базе данных. Замечание: были добавлены только члены данных, используемые в этом руководстве. Также эти два класса были встроены в интерфейс, а не сделаны двумя отдельными открытыми классами.

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

Реализация интерфейса в LinqChatService

Интерфейс реализован в файле отделенного кода службы WCF, "LinqChatService.svc.cs". Для реализации интерфейса сначала надо унаследовать интерфейс, как показано ниже. Наследование запрограммировано по умолчанию.

11     public class LinqChatService : ILinqChatService

Щелкните правой кнопкой мыши по интерфейсу "ILinqChatService", выберите «Реализовать интерфейс», далее выберите «Реализовать интерфейс явно», как показано ниже. Это генерирует члены интерфейса внутри метки области.

Методы члена интерфейса

Теперь, когда известно, как реализовать члены интерфейса, рассматривается реализация каждого из методов члена. Каждый из методов члена обращается к базе данных посредством LINQ-для-SQL. Каждый из методов был назван осмысленно исходя из его работы.

•    InsertMessage: Этот метод вставляет в базу данных одно сообщение за раз. Он вызывается, когда пользователь набирает сообщение в разделе чата и затем жмет кнопку «Отправить».

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

•    GetMessages: Этот метод получает сообщения для конкретного раздела с момента присоединения вошедшего пользователя к разделу. Он получает лишь еще не извлеченные сообщения; поэтому передается messageID последнего сообщения, извлеченного при предыдущем вызове этого метода. Выделенный ниже код timeUserJoined.AddSeconds(1) ограничивает сообщения теми, которые были получены за 1 секунду после момента присоединения вошедшего пользователя к разделу. Причина в том, что при присоединении пользователя к разделу в базу данных вставляется сообщение, гласящее "пользователь присоединился к разделу". Это сообщение увидят все остальные участники чата, исключая вошедшего пользователя.

Обратите внимание на цикл foreach. Появляется вопрос: почему нельзя вернуть обобщенный список типа List<Message>? Интерфейс не понимает сложные типы, не определенные явно как DataContract. Поэтому была создана почти точная копия членов таблицы «Сообщение» и явно определена как класс MessageContract, где каждый член DataContract является DataMember.

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

•    GetUsers: Этот метод получает всех пользователей в конкретном разделе. Сначала он проверяет, есть ли вошедший пользователь в таблице LoggedInUser; если нет – пользователь вставляется (строки 75-82). Эта проверка упрощает вставку нового пользователя; Когда пользователь впервые входит в раздел чата, он вставляется в таблицу LoggedInUser. Все последующие вызовы для получения пользователей извлекают всех пользователей из базы данных.

Как в методе GetMessages, все извлеченные пользователи присваиваются классу контракта данных UserContract (строки 92-97).

62     List<UserContract> ILinqChatService.GetUsers(int roomID, int userID)
63     {
64         LinqChatDataContext db = new LinqChatDataContext();
65
66         // Проверяется, существует ли этот аутентифицированный пользователь в
67         // таблице LoggedInUser (подразумевается, что пользователь вошел в этот раздел)
68         var user = (from u in db.LoggedInUsers
69                     where u.UserID == userID
70                     && u.RoomID == roomID
71                     select u).SingleOrDefault();
72
73         // если пользователь не существует в таблице LoggedInUser,
74         // он добавляется/вставляется в таблицу
75         if (user == null)
76         {
77             LoggedInUser loggedInUser = new LoggedInUser();
78             loggedInUser.UserID = userID;
79             loggedInUser.RoomID = roomID;
80             db.LoggedInUsers.InsertOnSubmit(loggedInUser);
81             db.SubmitChanges();
82         }
83
84         // получить всех вошедших в этот раздел пользователей
85         var loggedInUsers = from l in db.LoggedInUsers
86                             where l.RoomID == roomID
87                             orderby l.User.Username ascending
88                             select new { l.User.Username };
89
90         List<UserContract> userContracts = new List<UserContract>();
91
92         foreach (var loggedInUser in loggedInUsers)
93         {
94             UserContract userContract = new UserContract();
95             userContract.UserName = loggedInUser.Username;
96             userContracts.Add(userContract);
97         }
98
99         return userContracts;
100     }

•    UserExist: Проверяет, существует ли пользователь. Если пользователь существует –  возвращается идентификатор пользователя; если нет –  возвращается -1. Этот метод вызывается из файла отделенного кода Login.xaml, чтобы проверить, существуют ли в базе данных имя пользователя и пароль, введенные пользователем. Почему возвращается идентификатор пользователя, и почему только он? С момента входа пользователя приложение помнит идентификатор пользователя и имя пользователя этого пользователя, подобно эффекту сессии. Для минимизации извлечения данных в этот момент нужен только идентификатор пользователя, так как имя пользователя уже было предоставлено через текстовое поле имени пользователя со страницы входа.

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

•    LogOutUser: Разрегистрирует пользователя. Строки 124-130 удаляют пользователя из таблицы LoggedInUser. Строки 133-142 вставляют сообщение в таблицу «Сообщение», говорящее, что пользователь покинул раздел, чтобы другие пользователи видели это сообщение при выходе пользователя. Доступ к базе данных упрощен; вместо того, чтобы сделать дополнительный запрос на базе идентификатора пользователя для получения имени выходящего пользователя, метод ожидает имя пользователя, как выделено ниже. Поэтому запоминается имя пользователя текущего пользователя, вошедшего в этот раздел чата. Далее рассказано, как это делается. Метод LogOutUser вызывается при нажатии кнопки «Выйти» в пользовательском интерфейсе Chatroom.xaml.

Замечание: Можно разрегистрировать пользователя, когда он жмет кнопку закрытия браузера, перехватив событие onunload тега body.

119     void ILinqChatService.LogOutUser(int userID, int roomID, string username)
120     {
121         // Разрегистрировать пользователя, удалив его из таблицы LoggedInUser
122         LinqChatDataContext db = new LinqChatDataContext();
123
124         var loggedInUser = (from l in db.LoggedInUsers
125                             where l.UserID == userID
126                             && l.RoomID == roomID
127                             select l).SingleOrDefault();
128
129         db.LoggedInUsers.DeleteOnSubmit(loggedInUser);
130         db.SubmitChanges();
131
132         // вставить текст «пользователь покинул раздел»
133         Message message = new Message();
134         message.RoomID = roomID;
135         message.UserID = userID;
136         message.ToUserID = null;
137         message.Text = username + " left the room.";
138         message.Color = "Gray";
139         message.TimeStamp = DateTime.Now;
140
141         db.Messages.InsertOnSubmit(message);
142         db.SubmitChanges();
143     }

Web.config и WCF

При добавлении службы WCF к веб-приложению несколько строк кода автоматически было добавлено к файлу Web.config в теге system.ServiceModel. Хотя все тут стандартно, надо отметить информацию связывания в строке 126. По умолчанию она будет запрограммирована как "wsHttpBinding"; надо сменить ее на basicHttpBinding, как показано и выделено ниже:

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

Запоминание информации и переход от одного пользовательского управляющего элемента XAML к другому

Как сказано ранее, Silverlight – клиентская технология. Оттого нельзя использовать объекты сессии для запоминания чего-то и нельзя использовать известную команду Response.Redirect для перехода к следующей странице/пользовательскому управляющему элементу XAML. Хотя можно разместить один файл XAML в веб-форме ASP.NET, Silverlight не задуман так. Надо переключаться или переходить от одного файла XAML к другому, размещенному в одной и той же веб-форме ASP.NET или странице HTML. По умолчанию при создании приложения Silverlight генерируется файл Page.xaml, также являющийся стандартным пользовательским управляющим элементом XAML, используемым или размещаемым при запуске приложения Silverlight. Пользовательский управляющий элемент Page был удален в силу ненужности. Зато были добавлены два пользовательских управляющих элемента: Login.xaml и Chatroom.xaml. Будет совершаться перемещение туда-сюда от Login.xaml к Chatroom.xaml и наоборот с помощью пользовательского управляющего элемента App.xaml.

App.xaml - пользовательский управляющий элемент уровня приложения Silverlight. В нем устанавливается, какой пользовательский управляющий элемент будет показываться или вызываться первым в приложении. В нем запоминаются идентификатор пользователя и имя пользователя. Он работает как Web.config, где хранятся ресурсы уровня приложения.

1.    Установка Login.xaml в качестве стандартного пользовательского управляющего элемента XAML: В событии Application_Startup из App.xaml измените "Page()" на "Login()", как показано в строке 32.

28     private void Application_Startup(object sender, StartupEventArgs e)
29     {
30         // начать со страницы входа
31         this.RootVisual = rootGrid;
32         rootGrid.Children.Add(new Login());
33     }

2.    Метод RedirectTo, созданный в пользовательском управляющем элементе App.xaml, осуществляет переход от пользовательского управляющего элемента Login.xaml к Chatroom.xaml. Этот метод принимает пользовательский управляющий элемент, куда надо перенаправиться. Здесь удаляется текущий пользовательский управляющий элемент и добавляется пользовательский управляющий элемент, который надо показать пользователю. Этот метод вызывается из XAML входа, после того как пользователь войдет в раздел чата, и из XAML раздела чата при выходе пользователя. Код показан ниже:

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

Чтобы перенаправить пользователя из Login.xaml в Chatroom.xaml, из пользовательского управляющего элемента Login.xaml делается следующее:

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

3.    Чтобы помнить значение из одного пользовательского управляющего элемента XAML в другом, в пользовательском управляющем элементе App.xaml создается открытое свойство для каждого из значений, которое надо помнить.

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

После создания этих свойств им присваиваются значения, которые надо запомнить из Login.xaml или Chatroom.xaml. Ниже показан код из Login.xaml.

81     App app = (App)Application.Current;
82     app.UserID = userID;

Пользовательский интерфейс Chatroom.xaml

Надо отметить несколько вещей о пользовательском интерфейсе раздела чата, и все они выделены ниже. Сообщения и список пользователей используют управляющие элементы StackPanel, но привязки данных различаются. Список пользователей использует более простой тег DataTemplate, привязывающий все имена пользователей в виде гиперссылок. Сообщения привязываются в отделенном коде, потому что сообщения должны показывать более сложный набор управляющих элементов. Как видно на снимке раздела чата, сообщения разноцветные; имена и сообщения рядом с ними не должны быть одинаковых цветов. Подробнее это рассмотрено далее.

Задано событие KeyDown для текстового поля сообщения (куда пользователь вводит сообщения), так что можно проверить, нажал ли пользователь клавишу "ввод" клавиатуры. Когда пользователь нажимает клавишу «ввод», отправляется сообщение. Подробнее это рассмотрено далее.

<UserControl x:Class="Silverlight2Chat.Chatroom"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="600" Height="340">
    <Grid x:Name="LayoutRoot" Background="White"
              ShowGridLines="False" Loaded="LayoutRoot_Loaded">
        <Grid.RowDefinitions>
            <RowDefinition Height="10" />       <!-- отступ -->
            <RowDefinition Height="38" />       <!-- заголовок -->
            <RowDefinition Height="10" />       <!-- отступ -->
            <RowDefinition Height="*" />        <!-- сообщения, список пользователей -->
            <RowDefinition Height="10" />       <!-- отступ -->      
            <RowDefinition Height="26" />       <!— текстовое поле сообщения, кнопка отправки -->
            <RowDefinition Height="10" />       <!-- отступ -->
        </Grid.RowDefinitions>
 
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10" />     <!-- отступ -->
            <ColumnDefinition Width="*" />      <!-- сообщения, текстовое поле сообщения -->
            <ColumnDefinition Width="10" />     <!-- отступ -->
            <ColumnDefinition Width="120" />    <!-- список пользователей, кнопка отправки-->
            <ColumnDefinition Width="10" />     <!-- отступ -->
        </Grid.ColumnDefinitions>
 
        <TextBlock Text="Silverlight 2 Chat" Grid.Row="1"
          Grid.Column="1" FontSize="22" Foreground="Navy" />
 
        <StackPanel Orientation="Vertical" Grid.Row="1" Grid.Column="3">
            <TextBlock x:Name="TxtbLoggedInUser" FontSize="10"
               Foreground="Navy" FontWeight="Bold" HorizontalAlignment="Center" />
            <Button x:Name="BtnLogOut" Content="Log Out"
              FontSize="10" Click="BtnLogOut_Click" />
        </StackPanel>
 
        <ScrollViewer x:Name="SvwrMessages" Grid.Row="3" Grid.Column="1"
                      HorizontalScrollBarVisibility="Hidden"
                      VerticalScrollBarVisibility="Visible" BorderThickness="2">
            <StackPanel x:Name="SpnlMessages" Orientation="Vertical" />
        </ScrollViewer>

        <ScrollViewer x:Name="SvwrUserList" Grid.Row="3" Grid.Column="3"
                      HorizontalScrollBarVisibility="Auto"
                      VerticalScrollBarVisibility="Auto" BorderThickness="2">
            <StackPanel x:Name="SpnlUserList" Orientation="Vertical">
                <ItemsControl x:Name="ItmcUserList">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <HyperlinkButton Content="{Binding UserName}" />
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>  
            </StackPanel>
        </ScrollViewer>
 
        <StackPanel Orientation="Horizontal" Grid.Row="5" Grid.Column="1" >
            <TextBox x:Name="TxtMessage" TextWrapping="Wrap"
                 KeyDown="TxtMessage_KeyDown" 
                 ScrollViewer.VerticalScrollBarVisibility="Visible"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 Width="360"
                 BorderThickness="2" Margin="0,0,10,0"/> 
 
            <ComboBox x:Name="CbxFontColor" Width="80">
                <ComboBoxItem Content="Black" Foreground="White"
                   Background="Black" IsSelected="True" />
                <ComboBoxItem Content="Red" Foreground="White" Background="Red" />
                <ComboBoxItem Content="Blue" Foreground="White" Background="Blue" />
            </ComboBox>
        </StackPanel>
 
        <Button x:Name="BtnSend" Content="Send"
          Grid.Row="5" Grid.Column="3" Click="BtnSend_Click" />
    </Grid>
</UserControl>

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

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     }