Создание бизнес-приложений с помощью Silverlight
ОГЛАВЛЕНИЕ
Моя основная цель в данной статье заключается в том, чтобы взять бизнес-сценарий и построить приложение с нуля, иллюстрируя по дороге различные аспекты разработки Silverlight. Решение, о котором я буду говорить, – приложение центра обработки звонков, его логическая структура показана на рис. 1. В этой части я сконцентрируюсь на всплывающих на экране уведомлениях, асинхронной модели программирования, диалоговых оках Silverlight и реализации сервера междоменной политики TCP. В части 2 я поговорю о безопасности приложений, интеграции веб-служб, секционировании приложения и ряде других аспектов приложения.
Рис. 1. Логическая архитектура центра обработки звонков Silverlight
Основы Silverlight: CoreCLR
Прежде чем я приступлю, давайте обновим в памяти основы Silverlight. Сперва я загляну внутрь среды выполнения Silverlight, чтобы читателям было понятнее, что возможно с помощью Silverlight. CoreCLR – это виртуальный компьютер, используемый Silverlight. Он подобен CLR, на основе которой работают .NET Framework 2.0 и последующие версии, содержа похожие системы загрузки типов и сбора мусора.
CoreCLR имеет очень простую модель управления доступом для кода (CAS) – более простую чем в настольном CLR, поскольку Silverlight необходимо обеспечивать выполнение политик безопасности лишь на уровне приложения. Это обусловлено тем, что, как независимый от платформ веб-клиент, он не может полагаться на существование любых определенных политик предприятия или компьютера и не может позволять пользователю менять существующие политики. Есть несколько исключений, таких как OpenFileDialog и IsolatedStorage (изменение квоты хранения), где Silverlight необходимо прямое согласие пользователя на нарушение набора правил «песочницы» по умолчанию. OpenFileDialog используется для доступа к файловой системе, тогда как IsolatedStorage используется для доступа к изолированному от одноименных хранилищу и увеличения квоты хранения.
Для настольных приложений каждое исполняемое загружает ровно одну копию CLR и процесс ОС будет содержать лишь одно приложение. У каждого приложения имеется системный домен, общий домен, домен по умолчанию и ряд прямо созданных AppDomain (см. «JIT и гони: углубитесь во внутренние механизма .NET Framework, чтобы увидеть, как CLR создает объекты среды выполнения»). Похожая модель домена имеется в CoreCLR. В случае Silverlight несколько приложений, возможно, из разных доменов, будут выполнять один и тот же процесс ОС.
В Internet Explorer 8.0 каждая вкладка работает в собственном изолированном процессе; таким образом, все приложения Silverlight, размещенные внутри одной вкладки, будут работать в контексте экземпляра CoreCLR, как проиллюстрировано на рис. 2. Поскольку каждое приложение может происходить из различного домена, по соображениям безопасности, каждое приложение будет загружено в собственный AppDomain. Будет существовать столько же экземпляров CoreCLR, сколько существует вкладок, размещающих, на данный момент, приложения Silverlight.
Каждый AppDomain получит собственный пул статических переменных, как в настольной CLR. Каждый пул конкретного домена будет инициализирован в ходе процесса начальной загрузки AppDomain.
Рис. 2. Каждое приложение Silverlight запустит собственный AppDomain
Приложения Silverlight не могут создавать свои собственные, индивидуализированные домены приложений; эта способность зарезервирована для внутреннего использования. Более подробное описание CoreCLR можно найти в следующих статьях рубрики «CLR вдоль и поперек», написанных группой разработчиков CLR: "Программирование для Silverlight с помощью CoreCLR" и "Безопасность в Silverlight 2."
Среда выполнения Silverlight
Silverlight разработан для широкого диапазона приложений, требующих различных уровней инфраструктуры и библиотек поддержки. Простое приложение может, например, просто проигрывать аудиофайлы в несколько байт, чтобы помочь в понимании произношения слов на веб-сайте словаря или отобразить рекламное объявление. С другой стороны, бизнес-приложения корпоративного класса могут требовать безопасности, конфиденциальности данных, управления состоянием, интеграции с другими приложениями и службами и поддержки инструментария, перечисляя лишь несколько пунктов. В то же время приложениям Silverlight нужна меньшая по размеру среда выполнения, чтобы развертывание через Интернет не было бы проблемой при медленных подключениях.
Эти требования кажутся взаимопротиворечащими, но разработчики Silverlight справились с этим, разбив платформу на разделы, как показывает многослойное представление с рис. 2. Среда выполнения CoreCLR + Silverlight именуется «надстройкой», которую все пользователи загрузят, прежде чем они смогут запускать приложения. Этой надстройки достаточно для большинства ориентированных на пользователя приложений. Если приложение требует использования библиотеки SDK (интеграции WCF или сред выполнения DLR, таких как Iron Ruby) или специально созданной библиотеки, приложение должно упаковать эти компоненты в пакет XAP, чтобы Silverlight знал, как разрешать необходимые типы во время выполнения(см. рубрику «На переднем крае» в этом номере для получения дополнительных сведений о пакетах XAP).
Среда выполнения Silverlight имеет размер примерно 4МБ и, в дополнение к библиотекам CoreCLR, таким как agcore.dll и coreclr.dll, содержит необходимые библиотеки, требующиеся разработчикам приложений. К ним относятся следующие фундаментальные библиотеки: mscorlib.dll, System.dll, System.Net.dll, System.Xml.dll и System.Runtime.Serialization.dll. Среда выполнения, поддерживающая надстройку обозревателя, обычно устанавливается в каталоге C:\Program Files\Microsoft Silverlight\2.0.30930.0\. Это каталог, который создается когда компьютер загружает и устанавливает Silverlight в качестве части сеанса просмотра веб-страниц.
Разработчики, создающие и тестирующие приложения на одном и том же компьютере, будут иметь две копии среды выполнения: одну копию, установленную надстройкой и другую, через установку SDK. Последнюю можно найти в каталоге C:\Program Files\Microsoft SDKs\Silverlight\v2.0\Reference Assemblies. Эта копия будет использована шаблонами Visual Studio в качестве части списка ссылок времени компиляции.
Песочница предотвращает взаимодействие приложения Silverlight с основной частью локальных ресурсов, что верно для любого типичного веб-приложения. По умолчанию приложение Silverlight не может получить доступ к файловой системе (кроме изолированных хранилищ), создавать подключения через сокет, взаимодействовать с устройствами, присоединенными к компьютеру или устанавливать компоненты программного обеспечения. Это определенно налагает некоторые ограничения на типы приложений, которые можно конструировать на платформе Silverlight. Однако Silverlight имеет все необходимые ингредиенты для разработки основанных на данных бизнес-приложений корпоративного класса, которым необходимо интегрироваться с фоновыми бизнес-процессами и службами.
Сценарий приложения
Бизнес-приложение, которое я буду создавать здесь, демонстрирует архитектуру управления вызовами от стороннего производителя, где централизованный сервер подключается к УАТС для центрального управления телефонами. Поскольку моя цель состоит в сосредоточении внимания на Silverlight как на поверхности интерфейса пользователя, я не буду тратить особо времени на интеграцию телефонии. Вместо этого я использую простой симулятор вызовов для создания входящего события вызова. Симулятор сбросит пакет данных, представляющий вызов, в очередь ожидания диспетчера звонков, что инициирует центральный для данного проекта процесс.
Мой вымышленный сценарий требует от приложения центра обработки звонков работать внутри веб-обозревателя платформо-независимым образом, в то же время предоставляя насыщенное взаимодействие с пользователем в качестве настольного приложения. Silverlight является естественным выбором, поскольку ActiveX не очень популярен в клиентских средах, помимо Windows.
Давайте взглянем на архитектурные аспекты приложения. Здесь надо будет реализовать извещающие уведомления, интеграцию событий, интеграцию бизнес-служь, кэширование, безопасность и интеграцию с «облачными» службами.
Извещающие уведомления Они необходимы, поскольку системе нужно зафиксировать событие входящего вызова и передать данные интерактивного речевого ответа (IVR), которые были введены вызывающим, чтобы создать всплывающее уведомление на экране или заполнить экран интерфейса пользователя информацией о входящем вызове. Кроме того, пользователю должна быть дана возможность отвергнуть или принять вызов.
Потоковая передача событий В типичном веб-приложении, веб-сервер располагает всей информацией о бизнес-событиях, поскольку он выполняет основную часть бизнес-процессов. Но в случае функционально насыщенного интернет-приложения (RIA) реализация бизнес-процесса будет совместно использоваться и приложением внутри веб-обозревателя и серверов, применяющим веб-службы бизнеса. Это значит, что бизнес-события, а также события технологии, созданные внутри приложения Silverlight, необходимо отправлять серверу через набор специальных веб-служб.
Примерами бизнес-событий в случае этого решения являются отклонение пользователем (rep – представитель) вызова ("rep rejected the call") или его принятие ("rep accepted the call"). Типичными событиями технологии являются "Connection to Call Manager TCP server failed" («Сбой при подключении к ТСР-серверу диспетчера звонков») и "Web service exception" («Исключение веб-службы»).
Интеграция бизнес служб Решению центра обработки звонков, подобно любому бизнес-приложению, необходима интеграция с данными, которые могут храниться в реляционной базе данных. В качестве способа интеграции я буду использовать веб-службы.
Кэширование Для лучшего обслуживания пользователей я локально кэширую информацию в памяти, а также на диске. Кэшированная информация может включать в себя файлы XML, указывающие сценарии напоминаний пользователя и прочие справочные данные, которые не могут меняться часто.
Приложение безопасности Безопасность – фундаментальное требование в приложении такого рода. Безопасность включает проверку подлинности, авторизацию, конфиденциальность передаваемых и хранящихся данных, а также усечение данных на основе профилей пользователей.
Интеграция с «облачными» службами Интеграция с «облачными» службами в основании, такими как служба хранения, требует специальной инфраструктуры на стороне сервера. Это обусловлено необходимостью обеспечить близкое наблюдение за «облачными» службами и их регулировку, в плане ответственности и уровней обслуживания.
Интеграцию с бизнес-службами, безопасность приложений, междоменные политики для веб-служб и разбиение приложений я охвачу в части 2 данной статьи.
Извещающие уведомления с помощью сервера сокета
Всплывающие на экране уведомления относятся к числу фундаментальных требований к приложению центра обработки звонков, для передачи контекста звонков с инфраструктуры телефонии на экран агента. Переданный контекст звонка может включать в себя любую информацию, переданную словесно (для систем IVR) или путем набора клавиш клиентом на телефоне.
Уведомление может быть отправлено приложению Silverlight внутри обозревателя одним из двух способов: через опрос клиентов или принудительную отправку с сервера. Первый вариант достаточно легко реализовать, но для случаев центра обработки звонков, где синхронизация состояния между событиями телефонии и клиентским приложением должна быть точной, он может быть не оптимален. По этой причине я и буду использовать второй вариант, отправляя извещающие уведомления с помощью сокетов Silverlight.
Одной из важных возможностей Silverlight является связь с сокетами TCP. По соображениям безопасности Silverlight позволяет подключение лишь к портам серверов в диапазоне 4502-4532. Это одна из многих политик безопасности, примененных в «песочнице». Другая важная политика «песочницы» состоит в том, что Silverlight не может быть слушателем и, в силу этого, не может принимать входящие подключения через сокеты. Поэтому я и создам сервер сокетов, слушающие на порте 4530 и поддерживающий пул подключений, где каждое подключение представляет активного представителя центра обработки звонков.
Среда выполнения сокетов Silverlight также обеспечивает выполнение междоменных политик явного соглашения на сервере для всех подключений через сокет. Когда код приложения Silverlight пытается открыть подключение к конечной точке IP или на допустимом номере порта, непрозрачном для кода пользователя, среда выполнения создаст подключение к конечной точке IР на этом же IP-адресе с номером порта 943. Этот номер порта жестко закодирован в реализацию Silverlight и не может быть настроен приложениями, либо изменен разработчиком.
Рис. 1 показывает место в архитектуре сервера политик. При вызове Socket.ConnectAsync последовательность потока сообщений подобна показанной на рис. 3. Сообщения 2, 3 и 4 разработаны так, чтобы быть полностью непрозрачными для пользовательского кода.
Рис. 3. Среда выполнения Silverlight автоматически запрашивает междоменную политику для подключений через сокет
Мне необходимо реализовать сервер политики на том же IP-адресе, что и сервер диспетчера звонков. Я могу реализовать оба сервера в одном процессе ОС; но для простоты реализую их в двух отдельных программах консоли. Эти программы консоли можно легко преобразовать в службы Windows и приспособить к работе с кластерами, для обеспечения автоматических переходов на другой ресурс при сбоях, чтобы предоставить надежность и доступность.
Асинхронные циклы ввода/вывода
.NET Framework 3.5 представила новое асхинхронное программирование API-сокеты; это методы, заканчивающиеся на Async(). На данном сервере я буду использовать методы Socket.AcceptAsync, Socket.SendAsync и Socket.ReceiveAsync. Методы Async оптимизированы для серверных приложений с высокой пропускной способностью, путем использования портов завершения ввода/вывода, а также эффективного управления буфером отправки и получения через пригодный к многократному использованию класс SocketAsyncEventArgs.
Поскольку Silverlight не позволено создавать слушателей TCP, его класс Socket поддерживает лишь ConnectAsync, SendAsync и ReceiveAsync. Silverlight поддерживает лишь асинхронную модель программирования, и это касается не только API сокетов, но и любого сетевого взаимодействия.
Поскольку я буду использовать асинхронную модель программирования как на сервере, так и на клиенте, давайте познакомимся поближе с шаблонами разработки. Одним из повторяющихся шаблонов разработки является цикл ввода/вывода, применимый ко всем асинхронным операциям. Сперва стоит взглянуть на типичное синхронное исполнение цикла допуска сокета:
_listener.Bind(localEndPoint);
_listener.Listen(50);
while (true)
{
Socket acceptedSocket = _listener.Accept();
RepConnection repCon = new
RepConnection(acceptedSocket);
Thread receiveThread = new Thread(ReceiveLoop);
receiveThread.Start(repCon);
}
Синхронный допуск интуитивен, его просто программировать и обслуживать, но эта реализация на деле не масштабируется для серверов, по причине наличия выделенных потоков для каждого подключения клиента. Она легко может достигнуть своего пика на уровне в несколько подключений, если эти подключения очень активны.
Чтобы Silverlight могла хорошо работать со средой выполнения обозревателя, она должен занимать как можно меньше ресурсов. Все вызовы в псевдокоде «допуска сокета», показанном выше, блокируют потоки на которых исполняются, тем самым негативно влияя на масштабируемость. По этой причине Silverlight налагает очень серьезные ограничения на блокирования вызовов и на деле допускает только асинхронное взаимодействие с сетевыми ресурсами. Асинхронные циклы требует корректировки мысленной модели, чтобы представить себе невидимое окно сообщений, в котором должно находиться минимум одно сообщение, чтобы цикл сработал.
Рис. 4 показывает цикл receive (получения) – более завершенная его реализация содержится в прилагающемся к статье загружаемом коде. Здесь нет программных конструкций бесконечного цикла, подобных циклу while (true), который можно было увидеть в синхронном псевдокоде допуска сокета выше. Для разработчика Silverlight важно привыкнуть к программированию такого рода. Чтобы цикл получения продолжал получать данные после того, как сообщение было получено и обработано, в очереди должен быть минимум один запрос к порту завершения ввода/вывода, связанному с подключенным сокетом. Типичный асинхронный цикл проиллюстрирован на рис. 5 и применим к ConnectAsync, ReceiveAsync и SendAsync. AcceptAsync можно добавить к этому списку на сервере, где будет использоваться .NET Framework 3.5.
Рис. 4. Асинхронные циклы отправки/получения с помощью сокетов Silverlight
public class CallNetworkClient
{
private Socket _socket;
private ReceiveBuffer _receiveBuffer;
public event EventHandler<EventArgs> OnConnectError;
public event EventHandler<ReceiveArgs> OnReceive;
public SocketAsyncEventArgs _receiveArgs;
public SocketAsyncEventArgs _sendArgs;
//removed for space
public void ReceiveAsync()
{
ReceiveAsync(_receiveArgs);
}
private void ReceiveAsync(SocketAsyncEventArgs recvArgs)
{
if (!_socket.ReceiveAsync(recvArgs))
{
ReceiveCallback(_socket, recvArgs);
}
}
void ReceiveCallback(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError != SocketError.Success)
{
return;
}
_receiveBuffer.Offset += e.BytesTransferred;
if (_receiveBuffer.IsMessagePresent())
{
if (OnReceive != null)
{
NetworkMessage msg =
NetworkMessage.Deserialize(_receiveBuffer.Buffer);
_receiveBuffer.AdjustBuffer();
OnReceive(this, new ReceiveArgs(msg));
}
}
else
{
//adjust the buffer pointer
e.SetBuffer(_receiveBuffer.Offset, _receiveBuffer.Remaining);
}
//queue an async read request
ReceiveAsync(_receiveSocketArgs);
}
public void SendAsync(NetworkMessage msg) { ... }
private void SendAsync(SocketAsyncEventArgs sendSocketArgs)
{
...
}
void SendCallback(object sender, SocketAsyncEventArgs e)
{
...
}
}
Рис. 5. Шаблон асинхронного цикла сокета
В Реализации цикла получения, показанной на рис. 4, ReceiveAsync – это оболочка для повторно входящего метода ReceiveAsync(SockeAsyncEventArgs recvArgs), который поставит в очередь запрос на порте завершения ввода/вывода сокета. SocketAsyncEventArgs, представленный в .NET Framework 3.5 играет похожую роль в реализации сокета Silverlight и может быть повторно использован в ходе нескольких запросов, избегая смешивания с собираемым мусором. За извлечение сообщения, инициацию события обработки сообщения и установки в очередь следующего элемента для продолжения цикла будет отвечать процедура обратного вызова.
Чтобы обрабатывать случаи частичного получения сообщений, ReceiveCallback корректирует буфер перед установкой в очередь следующего вопроса. NetworkMessage заключается в экземпляр ReceiveArgs и передается внешнему обработчику событий для обработки полученного сообщения.
Буфер сбрасывается для каждого завершенного получения NetworkMessage, после копирования частичного сообщения, если такое есть, в начало буфера. Похожая схема используется на сервере, но практические реализации могут выиграть от кольцевых буферов.
Для реализации сценария «принятия вызова», необходимо создать расширяемую архитектуру сообщения, позволяющую сериализовывать и десериализовывать сообщения с произвольным содержанием, не требуя переписки логики сериализации для каждого нового сообщения.
Рис. 6. Компоновка сериализованных типов NetworkMessage
Архитектура сообщения довольно проста: каждый дочерний объект NetworkMessage заявляет свою подпись в момент создания экземпляра с помощью соответствующего MessageAction. Реализации NetworkMessage.Serialize и Deserialize будут работать на Silverlight и .NET Framework 3.5 (на сервере), благодаря совместимости на уровне исходного кода. Сериализованное сообщение будет иметь компоновку, показанную на рис. 6.
Вместо вставки длины в начале сообщения можно использовать маркеры "begin" и "end" («начало» и «конец») с соответствующими escape-последовательностями. Закодировать длину в сообщение гораздо проще для обработки буферов.
Первые четыре байта каждого сериализованного сообщения будут включать число байтов в сериализованном объекте, следующем за этими 4 байтами. Silverlight поддерживает XmlSerializer, расположенный внутри System.Xml.dll, являющегося частью Silverlight SDK. Код сериализации прилагается к статье в загружаемом файле. Можно заметить, что у него нет прямых зависимостей от дочерних классов, таких как RegisterMessage или других сообщений, включая UnregisterMessage и AcceptMessage. Серия пометок XmlInclude поможет сериализатору адекватно разрешать типы.NET при сериализации дочерних классов.
Использование NetworkMessage.Serlialize и Deserialize показано в ReceiveCallback и SendAsync на рис. 4. В цикле получения собственно обработка сообщения выполняется обработчиком событий, присоединенным к событию NetworkClient.OnReceive. Я мог бы обработать сообщение внутри CallNetworkConnection, но написание обработчика получения для обработки сообщения поможет расширяемости путем отделения обработчика от CallNetworkConnection во время разработки.
Рис. 7 показывает приложение Silverlight, RootVisual, запускающее CallNetworkClient (показанный на рис. 4). Все элементы управления Silverlight присоединены к одному потоку интерфейса пользователя и обновления интерфейса пользователя могут проводится только когда код исполняется в контексте этого потока интерфейса пользователя. Асинхронная программная модель Silverlight исполняет код доступа к сети и действующие обработчики на рабочих потоках пула потоков. Все классы, производные от FrameworkElement (такие как Control («Элемент управления»), Border («Граница»), Panel («Панель») и большая часть элементов интерфейса пользователя наследуют свойство Dispatcher (от DispatcherObject), которое может исполнять код на потоке интерфейса пользователя.
На рис. 7 случай MessageAction.RegisterResponse обновит интерфейс пользователя подробностями смены центра обработки звонков через анонимный делегат. Обновленный интерфейс пользователя, к которому привело исполнение делегата, показан на рис. 8.
Рис. 7. Пользовательский элемент управления Silverlight UserControl, обрабатывающие входящие сообщения
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
ClientGlobals.socketClient = new CallNetworkClient();
ClientGlobals.socketClient.OnReceive += new
EventHandler<ReceiveArgs>(ReceiveCallback);
ClientGlobals.socketClient.Connect(4530);
//code omitted for brevity
}
void ReceiveCallback(object sender, ReceiveArgs e)
{
NetworkMessage msg = e.Result;
ProcessMessage(msg);
}
void ProcessMessage(NetworkMessage msg)
{
switch(msg.GetMessageType())
{
case MessageAction.RegisterResponse:
RegisterResponse respMsg = msg as RegisterResponse;
//the if is unncessary as the code always executes in the
//background thread
this.Dispatcher.BeginInvoke(
delegate()
{
ClientGlobals.networkPopup.CloseDialog();
this.registrationView.Visibility = Visibility.Collapsed;
this.callView.Visibility = Visibility.Visible;
this.borderWaitView.Visibility = Visibility.Visible;
this.tbRepDisplayName.Text = this.txRepName.Text;
this.tbRepDisplayNumber.Text = respMsg.RepNumber;
this.tbCallServerName.Text =
respMsg.CallManagerServerName;
this.tbCallStartTime.Text =
respMsg.RegistrationTimestamp.ToString();
});
break;
case MessageAction.Call:
CallMessage callMsg = msg as CallMessage;
//Code omitted for brevity
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(
delegate()
{
ClientGlobals.notifyCallPopup.ShowDialog(true);
});
}
break;
//
//Code omitted for brevity
//
default:
break;
}
}
}
Рис. 8. Регистрация на сервере центра обработки звонков находится в процессе
Рис. 9. Регистрация на сервере центра обработки звонков находится в процессе
Модальные диалоговые окна в Silverlight
Когда представитель центра обработки звонков входит в систему, он получит запрос начать смену, зарегистрировавшись на сервере центра обработки звонков. Процесс регистрации на сервере сохранит сеанс, проиндексированный номером представителя. Этот сеанс будет использован для последующих всплывающих на экране объявлений и других уведомлений. Переход экрана приложения центра вызовов для процесса регистрации показан на рис. 8 и 9. Я буду использовать модельное диалоговое окно, показывающее ход сетевой отправки. Типичные корпоративные бизнес-приложения достаточно свободно используют всплывающие диалоговые окна, модальные и не модальные. Поскольку встроенного элемента диалогового окна DialogBox в Silverlight SDK нет, я покажу, как разработать его Silverlight для использования данном приложении.
До Silverlight не существовало простых способов создания модальных диалогов, поскольку не было простых способов предотвращения передачи событий клавиатуры в интерфейс пользователя. Взаимодействие с мышью можно отключить непрямо, введя UserControl.IsTestVisible = false. Начиная с RC0, параметр Control.IsEnabled = false предотвращает получение любых событий мыши или клавиатуры элементами управления интерфейса пользователя. Я буду использовать System.Windows.Controls.Primitives.Popup для отображения диалогового интерфейса пользователя поверх существующего элемента управления.
Рис. 10 показывает базовый элемент управления SLDialogBox с абстрактными методами GetControlTree, WireHandlers и WireUI. Эти методы будут переопределены дочерними классами, как показано на рис. 11. Primitives.Popup требует экземпляра элемента управления, не являющегося частью дерева управленияЭ, к которому будет присоединен Popup. В коде на рис. 10 метод ShowDialog(true) рекурсивно отключит все дерево управления, так что ни один из содержащихся в нем элементов управления не будет получать событий мыши или клавиатуры. Поскольку мой всплывающий диалог должен быть интерактивным, Popup.Child следует устанавливать из нового экземпляра элемента управления. Реализация GetControlTree в дочерних классах будет действовать как фабрика элементов управления и предоставлять новый экземпляр пользовательского элемента управления, соответствующего требованиям диалога к интерфейсу пользователя.
Рис. 10. Элемент всплывающего диалогового окна DialogBox в Silverlight
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace SilverlightPopups
{
public abstract class SLDialogBox
{
protected Popup _popup = new Popup();
Control _parent = null;
protected string _ caption = string.Empty;
public abstract UIElement GetControlTree();
public abstract void WireHandlers();
public abstract void WireUI();
public SLDialogBox(Control parent, string caption)
{
_parent = parent;
_ caption = caption;
_popup.Child = GetControlTree();
WireUI();
WireHandlers();
AdjustPostion();
}
public void ShowDialog(bool isModal)
{
if (_popup.IsOpen)
return;
_popup.IsOpen = true;
((UserControl)_parent).IsEnabled = false;
}
public void CloseDialog()
{
if (!_popup.IsOpen)
return;
_popup.IsOpen = false;
((UserControl)_parent).IsEnabled = true;
}
private void AdjustPostion()
{
UserControl parentUC = _parent as UserControl;
if (parentUC == null) return;
FrameworkElement popupElement = _popup.Child as FrameworkElement;
if (popupElement == null) return;
Double left = (parentUC.Width - popupElement.Width) / 2;
Double top = (parentUC.Height - popupElement.Height) / 2;
_popup.Margin = new Thickness(left, top, left, top);
}
}
}
Рис. 11. NotifyCallPopup.xaml Skin
//XAML Skin for the pop up
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="200" Height="95">
<Grid x:Name="gridNetworkProgress" Background="White">
<Border BorderThickness="5" BorderBrush="Black">
<StackPanel Background="LightGray">
<StackPanel>
<TextBlock x:Name="tbCaption" HorizontalAlignment="Center"
Margin="5" Text="<Empty Message>" />
<ProgressBar x:Name="progNetwork" Margin="5" Height="15"
IsIndeterminate="True"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<Button x:Name="btAccept" Margin="10,10,10,10"
Content="Accept" HorizontalAlignment="Center"/>
<Button x:Name="btReject" Margin="10,10,10,10"
Content="Reject" HorizontalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</UserControl>
GetControlTree может быть применен для создания экземпляра пользовательского элемента управления (UserControl) Silverlight, который компилируется в пакет приложения, либо элемент управления может быть создан из файла XAML с помощью XamlReader.LoadControl. Как правило, диалоговые окна можно легко применить в обложках, к которым можно присоединять скомпилированные обработчики во время выполнения. Рис. 11 показывает обложку XAML с кнопками btAccept и btReject. Метод LoadControl выдаст исключение в случае оставления атрибута класса (<userControl class="AdvCallCenter.NotifyCallPopup"…>…</UserControl>) в XAML после завершения задачи разработки в Microsoft Expression Studio или Visual Studio. Все атрибуты обработчика событий интерфейса пользователя должны быть удалены для успешного анализа с помощью LoadControl.
Для создания обложек можно добавить элемент управления Silverlight к проекту, разработать его в Expression и удалить атрибут «класс» и имена обработчика событий, если они есть, присоединенные к элементам управления из файла XAML. Обработчики щелчка могут быть частью дочернего всплывающего класса, как показано на рис. 12, или, как вариант, можно создать отдельную библиотеку обработчиков и соединить ее с элементами управления, используя отражение.
Рис. 12. Реализация NotifyCallPopup
public class NotifyCallPopup : SLDialogBox
{
public event EventHandler<EventArgs> OnAccept;
public event EventHandler<EventArgs> OnReject;
public NotifyCallPopup(Control parent, string msg)
: base(parent, msg)
{
}
public override UIElement GetControlTree()
{
Return SLPackageUtility.GetUIElementFromXaml("NotifyCallPopup.txt");
}
public override void WireUI()
{
FrameworkElement fe = (FrameworkElement)_popup.Child;
TextBlock btCaption = fe.FindName("tbCaption") as TextBlock;
if (btCaption != null)
btCaption.Text = _caption;
}
public override void WireHandlers()
{
FrameworkElement fe = (FrameworkElement)_popup.Child;
Button btAccept = (Button)fe.FindName("btAccept");
btAccept.Click += new RoutedEventHandler(btAccept_Click);
Button btReject = (Button)fe.FindName("btReject");
btReject.Click += new RoutedEventHandler(btReject_Click);
}
void btAccept_Click(object sender, RoutedEventArgs e)
{
CloseDialog();
if (OnAccept != null)
OnAccept(this, null);
}
void btReject_Click(object sender, RoutedEventArgs e)
{
CloseDialog();
if (OnReject != null)
OnReject(this, null);
}
}
Обработчики могут находиться в любом проекте библиотеки Silverlight, поскольку они будут автоматически скомпилированы в пакет XAP как результат зависимости проекта. Чтобы файлы обложки вошли в пакет XAP, добавьте их к проекту Silverlight как файлы XML и измените расширение на XAML. Действием сборки по умолчанию для файлов с расширением XAML будет компиляция их в DLL приложения. Поскольку я хочу, чтобы эти файлы были упакованы как текстовые файлы, необходимо установить следующие атрибуты из окна свойств:
- BuildAction = "Content"
- Copy to Output Directory = "Do Not Copy"
- Custom Tool = <clear any existing value>
Анализатор XAML (XamlReader.Load) не интересуется расширением; однако использование расширения XAML будет более интуитивно и лучше представлять содержание. SLDialogBox отвечает лишь за показ и закрытие диалога. Дочерние реализации будут модифицированы, чтобы соответствовать нуждам приложения.
Реализация извещающих уведомлений
Приложение центра обработки звонков должно иметь возможность создать всплывающее сообщение с информацией о вызывающих. Рабочий день центра обработки звонков начинается с регистрации представителя с помощью сервера центра обработки звонков. Извещающие уведомления реализуются с помощью ориентированных на подключения сокетов. Полная реализация сервера диспетчера звонков не показана на рисунках, но входит в прилагающийся к статье загружаемый код. Когда клиент Silverlight выполняет подключение через сокет на сервере, новый объект RepConnection добавляется к RepList. RepList – это общий список, проиндексированный уникальным номером представителя. При приходе вызова этот список используется для обнаружения доступного представителя и, посредством подключения через сокет, связанного с RepConnection, отправки информации о вызове в виде уведомления для агента. RepConnection использует ReceiveBuffer, как показано на рис. 13.
Рис. 13. RepConnection использует ReceiveBuffer
class SocketBuffer
{
public const int BUFFERSIZE = 5120;
protected byte[] _buffer = new byte[BUFFERSIZE]
protected int _offset = 0;
public byte[] Buffer
{
get { return _buffer; }
set { _buffer = value; }
}
//offset will always indicate the length of the buffer that is filled
public int Offset
{
get {return _offset ;}
set { _offset = value; }
}
public int Remaining
{
get { return _buffer.Length - _offset; }
}
}
class ReceiveBuffer : SocketBuffer
{
//removes a serialized message from the buffer, copies the partial message
//to the beginning and adjusts the offset
public void AdjustBuffer()
{
int messageSize = BitConverter.ToInt32(_buffer, 0);
int lengthToCopy = _offset - NetworkMessage.LENGTH_BYTES - messageSize;
Array.Copy(_buffer, _offset, _buffer, 0, lengthToCopy);
offset = lengthToCopy;
}
//this method checks if a complete message is received
public bool IsMessageReceived()
{
if (_offset < 4)
return false;
int sizeToRecieve = BitConverter.ToInt32(_buffer, 0);
//check if we have a complete NetworkMessage
if((_offset - 4) < sizeToRecieve)
return false; //we have not received the complete message yet
//we received the complete message and may be more
return true;
}
}
Симулятор Silverlight будет здесь использоваться для сброса вызова в CallDispatcher._callQueue и инициации процесса всплывания окна. CallDispatcher не показан ни на одном из рисунков, но доступен в прилагающемся к статье коде. Он присоединяет обработчик к _callQueue.OnCallReceived и получает уведомление, когда симулятор ставит в очередь сообщение к _callQueue внутри реализации ProcessMessage. Пользуясь всплывающими диалогами, о которых я говорил ранее, клиент отобразит уведомление о принятии или отклонении вызова, как показано на рис. 14. Вот строка кода, отвечающая за отображение собственно диалога уведомления с рис. 8:
ClientGlobals.notifyCallPopup.ShowDialog(true);
Рис. 14. Уведомление о входящем вызове
Междоменный доступ служб TCP
В отличие от приложений мультимедиа и рекламных объявлений, настоящие бизнес-приложения корпоративного класса требуют интеграции с широким набором сред размещения служб. Например, приложение центра обработки звонков на веб-сайте (advcallclientweb hosted at localhost:1041) использует сервер сокетов с сохранением состояния в другом домене (localhost:4230) для создания всплывающих окон и дотягивается до бизнес-данных через службы, размещенные в другом домене (localhost:1043). Оно будет использовать еще один домен для передачи инструментальных данных.
Песочница Silverlight по умолчанию не позволяет сетевого доступа к любому домену, кроме того, откуда она происходит – advcallclientweb (localhost:1041). Когда обнаруживается такой сетевой доступ, среда выполнения Silverlight проверяет наличие политик явного соглашения, установленных доменом назначения. Вот типичный список сценариев размещения служб, который должен поддерживать запросы междоменной политики клиентом:
- Службы, размещенные в «облаке»
- Веб-службы, размещенные в процессе службы
- Веб-службы, размещенные в IIS или других веб-серверах
- Ресурсы HTTP, такие как разметка XAML и пакеты XAP
- Службы TCP, размещенные в процессе службы
Тогда как реализация междоменных политик для ресурсов HTTP и конечных точек веб-служб, размещенных в IIS, прямолинейна, прочие случаи требуют знания семантики запроса/ответа политики. В этом отделе я вкратце реализую инфраструктуру политики, необходимую для сервера всплывающих окон TCP, именуемого диспетчером звонков на рис. 1. О прочих междоменных сценариях будет рассказано во второй части этой статьи.
Междоменные политики с помощью служб TCP
Любой доступ службы TCP в Silverlight считается междоменным запросом, и серверу необходимо применить TCP-слушатель на том же IP-адресе, который привязан к порту 943. Сервер политик, показанный на рис. 3, является слушателем, примененным для данной цели. Этот сервер реализует процесс запроса/ответа для потоковой выдачи декларативных политик, которые необходимы среде выполнения Silverlight, перед тем как позволить сетевому стеку на клиенте подключиться к серверу всплывающих окон (диспетчеру звонков на рис. 3).
Для простоты я размещу сервер диспетчера звонков в приложении консоли. Это приложение консоли можно легко преобразовать в службу Windows для реальных реализаций. Рис. 3 показывает типичное взаимодействие с сервером политики; среда выполнения Silverlight может подключиться к серверу на порте 943 и отправить запрос политики, который будет содержать единственную строку текста: "<policy-file-request/>".
Политики на основе XML делают возможным сценарий, показанный на рис. 3. Раздел ресурсов сокета может указать группу портов внутри допустимого диапазона от 4502 до 4534. Причина ограничения их этим диапазоном заключается в минимизации направления, открытого для атаки, что снижает риск случайных слабостей в конфигурации брандмауэра. Поскольку сервер центра обработки звонков (диспетчер звонков с рис. 1) слушает на порте под номером 4530, ресурс сокета настроен следующим образом:
<access-policy>
<policy>
<allow-from> list of URIs</allow-from>
<grant-to> <socket-resource port="4530" protocol="tcp"/></grant-to>
</policy>
</access-policy>
Также можно настроить <socket-resource> на допуск всех дозволенных номеров портов, указав port="4502–4534".
Чтобы сэкономить время, я переделаю код из сервера диспетчера звонков под новую задачу при реализации сервера политики. Клиент Silverlight подключается к серверу политики, отправляет запрос и читает ответ. Сервер политики закрывает подключение после того, как ответ политики успешно отослан. Содержимое политики читается сервером политики из локального файла, clientaccesspolicy.xml, который входит в загружаемые приложения к статье.
Реализация слушателя TCP для сервера политик показана на рис. 15. Она использует тот же асинхронный шаблон цикла, о котором рассказывалось ранее, применительно к TCP Accept. Clientaccesspolicy.xml считывается в буфер и повторно используется для отправки всем клиентам Silverlight. ClientConnection инкапсулирует допущенный сокет и буфер получения, который будет связан с SocketAsyncEventArgs.
Рис. 15. Реализация сервера политик TCP
class TcpPolicyServer
{
private Socket _listener;
private byte[] _policyBuffer;
public static readonly string PolicyFileName = "clientaccesspolicy.xml";
SocketAsyncEventArgs _socketAcceptArgs = new SocketAsyncEventArgs();
public TcpPolicyServer()
{
//read the policy file into the buffer
FileStream fs = new FileStream(PolicyServer.PolicyFileName,
FileMode.Open);
_policyBuffer = new byte[fs.Length];
fs.Read(_policyBuffer, 0, _policyBuffer.Length);
_socketAcceptArgs.Completed += new
EventHandler<SocketAsyncEventArgs>(AcceptAsyncCallback);
}
public void Start(int port)
{
IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
//Should be within the port range of 4502-4532
IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, port);
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
// Bind the socket to the local endpoint and listen for incoming connections
try
{
_listener.Bind(ipEndPoint);
_listener.Listen(50);
AcceptAsync();
}
//code omitted for brevity
}
void AcceptAsync()
{
AcceptAsync(socketAcceptArgs);
}
void AcceptAsync(SocketAsyncEventArgs socketAcceptArgs)
{
if (!_listener.AcceptAsync(socketAcceptArgs))
{
AcceptAsyncCallback(socketAcceptArgs.AcceptSocket,
socketAcceptArgs);
}
}
void AcceptAsyncCallback(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError == SocketError.Success)
{
ClientConnection con = new ClientConnection(e.AcceptSocket,
this._policyBuffer);
con.ReceiveAsync();
}
//the following is necessary for the reuse of _socketAccpetArgs
e.AcceptSocket = null;
//schedule a new accept request
AcceptAsync();
}
}
Пример кода, показанный на рис. 15, повторно использует SocketAsyncEventArgs между несколькими допусками TCP. Чтобы это работало, e.AcceptSocket необходимо установить на null в AcceptAsyncCallback. Этот подход предотвратит замешивание в сбор мусора на сервере с высокими требованиями по масштабируемости.
Интеграция с бизнес-службами
Интеграция со службами является одним из важных аспектов бизнес-приложения (LOB), и Silverlight предоставляет много компонентов для доступа к веб- ресурсам и службам. HttpWebRequest, WebClient и инфраструктура прокси Windows Communication Foundation (WCF) являются одними из сетевых компонентов, широко используемых для взаимодействия на основе HTTP. В данной статье интеграция бизнес-процессами на сервере будет осуществляться с помощью службы WCF.
Большинство из нас в ходе разработки приложения используют для интеграции с серверными источниками данных веб-службы; доступ к веб-службам WCF с помощью Silverlight мало отличается от доступа с помощью традиционных приложений, таких как ASP.NET, Windows Presentation Foundation (WPF) или Windows Forms. Разница заключается в поддержке привязки и асинхронной модели программирования. Silverlight будет поддерживать только basicHttpBinding и PollingDuplexHttpBinding. Отмечу, что HttpBinding является привязкой, обеспечивающей оптимальные возможности взаимодействия. По этой причине в данной статье я буду использовать для интеграции именно ее.
PollingDuplexHttpBinding дает возможность использовать контракты обратного вызова для передачи уведомлений по HTTP. В моем центре обработки вызовов эта привязка могла бы использоваться для всплывающих на экране уведомлений. Но для реализации потребуется кэширование подключения HTTP на сервере, что приводит к монополизации одного из двух параллельных подключений HTTP, допускаемых обозревателями, например Internet Explorer 7.0. Это может вызвать снижение производительности, поскольку все веб-содержимое придется сериализовать через одно подключение. Internet Explorer 8.0 допускает шесть подключений на один домен и избавляет от таких проблемы производительности. (Передача уведомлений с помощью PollingDuplexHttpBinding могла бы стать темой будущей статьи, когда Internet Explorer 8.0 получит широкое распространение.)
Вернемся к приложению. Когда агент принимает вызов, процедура вывода данных на экран заполняет экран информацией о вызывающей стороне — в данном случае, сведениями о заказе вызывающей стороны. В сведениях о вызывающей стороне должна присутствовать необходимая информация, позволяющая однозначно идентифицировать заказ в серверной базе данных. В данной демонстрационной ситуации я полагаю, что номер заказа был сообщен системе интерактивного речевого ответа (IVR). Приложение Silverlight будет вызывать веб-службы WCF с использованием номера заказа в качестве уникального идентификатора. Определение контракта службы и реализация показаны на рис. 2.
Рис. 2. Реализация бизнес-службы
ServiceContracts.cs
[ServiceContract]
public interface ICallService
{
[OperationContract]
AgentScript GetAgentScript(string orderNumber);
[OperationContract]
OrderInfo GetOrderDetails(string orderNumber);
}
[ServiceContract]
public interface IUserProfile
{
[OperationContract]
User GetUser(string userID);
}
CallService.svc.cs
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class CallService:ICallService, IUserProfile
{
public AgentScript GetAgentScript(string orderNumber)
{
...
script.QuestionList = DataUtility.GetSecurityQuestions(orderNumber);
return script;
}
public OrderInfo GetOrderDetails(string orderNumber)
{
...
oi.Customer = DataUtility.GetCustomerByID(oi.Order.CustomerID);
return oi;
}
public User GetUser(string userID)
{
return DataUtility.GetUserByID(userID);
}
}
Web.Config
<system.servicemodel>
<services>
<endpoint binding="basicHttpBinding" contract="AdvBusinessServices.ICallService"/>
<endpoint binding="basicHttpBinding" contract="AdvBusinessServices.IUserProfile"/>
</services>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<system.servicemodel>
Реализация этих конечных точек службы не представляет большого интереса, поскольку это простые реализации WCF. Для простоты я не буду использовать никакую базу данных для бизнес-объектов, а буду просто использовать находящиеся в памяти объекты List для хранения объектов Customer, Order и User. Класс DataUtil (он здесь не показан, но доступен в загружаемом коде) инкапсулирует доступ к этим находящимся в памяти объектам List.
Рис. 3 Сценарий агента с вопросами, касающимися безопасности
Конечным точкам службы WCF для использования Silverlight требуется доступ к конвейеру ASP.NET и, следовательно, требуется атрибут AspNetCompatibilityRequirements реализации CallService. Это соответствие должно быть обеспечено настройкой <serviceHostingEnvironment/> в файле web.config.
Как упоминалось ранее, Silverlight поддерживает только basicHttpBinding и PollingDuplexHttpBinding. Если используется шаблон WCF Service Visual Studio, он настраивает привязку конечной точки к wsHttpBinding, которую необходимо вручную привязкой basicHttpBinding, прежде чем Silverlight сможет добавлять ссылки на службы для генерации прокси. Изменения совместимости размещения ASP.NET и изменения привязок автоматически учитываются, если CallService.svc добавляется к проекту AdvBusinessServices с помощью шаблона WCF Service Visual Studio, поддерживающего Silverlight.
Вызов служб
После того, как реализована вызываемая из Silverlight служба, наступает момент создания прокси служб и их использования для установления связи пользовательского интерфейса с реализациями серверных служб. Надежная генерация прокси для служб WCF возможна только с помощью последовательного выбора пунктов меню Service References | Add Service Reference («Ссылки на службу» | «Добавление ссылки на службу») в Visual Studio. Прокси в моем демонстрационном примере были сгенерированы в пространство имен CallBusinessProxy. Silverlight допускает только асинхронные вызовы сетевых ресурсов, и вызов службы не является исключением. Когда от клиента поступает вызов, клиент Silverlight прослушивает уведомление и отображает диалоговое окно «Принять/Отклонить».
После того, как вызов принят агентом, следующий шаг процедуры заключается в вызове веб-службы для получения сценария агента, соответствующего ситуации данного вызова. Для данного демонстрационного примера я буду использовать только один сценарий, отображенный на рис. 3. В отображенный сценарий входит приветствие и список вопросов, относящихся к безопасности. Агент гарантирует, что прежде, чем предоставлять поддержку, будет получен необходимый минимум ответов на вопросы.
Сценарий агента извлекается посредством получения доступа к методу ICallService.GetAgentScript(), которому в качестве входных данных передается номер заказа. В соответствии с асинхронной моделью программирования, обусловленной стеком веб-служб Silverlight, метод GetAgentScript() доступен в виде CallServiceClient.BeginGetAgentScript(). Во время вызова службы вам необходимо предоставить обработчик обратного вызова, GetAgentScriptCallback, как показано на рис. 4.
Рис. 4 Вызов службы и изменение пользовательского интерфейса Silverlight
class Page:UserControl
{
...
void _notifyCallPopup_OnAccept(object sender, EventArgs e)
{
AcceptMessage acceptMsg = new AcceptMessage();
acceptMsg.RepNumber = ClientGlobals.currentUser.RepNumber;
ClientGlobals.socketClient.SendAsync(acceptMsg);
this.borderCallProgressView.DataContext = ClientGlobals.callInfo;
ICallService callService = new CallServiceClient();
IAsyncResult result =
callService.BeginGetAgentScript(ClientGlobals.callInfo.OrderNumber,
GetAgentScriptCallback, callService);
//do a preemptive download of user control
ThreadPool.QueueUserWorkItem(ExecuteControlDownload);
//do a preemptive download of the order information
ThreadPool.QueueUserWorkItem(ExecuteGetOrderDetails,
ClientGlobals.callInfo.OrderNumber);
}
void GetAgentScriptCallback(IAsyncResult asyncReseult)
{
ICallService callService = asyncReseult.AsyncState as ICallService;
CallBusinessProxy.AgentScript svcOutputAgentScript =
callService.EndGetAgentScript(asyncReseult);
ClientEntityTranslator astobas =
SvcScriptToClientScript.entityTranslator;
ClientEntities.AgentScript currentAgentScript =
astobas.ToClientEntity(svcOutputAgentScript)
as ClientEntities.AgentScript;
Interlocked.Exchange<ClientEntities.AgentScript>(ref
ClientGlobals.currentAgentScript, currentAgentScript);
if (this.Dispatcher.CheckAccess())
{
this.borderAgentScript.DataContext = ClientGlobals.agentScript;
...
this.hlVerifyContinue.Visibility = Visibility.Visible;
}
else
{
this.Dispatcher.BeginInvoke(
delegate()
{
this.borderAgentScript.DataContext = ClientGlobals.agentScript;
...
this.hlVerifyContinue.Visibility = Visibility.Visible;
} );
}
}
private void ExecuteControlDownload(object state)
{
WebClient webClient = new WebClient();
webClient.OpenReadCompleted += new
OpenReadCompletedEventHandler(OrderDetailControlDownloadCallback);
webClient.OpenReadAsync(new Uri("/ClientBin/AdvOrderClientControls.dll",
UriKind.Relative));
}
...
}
Поскольку результат вызова службы можно извлечь только из обработчика обратного вызова, любые изменения состояния приложения Silverlight должны происходить в обработчике обратного вызова.
CallServiceClient.BeginGetAgentScript() вызывается из _notifyCallPopup_OnAccept, работающего в потоке пользовательского интерфейса, и ставит в очередь асинхронный запрос, после чего незамедлительно возвращается к следующему оператору. Поскольку агент сценария еще не доступен, придется подождать, пока не будет запущен обратный вызов, прежде чем кэшировать сценарий и привязывать его к пользовательскому интерфейсу посредством данных.
Успешное завершение вызова службы запускает GetAgentScriptCallback, который получает сценарий, заполняет глобальные переменные и корректирует пользовательский интерфейс, осуществляя привязку сценария агента к соответствующим элементам интерфейса посредством данных. Корректируя пользовательский интерфейс, GetAgentScriptCallback обеспечивает его обновление в потоке интерфейса посредством Dispatcher.CheckAccess().
UIElement.Dispatcher.CheckAccess() сравнит идентификатор потока пользовательского интерфейса с идентификатором рабочего потока и возвратит значение «истина», если это один и тот же поток; в противном случае возвращается значение «ложь». Когда GetAgentScriptCallback исполняется в рабочем потоке (так как выполнение всегда будет происходить в рабочем потоке, по существу, можно просто вызывать Dispatcher.BeginInvoke), CheckAccess() вернет значение «ложь», и пользовательский интерфейс будет обновлен посредством направления анонимного делегата через Dispatcher.Invoke().
Синхронизированные вызовы служб
Вследствие асинхронной природы сетевой среды Silverlight практически невозможно осуществить асинхронный вызов в потоке пользовательского интерфейса и ждать его выполнения с намерением изменить состояние приложения на основе результатов вызова. На рис. 4 _notifyCallPopup_OnAccept должен получить сведения о заказе, преобразовать выходное сообщение в объект клиента и сохранить его в глобальной переменной безопасным с точки зрения потока способом. При выполнении этой задачи можно поддаться искушению и написать код обработчика, как показано ниже.
CallServiceClient client = new CallServiceClient();
client.GetOrderDetailsAsync(orderNumber);
this._orderDetailDownloadHandle.WaitOne();
//do something with the results
Но этот код заблокирует приложение, как только оно дойдет до оператора this._orderDetailDownloadHandle.WaitOne(). Это обусловлено тем, что оператор WaitOne() блокирует для потока пользовательского интерфейса возможность получения любых сообщений, направленных из других потоков. Вместо этого можно предусмотреть, чтобы рабочий поток выполнял вызов службы, ждал завершения вызова, и чтобы завершение последующей обработки результатов работы службы в целом происходило в рабочем потоке. Эта методика показана на рис. 5. Для предотвращения непреднамеренного использования блокирующих вызовов в потоке пользовательского интерфейса я заключил ManualResetEvent в пользовательский SLManualResetEvent и делаю проверку для потока пользовательского интерфейса, когда осуществляется вызов WaitOne().
Рис. 5 Получение сведений о заказе
void _notifyCallPopup_OnAccept(object sender, EventArgs e)
{
...
ThreadPool.QueueUserWorkItem(ExecuteGetOrderDetails,
ClientGlobals.callInfo.OrderNumber);
}
private SLManualResetEvent _ orderDetailDownloadHandle = new
SLManualResetEvent();
private void ExecuteGetOrderDetails(object state)
{
CallServiceClient client = new CallServiceClient();
string orderNumber = state as string;
client.GetOrderDetailsCompleted += new
EventHandler<GetOrderDetailsCompletedEventArgs>
(GetOrderDetailsCompletedCallback);
client.GetOrderDetailsAsync(orderNumber);
this._orderDetailDownloadHandle.WaitOne();
//translate entity and save it to global variable
ClientEntityTranslator oito = SvcOrderToClientOrder.entityTranslator;
ClientEntities.Order currentOrder =
oito.ToClientEntity(ClientGlobals.serviceOutputOrder)
as ClientEntities.Order;
Interlocked.Exchange<ClientEntities.Order>(ref ClientGlobals.
currentOrder, currentOrder);
}
void GetOrderDetailsCompletedCallback(object sender,
GetOrderDetailsCompletedEventArgs e)
{
Interlocked.Exchange<OrderInfo>(ref ClientGlobals.serviceOutputOrder,
e.Result);
this._orderDetailDownloadHandle.Set();
}
Поскольку SLManualResetEvent является классом общего назначения, вы не зависите от метода Dispatcher.CheckAccess() конкретного элемента управления. ApplicationHelper.IsUiThread() может проверить Application.RootVisual.Dispatcher.CheckAccess(); однако, доступ к этому методу запускает исключение недопустимого межпотокового доступа. Поэтому единственным надежным способом проверки этого в рабочем потоке, когда нет никакого доступа к экземпляру UIElement, является использование метода Deployment.Current.Dispatcher.CheckAccess(), как показано ниже.
public static bool IsUiThread()
{
if (Deployment.Current.Dispatcher.CheckAccess())
return true;
else
return false;
}
Для фонового выполнения задач вместо использования метода ThreadPool.QueueUserWorkItem можно использовать BackGroundWorker, который также использует ThreadPool, но позволяет устанавливать связь с обработчиками, которые могут выполняться в потоке пользовательского интерфейса. Данный шаблон позволяет выполнять несколько вызовов служб параллельно и ждать завершения всех вызовов, используя метод SLManualResetEvent.WaitOne(), прежде чем результаты будут объединены для последующей обработки.
Преобразование объектов сообщения
Кроме этого, метод GetAgentScriptCallback преобразует объекты выходного сообщения (известные также как DataContracts) из службы в объект клиентской стороны, представляющий обычную семантику клиентской стороны. Например, при проектировании объектов сообщений серверной стороны можно не заботиться о привязке данных, обращая при этом внимание на то, что по своей природе служба используется многократно и должна предусматривать широкий диапазон применений, а не только центр обработки вызовов.
Помимо этого разумно избегать тесной связи с объектами сообщения, поскольку у клиента не будет возможности контролировать изменения объектов сообщений. Методика преобразования объектов сообщения в объекты клиентской стороны не применима напрямую к Silverlight, но в общем случае может быть применена к любому потребителю веб-службы, если требуется избежать тесной связи на этапе проектирования.
Я принял решение сделать реализацию преобразователей объектов крайне простой — никаких экзотических вложенных обобщенных типов, лямбда-выражений или инверсий контейнеров элементов управления. ClientEntityTranslator является абстрактным классом, определяющим метод ToClientEntity(), который каждый подкласс должен переопределять.
public abstract class ClientEntityTranslator
{
public abstract ClientEntities.ClientEntity ToClientEntity(object
serviceOutputEntity);
}
Каждый дочерний класс является уникальным для типа обмена между службами; следовательно, я буду создавать столько преобразователей, сколько потребуется. В моем демонстрационном примере имеется три типа вызовов служб: IUserProfile.GetUser(), ICallService.GetAgentScript() и ICallService.GetOrderDetails(). Поэтому я создал три преобразователя, как показано на рис. 6.
Рис. 6 Преобразователь объекта сообщения в объект клиентской стороны
public class SvcOrderToClientOrder : ClientEntityTranslator
{
//singleton
public static ClientEntityTranslator entityTranslator = new
SvcOrderToClientOrder();
private SvcOrderToClientOrder() { }
public override ClientEntities.ClientEntity ToClientEntity(object
serviceOutputEntity)
{
CallBusinessProxy.OrderInfo oi = serviceOutputEntity as
CallBusinessProxy.OrderInfo;
ClientEntities.Order bindableOrder = new ClientEntities.Order();
bindableOrder.OrderNumber = oi.Order.OrderNumber;
//code removed for brevity ...
return bindableOrder;
}
}
public class SvcUserToClientUser : ClientEntityTranslator
{
//code removed for brevity ...
}
public class SvcScriptToClientScript : ClientEntityTranslator
{
//code removed for brevity ...
}
}
Если вы обратили внимание, вышеприведенные преобразователи имеют неизменное состояние и используют единственный шаблон. Из соображений согласованности преобразователь должен быть в состоянии наследовать классу ClientEntityTranslator и он должен быть singleton-классом во избежание попадания сборщику мусора.
Я постоянно использую один и тот же экземпляр при любом вызове соответствующей службы. Можно было бы также создать ServiOutputEntityTranslator для взаимодействия служб, которое требует больших входных сообщений (обычно так бывает в случае вызова транзакционной службы), с помощью следующего определения класса.
public abstract class ServiOutputEntityTranslator
{
public abstract object ToServiceOutputEntity(ClientEntity
clientEntity);
}
Если вы обратите внимание на значение, возвращаемое упомянутой выше функцией, то увидите, что это «object», поскольку я не управляю базовым классом объектов сообщения (это было бы возможно в данном демонстрационном примере, но не в реальной ситуации). Безопасность типа будет реализована соответствующими преобразователями. Для упрощения демонстрационного примера я не сохраняю никакие данные на сервере, поэтому в данный пример не входят никакие преобразователи объектов клиента в объекты сообщения.
Изменение состояния Silverlight после вызовов служб
Изменение визуального состояния Silverlight может быть выполнено только кодом, исполняющимся в потоке пользовательского интерфейса. Поскольку при асинхронном выполнении вызовов служб результаты всегда возвращаются обработчику обратного вызова, то именно обработчик является местом, откуда можно изменять визуальное или невидимое состояние приложения.
Если несколько служб могут пытаться асинхронно изменить общее состояние, изменениями невидимого состояния следует обмениваться безопасным с точки зрения потока способом. Всегда, прежде чем изменять пользовательский интерфейс, рекомендуется проверять значение Deployment.Current.Dispatcher.CheckAccess().
Междоменные политики
В отличие от мультимедийных приложений и приложений, отображающих рекламные объявления, настоящие бизнес-приложения уровня предприятия класса требуют интеграции с широким набором сред размещения служб. Например, приложение центра обработки вызовов, на которое я постоянно ссылаюсь в этой статье, является типичным приложением уровня предприятия. Это приложение, размещенное на веб-сайте, осуществляет доступ к серверу сокетов с сохранением состояния для вывода на экран всплывающих данных, к веб-службам на основе WCF для получения доступа к данным LOB и может загружать дополнительные пакеты XAP (сжатые пакеты для развертывания Silverlight) из другого домена. Оно будет использовать еще один домен для передачи инструментальных данных.
Песочница Silverlight по умолчанию не разрешает сетевой доступ к произвольному домену, кроме того, откуда она происходит — advcallclientweb, как вы видели на рис. 1. Среда выполнения Silverlight проверяет наличие политик явного согласия, когда приложение осуществляет доступ к некоторому домену (за исключением того, откуда оно происходит). Вот типичный список вариантов размещения служб, которые должны поддерживать запросы междоменной политики клиентом.
- Веб-службы, размещенные в процессе службы (или, для простоты, в приложении консоли)
- Веб-службы, размещенные на сервере IIS или других веб-серверах
- Службы TCP, размещенные в процессе службы (или приложении консоли)
В прошлом месяце я обсуждал реализацию междоменной политики для служб TCP, и поэтому основное внимание буду уделять веб-службам, размещенным в пользовательских процессах и внутри IIS.
Хотя реализовать междоменные политики для конечных точек веб-службы, размещенной в IIS, несложно, в других случаях требуется понимание природы запросов и ответов политики.
Междоменные политики для веб-служб, размещенных вне IIS
Возможны ситуации, когда с целью эффективного управления состоянием службы размещают в процессах ОС вне IIS. Для междоменного доступа к таким службам WCF процесс должен будет разместить политики в корневом каталоге конечной точки HTTP. При вызове междоменной веб-службы Silverlight выдает запрос HTTP Get к clientaccesspolicy.xml. Если служба размещена внутри IIS, файл clientaccesspolicy.xml можно скопировать в корневой каталог веб-узла, а все остальное обслуживание файла выполнит IIS. В случае пользовательского размещения на локальной машине http://localhost:<port>/clientaccesspolicy.xml должен представлять допустимый адрес URL.
Поскольку в демонстрационном примере центра обработки вызовов не используются никакие размещаемые пользователем веб-службы, для демонстрации концепций я буду использовать в приложении консоли простую службу TimeService. Консоль будет предоставлять конечную точку передачи репрезентативного состояния (REST) с помощью новых возможностей REST платформы Microsoft .NET Framework 3.5. В качестве значения свойства UriTemplate необходимо установить именно тот литерал, который приведен на рис. 7.
Рис. 7. Реализация для размещаемых пользователем служб WCF
[ServiceContract]
public interface IPolicyService
{
[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = "/clientaccesspolicy.xml")]
Stream GetClientAccessPolicy();
}
public class PolicyService : IPolicyService
{
public Stream GetClientAccessPolicy()
{
FileStream fs = new FileStream("PolicyFile.xml", FileMode.Open);
return fs;
}
}
Имя интерфейса или имя метода не имеет никакого значения для результата; вы можете выбрать любое, которое вам понравится. У WebInvoke имеются другие свойства, такие как RequestFormat и ResponseFormat, которые по умолчанию настроены на XML; нет необходимости указывать их значения явно. Точно так же мы полагаемся на значение BodyStyle.Bare, установленное по умолчанию для свойства BodyStyle, что означает, что ответ не будет заключаться в оболочку.
Реализация службы крайне проста: в ответ на запрос клиента Silverlight файл clientaccesspolicy.xml просто передается в потоке. Имя файла политики можно выбирать самостоятельно, поэтому оно может быть любым. Реализация службы политик показана на рис. 7.
Теперь нам требуется настроить IPolicyService для обслуживания запросов HTTP в стиле REST. App.Config для приложения консоли (ConsoleWebServices) показан на рис. 8. Следует сделать несколько замечаний относительно необходимости специальной настройки: привязку конечной точки ConsoleWebServices.IPolicyServer необходимо настроить на webHttpBinding. Кроме этого, поведение конечной точки IPolicyService необходимо настроить с помощью WebHttpBehavior, как показано в файле настройки. В качестве базового адреса PolicyService следует установить адрес URL корневого каталога (как в ), а адрес конечной точки следует оставить пустым (например <endpoint address="" … contract="ConsoleWebServices.IPolicyService" />.
Рис. 8. Настройки WCF для пользовательской среды размещения
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<!-- IPolicyService end point should be configured with
webHttpBinding-->
<service name="ConsoleWebServices.PolicyService">
<endpoint address=""
behaviorConfiguration="ConsoleWebServices.WebHttp"
binding="webHttpBinding"
contract="ConsoleWebServices.IPolicyService" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:3045/" />
</baseAddresses>
</host>
</service>
<service behaviorConfiguration="ConsoleWebServices.TimeServiceBehavior"
name="ConsoleWebServices.TimeService">
<endpoint address="TimeService" binding="basicHttpBinding"
contract="ConsoleWebServices.ITimeService">
</endpoint>
<host>
<baseAddresses>
<add baseAddress="http://localhost:3045/TimeService.svc" />
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<endpointBehaviors>
<!--end point behavior is used by REST endpoints like
IPolicyService described above-->
<behavior name="ConsoleWebServices.WebHttp">
<webHttp />
</behavior>
</endpointBehaviors>
...
</behaviors>
</system.serviceModel>
</configuration>
Наконец, службы, размещаемые в консоли, например TimeService, приведенная в примерах кода и настроек, должны быть настроены так, чтобы их адрес URL имел вид, подобный адресу их IIS аналогов. Например, адрес URL конечной точки службы TimeService, размещаемой в IIS, может иметь следующий вид на HTTP по умолчанию: http://localhost/TimeService.svc. В этом случае метаданные можно получить с http://localhost/TimeService.svc?WSDL.
В случае размещения в консоли метаданные можно получить, добавив «?WSDL» к базовому адресу места размещения службы. В настройке, показанной на рис. 8, видно, что базовый адрес службы TimeService имеет вид http://localhost:3045/TimeService.svc, следовательно, метаданные можно получить с адреса http://localhost:3045/TimeService.svc?WSDL.
Этот адрес URL подобен тому, который мы используем при размещении в IIS. Если базовый адрес настроен на значение http://localhost:3045/TimeService.svc/, тогда метаданные адреса URL имеют вид http://localhost:3045/TimeService.svc/?WSDL, что выглядит несколько странно. Так что следите за этим поведением, поскольку это может сэкономить вам время при вычислении метаданных URL.
Междоменные политики для служб, размещенных внутри IIS
Как обсуждалось ранее, развертывание междоменных политик для размещаемых в IIS служб выполняется просто: достаточно скопировать файл clientaccesspolicy.xml в корневой каталог того узла, на котором размещаются веб-службы. Как вы видели на рис. 1, приложение Silverlight размещается на advcallclientweb (localhost:1041) и получает доступ к бизнес-службам из AdvBusinessServices (localhost:1043). Среде выполнения Silverlight требуется, чтобы файл clientaccesspolicy.xml был помещен в корневой каталог веб-узла AdvBusinessServices с помощью кода, показанного на рис. 9.
Рис. 9 Clientaccesspolicy.xml для веб-служб, размещаемых в IIS
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<!--allows the access of Silverlight application with localhost:1041
as the domain of origin-->
<domain uri="http://localhost:1041"/>
<!--allows the access of call simulator Silverlight application
with localhost:1042 as the domain of origin-->
<domain uri="http://localhost:1042"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
Если вы помните формат междоменной политики для сервера сокетов (advpolicyserver) из первого выпуска данной серии статей, видно, что формат <allow-from> подобен ему. Различие заключается в разделе <grant-to>, где серверу сокетов требуется настройка <socket-resource> с указанием диапазона портов и атрибутов протокола, как показано ниже.
<grant-to>
<socket-resource port="4530" protocol="tcp" />
</grant-to>
Если узел для размещения служб WCF создается с помощью шаблона веб-узла ASP.NET, и впоследствии добавляются конечные точки WCF, тестовый веб-сервер включает виртуальный каталог в имя проекта (например «/AdvBusinessServices»). На странице свойств проекта его следует заменить на «/», чтобы файл clientaccesspolicy.xml предоставлялся из корневого каталога. Если не сделать этого изменения, то файл clientaccesspolicy.xml не попадет в корневой каталог, и приложения Silverlight при осуществлении доступа к службе будут получать ошибки сервера. Отмечу, что такой проблемы не возникает при создании веб-узлов с помощью шаблона проекта веб-службы WCF.
Рис. 10 Управление входом в систему с помощью PasswordBox
<UserControl x:Class="AdvCallCenterClient.Login">
<Border x:Name="LayoutRoot" ... >
<Grid x:Name="gridLayoutRoot">
<Border x:Name="borderLoginViw" ...>
<TextBlock Text="Pleae login.." Style="{StaticResource headerStyle}"/>
<TextBlock Text="Rep ID" Style="{StaticResource labelStyle}"/>
<TextBox x:Name="txRepID" Style="{StaticResource valueStyle}"/>
<TextBlock Text="Password" Style="{StaticResource labelStyle}"/>
<PasswordBox x:Name="pbPassword" PasswordChar="*"/>
<HyperlinkButton x:Name="hlLogin" Content="Click to login"
ToolTipService.ToolTip="Clik to login" Click="hlLogin_Click" />
</Border>
<TextBlock x:Name="tbLoginStatus" Foreground="Red" ... />
...
</UserControl>
public partial class Login : UserControl
{
public Login()
{
InitializeComponent();
}
public event EventHandler<EventArgs> OnSuccessfulLogin;
private void hlLogin_Click(object sender, RoutedEventArgs e)
{
//validate the login
AuthenticationProxy.AuthenticationServiceClient authService
= new AuthenticationProxy.AuthenticationServiceClient();
authService.LoginCompleted += new
EventHandler< AuthenticationProxy.LoginCompletedEventArgs>
(authService_LoginCompleted);
authService.LoginAsync(this.txRepID.Text, this.pbPassword.Password,
null, false);
}
void authService_LoginCompleted(object sender,
AuthenticationProxy.LoginCompletedEventArgs e)
{
if (e.Result == true)
{
if (OnSuccessfulLogin != null)
OnSuccessfulLogin(this, null);
}
else
{
this.tbLoginStatus.Text = "Invalid user id or password";
}
}
}
Безопасность приложений
Одним из важнейших требований приложения LOB является проверка подлинности; прежде чем агент центра обработки вызовов начнет смену, он проходит проверку подлинности, предоставляя идентификатор пользователя и пароль. В веб-приложениях ASP.NET это легко осуществить, воспользовавшись преимуществом поставщика членства и серверными элементами управления входом ASP.NET. В Silverlight имеются два способа обеспечить обязательность применения проверки подлинности: внешняя и внутренняя проверка подлинности.
Внешняя проверка подлинности реализуется очень просто и подобна реализации проверки подлинности приложений ASP.NET. При этом подходе проверка подлинности происходит на веб-странице, созданной на основе ASP.NET, до того, как отображается приложение Silverlight. Контекст проверки подлинности можно передать в приложение Silverlight через параметр InitParams до загрузки приложения Silverlight или посредством вызова веб-службы (для извлечения информации о состоянии проверки подлинности) после загрузки приложения.
Этот подход уместен, когда приложение Silverlight является частью большой системы на основе ASP.NET/HTML. Но в случаях, когда Silverlight является основной движущей силой приложения, естественно выполнять проверку подлинности в рамках Silverlight. С целью проверки учетных данных пользователя я буду использовать элемент управления PasswordBox из Silverlight 2 для перехвата пароля и проверки подлинности с помощью конечной точки ASP.NET AuthenticationService WCF. AuthenticationService, ProfileService и RoleService входят в новое пространство имен, System.Web.ApplicationServices, которое появилось в .NET Framework 3.5. На рис. 10 показан XAML для элемента управления Login, созданного для этой цели. Элемент управления Login вызывает ASP.NET AuthenticationService.LoginAsync(), передавая введенные пользователем идентификатор и пароль.
Рис. 11 Пользовательский элемент управления Login в Silverlight
Экран входа в систему центра обработки данных, показанный на рис. 11, выглядит просто, но вполне годится для целей демонстрационного примера. Я реализовал обработчик для работы с событием LoginCompleted внутри элемента управления таким образом, чтобы он был способен самостоятельно отображать диалоговые окна с сообщениями о неверном идентификаторе пользователя и сбросе пароля в случае более сложных реализаций. После успешной регистрации создается событие OnSuccessfulLogin для передачи родительскому элементу управления (в данном случае это Application.RootVisual) информации о необходимости отобразить первый экран приложения, заполненный сведениями о пользователе.
Обработчик LoginCompleted (ctrlLoginView_OnSuccessfulLogin), находящийся на главной странице Silverlight, вызовет службу профилей, размещенную на веб-сайте бизнес-служб, как показано на рис. 12. По умолчанию AuthenticationService не сопоставляется никакой конечной точке .svc; следовательно, я буду сопоставлять svc-файл физической реализации, как показано ниже:
<!-- AuthenticationService.svc -->
<%@ ServiceHost Language="C#" Service="System.Web.ApplicationServices.
AuthenticationService" %>
Рис. 12 Использование Login.xaml в рамках Page.xaml
<!-- Page.xaml of the main UserControl attached to RootVisual-->
<UserControl x:Class="AdvCallCenterClient.Page" ...>
<page:Login x:Name="ctrlLoginView" Visibility="Visible"
OnSuccessfulLogin="ctrlLoginView_OnSuccessfulLogin"/>
...
</UserControl>
<!-- Page.xaml.cs of the main UserControl attached to RootVisual-->
public partial class Page : UserControl
{
...
private void ctrlLoginView_OnSuccessfulLogin(object sender, EventArgs e)
{
Login login = sender as Login;
login.Visibility = Visibility.Collapsed;
CallBusinessProxy.UserProfileClient userProfile
= new CallBusinessProxy.UserProfileClient();
userProfile.GetUserCompleted += new
EventHandler<GetUserCompletedEventArgs>(userProfile_GetUserCompleted);
userProfile.GetUserAsync(login.txRepID.Text);
}
...
void userProfile_GetUserCompleted(object sender,
GetUserCompletedEventArgs e)
{
CallBusinessProxy.User user = e.Result;
UserToBindableUser utobu = new UserToBindableUser(user);
ClientGlobals.currentUser = utobu.Translate() as ClientEntities.User;
//all the time the service calls will be complete on a worker thread
//so the following check is redunant but done to be safe
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(delegate()
{
this.registrationView.DataContext = ClientGlobals.currentUser;
this.ctrlLoginView.Visibility = Visibility.Collapsed;
this.registrationView.Visibility = Visibility.Visible;
});
}
}
}
Silverlight может вызывать только веб-службы, которые настроены для вызова средами сценариев, такими как AJAX. Подобно всем вызываемым службам AJAX, службе AuthenticationService необходим доступ к среде выполнения ASP.NET. Я обеспечиваю этот доступ настройкой <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> непосредственно в узле <system.servicemodel>. Для того чтобы служба проверки подлинности могла быть вызвана процессом Silverlight, обеспечивающим вход в систему (или могла быть вызвана посредством AJAX), файл web.config должен быть настроен в соответствии с указаниями из статьи «Практическое руководство. Включение службы проверки подлинности WCF». Службы автоматически настроены для Silverlight, если они созданы с помощью расположенного в категории Silverlight шаблона служб WCF, поддерживающего Silverlight.
На рис. 13 показана измененная настройка с важными элементами, необходимыми для службы проверки подлинности. Кроме настройки службы я заменил в настройке SQL Server значение параметра aspnetdb, хранящего данные проверки подлинности. Machine.config определяет параметр LocalSqlServer, определяющий, что aspnetdb.mdf необходимо внедрить в каталог App_Data веб-узла. Данный параметр настройки отменяет значение по умолчанию и назначает aspnetdb, связанный с экземпляром SQL Server. Это легко изменить и назначить экземпляр базы данных, работающий на отдельной машине.
Рис. 13 Настройки для службы проверки подлинности ASP.NET
//web.config
<Configuration>
<connectionStrings>
<!-- removal and addition of LocalSqlServer setting will override the
default asp.net security database used by the ASP.NET Configuration tool
located in the Visul Studio Project menu-->
<remove name="LocalSqlServer"/>
<add name="LocalSqlServer" connectionString="Data
Source=localhost\SqlExpress;Initial Catalog=aspnetdb; ... />
</connectionStrings>
<system.web.extensions>
<scripting>
<webServices>
<authenticationService enabled="true" requireSSL="false"/>
</webServices>
</scripting>
</system.web.extensions>
...
<authentication mode="Forms"/>
...
<system.serviceModel>
<services>
<service name="System.Web.ApplicationServices.AuthenticationService"
behaviorConfiguration="CommonServiceBehavior">
<endpoint
contract="System.Web.ApplicationServices.AuthenticationService"
binding="basicHttpBinding" bindingConfiguration="useHttp"
bindingNamespace="http://asp.net/ApplicationServices/v200"/>
</service>
</services>
<bindings>
<basicHttpBinding>
<binding name="useHttp">
<!--for production use mode="Transport" -->
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
...
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
</system.serviceModel>
</configuration>
Чтобы сохранить инкапсуляцию элемента управления Login и поддерживать на этапе разработки ослабленную связь с родительским элементом управления, об успешном завершении процесса входа сообщается посредством создания события OnSuccessfulLogin. Application.RootVisual (являющийся классом Page) выполнит необходимый бизнес-процесс для отображения первого экрана после успешного входа в систему. Первый экран, отображенный после успешного входа, называется registrationView, как видно из метода userProfile_GetUserCompleted на рис. 12. Прежде чем будет отображено это представление, я получу информацию о пользователе, вызывая CallBusinessProxy.UserProfileClient.GetUserAsync(). Обратите внимание на асинхронный вызов службы, подобный вызову при интеграции бизнес-служб, которая будет обсуждаться далее.
Имейте в виду, что в предыдущей настройке не используется протокол защищенных сокетов (SSL); при создании производственных систем ее необходимо изменить так, чтобы использовался протокол SSL.
Рис. 14 Элемент управления OrderDetails.xaml с данными о заказе
Разбиение приложений на разделы
Одним из факторов, влияющих на длительность запуска приложения Silverlight, является размер исходного пакета. Рекомендации относительно размера пакета XAP не отличаются от рекомендаций по объему страницы для веб-приложений. Пропускная способность является ограниченным ресурсом. Строгие требования ко времени ответа веб-приложений требуют пристального внимания к длительности запуска приложения Silverlight.
Помимо времени на обработку, проходящего до отображения первого UserControl, на эту важную характеристику приложения непосредственно влияет объем пакета приложения. Для повышения скорости запуска необходимо избегать монолитных файлов XAP, размер которых может достигать десятков мегабайт в случае сложных приложений.
Приложение Silverlight можно разбить на коллекцию файлов XAP; отдельные библиотеки DLL; или отдельные файлы XML, изображения и файлы других типов с известными MIME. Для демонстрации мелкомасштабного разбиения приложения центра обработки вызовов на разделы я буду использовать элемент управления OrderDetail Silverlight в качестве отдельной библиотеки DLL (AdvOrderClientControls.dll) наряду с файлом AdvCallCenterClient.xap в каталоге ClientBin проекта AdvCallClientWeb (обратитесь к рис. 1).
Библиотека DLL будет заранее загружаться в рабочий поток, когда агент принимает входящий вызов. За это отвечает вызов, который вы видели на рис. 4, ThreadPool.QueueUserWorkItem(ExecuteControlDownload). После того, как вызывающая сторона ответит на вопросы, относящиеся к безопасности, я использую отражение для создания элемента управления OrderDetail и добавляю его к дереву элементов управления, прежде чем отображать его на экране. На рис. 14 показан элемент управления OrderDetail.xaml с данными заказа, загруженный в дерево элементов управления.
Библиотека DLL, содержащая элемент управления OrderDetail, разворачивается на том же веб-сайте, что и клиент центра обработки вызовов. Это характерно для библиотек DLL, принадлежащих одному и тому же приложению, поэтому в данном случае не возникает никаких проблем, связанных с разными доменами. Однако они могут возникнуть при работе со службами. Приложения Silverlight могут осуществлять доступ к службам, развернутым в нескольких доменах, включая локальные и из «облака», как показано на схеме архитектуры (и снова следует обратиться к рис. 1).
Метод ExecuteControlDownload (см. рис. 4) выполняется в фоновом рабочем потоке и использует класс WebClient для загрузки DLL. В WebClient по умолчанию предполагается, что загрузка выполняется из домена происхождения и, следовательно, используются только относительные URI.
Обработчик OrderDetailControlDownloadCallback принимает поток DLL и создает сборку с помощью ResourceUtility.GetAssembly(), который показан на рис. 15. Поскольку создание сборки должно происходить в потоке пользовательского интерфейса, я отправляю GetAssembly() и (безопасное с точки зрения потока) присваивание сборки глобальной переменной для потока пользовательского интерфейса:
void OrderDetailControlDownloadCallback(object sender, OpenReadCompletedEventArgs e)
{
this.Dispatcher.BeginInvoke(delegate() {
Assembly asm = ResourceUtility.GetAssembly(e.Result);
Interlocked.Exchange<Assembly>(ref
ClientGlobals.advOrderControls_dll, asm ); });
}
Рис. 15 Функции вспомогательной программы для извлечения ресурсов
public class ResourceUtility
{
//helper function to retrieve assembly from a package stream
public static Assembly GetAssembly(string assemblyName, Stream
packageStream)
{
StreamResourceInfo srInfo =
Application.GetResourceStream(
new StreamResourceInfo(packageStream, "application/binary"),
new Uri(assemblyName, UriKind.Relative));
return GetAssembly(srInfo.Stream);
}
//helper function to retrieve assembly from a assembly stream
public static Assembly GetAssembly(Stream assemblyStream)
{
AssemblyPart assemblyPart = new AssemblyPart();
return assemblyPart.Load(assemblyStream);
}
//helper function to create an XML document from the stream
public static XElement GetXmlDocument(Stream xmlStream)
{
XmlReader reader = XmlReader.Create(xmlStream);
XElement element = XElement.Load(reader);
return element;
}
//helper function to create an XML document from the default package
public static XElement GetXmlDocumentFromXap(string fileName)
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.XmlResolver = new XmlXapResolver();
XmlReader reader = XmlReader.Create(fileName);
XElement element = XElement.Load(reader);
return element;
}
//gets the UIElement from the default package
public static UIElement GetUIElementFromXaml(string xamlFileName)
{
StreamResourceInfo streamInfo = Application.GetResourceStream(new
Uri(xamlFileName, UriKind.Relative));
string xaml = new StreamReader(streamInfo.Stream).ReadToEnd();
UIElement uiElement = null;
try
{
uiElement = (UIElement)XamlReader.Load(xaml);
}
catch
{
throw new SLApplicationException(string.Format("Can't create
UIElement from {0}", xamlFileName));
}
return uiElement;
}
}
Поскольку отправленный делегат и обработчик обратного вызова выполняются в разных потоках, необходимо отдавать себе отчет в том, что доступ к объектам осуществляет анонимный делегат. В предыдущем коде состояние загружаемого потока DLL имело большое значение. Вы не можете написать код, запрашивающий ресурсы потока, в функции OrderDetailControlDownloadCallback. Такой код будет преждевременно избавляться от загруженного потока до того, как поток пользовательского интерфейса получит шанс создать сборку. Я использую отражение для создания экземпляра пользовательского элемента управления OrderDetail и добавлю его на панель, как показано ниже.
_orderDetailContol = ClientGlobals.advOrderControls_dll.CreateInstance
("AdvOrderClientControls.OrderDetail") as UserControl;
spCallProgressPanel.Children.Add(_orderDetailContol);
ResourceUtility на рис. 15 также демонстрирует различные функции вспомогательной программы, предназначенные для извлечения UIElement из документа XAML иXML, входящего в загруженные потоки и пакеты по умолчанию.
Производительность и другие вопросы
Я рассматривал Silverlight с точки зрения традиционного приложения уровня предприятия, затронув несколько вопросов, относящихся к архитектуре приложения. Реализация извещающих уведомлений с помощью сокетов Silverlight позволяет осуществлять поддержку таких бизнес-процессов, какие встречаются в центрах обработки вызовов. С появлением выпуска Internet Explorer 8.0, который, как планируется, будет обеспечивать шесть параллельных подключений HTTP на узел, реализация извещающих уведомлений по сети Интернет будет более соблазнительной при использовании дуплексной привязки WCF. Интеграция с данными и процессами LOB выполняется так же просто, как в традиционных настольных приложениях.
Это приведет к громадному росту производительности по сравнению с AJAX и другими платформами многофункциональных веб-приложений (RIA). Приложения Silverlight можно сделать безопасными, используя конечные точки WCF для проверки подлинности и авторизации, предоставляемые в последнем выпуске ASP.NET. Надеюсь, что это небольшое исследование разработки приложения LOB с помощью Silverlight сообщит вам импульс к использованию Silverlight не только для мультимедийных и рекламных задач.
Автор: Хану Коммалапати (Hanu Kommalapati)
Источник: http://msdn.microsoft.com/ru-ru/magazine/