Создание приложения веб-чата на Silverlight 2
ОГЛАВЛЕНИЕ
Silverlight 2 сейчас доступен всем, и лучший способ его изучить – создать маленькое веб-приложение с помощью этой чудесной технологии. Именно это и будет сделано здесь. Будет создан веб-чат с помощью Silverlight 2. Также будут рассказаны некоторые вещи, выясненные о данной новой технологии. Ниже показан снимок приложения веб-чата на Silverlight 2, которое будет создано.
Требования
Будет с нуля создано очень простое приложение веб-чата с помощью 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 }