Silverlight

Создание бизнес-приложений с помощью Silverlight, часть 1

Хану Коммалапати (Hanu Kommalapati)

В данной статье рассматриваются следующие вопросы.
  • Среда выполнения Silverlight
  • Асинхронное программирование Silverlight
  • Междоменные политики
  • Пример корпоративного приложения
В данной статье используются следующие технологии:
Silverlight 2

Загружаемый файл с кодом доступен в коллекции кода MSDN
Обзор кода в интерактивном режиме

Cодержание

Основы Silverlight: CoreCLR
Среда выполнения Silverlight
Сценарий приложения
Извещающие уведомления с помощью сервера сокета
Асинхронные циклы ввода/вывода
Модальные диалоговые окна в Silverlight
Реализация извещающих уведомлений
Междоменный доступ служб TCP
Междоменные политики с помощью служб TCP
Подводя итоги

Когда я недавно представил оперативный обзор по влиянию Silverlight на бизнес крупноq компании в Хьюстоне, прием оказался прохладным. Интересные демонстрационные версии, показывавшие DeepZoom, Picture-In-Picture и видео HD-качества и высококачественные анимации, должны были легко заинтересовать группу. Когда я опросил присутствовавших в аудитории о причинах их ограниченного интереса, стало ясно, что, хотя ослепительная графика была хороша, реальных рекомендаций по созданию ориентированных на данные бизнес-приложений (LOB) корпоративного качества с помощью Silverlight было доступно очень немного.

В наше время приложения корпоративного класса требуют безопасной доставки бизнес-информации через границы сетей, часто по Интернету, с применением интерфейса пользователя на основе ролей и усечения данных для бизнес-контекста. Применение Silverlight на клиенте и Microsoft .NET Framework 3.5 на сервере предоставляет отличные возможности для создания масштабируемых и безопасных бизнес-приложений. Облегченная среда выполнения Silverlight, работающая в «песочнице», предоставляет библиотеки платформы для интеграции со службами данных в служебном офисе. Чтобы создавать надежные приложения с помощью Silverlight, архитекторам и разработчикам нужно понимать программную модель Silverlight и функции его платформы в контексте реально существующих приложений.

Моя основная цель в данной статье заключается в том, чтобы взять бизнес-сценарий и построить приложение с нуля, иллюстрируя по дороге различные аспекты разработки Silverlight. Решение, о котором я буду говорить, – приложение центра обработки звонков, его логическая структура показана на рис. 1. В этой части я сконцентрируюсь на всплывающих на экране уведомлениях, асинхронной модели программирования, диалоговых оках Silverlight и реализации сервера междоменной политики TCP. В части 2 я поговорю о безопасности приложений, интеграции веб-служб, секционировании приложения и ряде других аспектов приложения.

fig01.gif

Рис. 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.

fig02.gif

Рис. 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 разработаны так, чтобы быть полностью непрозрачными для пользовательского кода.

fig03.gif

Рис. 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)  
    { 
    ... 
    }
   }

fig05.gif

Рис. 5. Шаблон асинхронного цикла сокета

В Реализации цикла получения, показанной на рис. 4, ReceiveAsync – это оболочка для повторно входящего метода ReceiveAsync(SockeAsyncEventArgs recvArgs), который поставит в очередь запрос на порте завершения ввода/вывода сокета. SocketAsyncEventArgs, представленный в .NET Framework 3.5 играет похожую роль в реализации сокета Silverlight и может быть повторно использован в ходе нескольких запросов, избегая смешивания с собираемым мусором. За извлечение сообщения, инициацию события обработки сообщения и установки в очередь следующего элемента для продолжения цикла будет отвечать процедура обратного вызова.

Чтобы обрабатывать случаи частичного получения сообщений, ReceiveCallback корректирует буфер перед установкой в очередь следующего вопроса. NetworkMessage заключается в экземпляр ReceiveArgs и передается внешнему обработчику событий для обработки полученного сообщения.

Буфер сбрасывается для каждого завершенного получения NetworkMessage, после копирования частичного сообщения, если такое есть, в начало буфера. Похожая схема используется на сервере, но практические реализации могут выиграть от кольцевых буферов.

Для реализации сценария «принятия вызова», необходимо создать расширяемую архитектуру сообщения, позволяющую сериализовывать и десериализовывать сообщения с произвольным содержанием, не требуя переписки логики сериализации для каждого нового сообщения.

fig06.gif

Рис. 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;
    }
  }
}

fig08.gif

Рис. 8. Регистрация на сервере центра обработки звонков находится в процессе

fig08.gif

Рис. 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="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://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="&lt;Empty Message&gt;" />
             <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);  

fig14.gif

Рис. 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. Этот подход предотвратит замешивание в сбор мусора на сервере с высокими требованиями по масштабируемости.

Подводя итоги

Применение извещающих уведомлений является важным аспектом приложения сервера центра обработки звонков и позволяет применять процесс всплывающих сообщений. Silverlight делает реализацию всплывающих сообщений гораздо проще, чем AJAX или подобные платформы. Поскольку серверная и клиентская модели программирования похожи, мне удалось достигнуть некоторой степени повторного использования на уровне исходного кода. В случае центра обработки звонков, я могу использовать определения сообщений и получать абстракции буфера как в серверных так и в клиентских реализациях.

Я реализую интеграцию веб-служб на месте, безопасности, интеграцию «облачных» служб и разбиение приложений на разделы во второй части этой статьи. Я надеюсь, что мои усилия нацелены в направлении тех сценариев, с которыми сталкиваются мои читатели и я жду ваших отзывов.

Я хотел бы поблагодарить Дэйва Мюррэя (Dave Murray) и Шэйн Десеранно (Shane DeSeranno) из корпорации Майкрософт за руководство в области внутренней реализации сокетов Silverlight, а также Роберта Брукса (Robert Brooks), эксперта по доменам на центрах обработки звонков за обсуждение всплывающих сообщений.

Хану Коммалапати (Hanu Kommalapati) – советник по стратегии платформ в корпорации Майкрософт, занимающийся сейчас консультированием корпоративных клиентов в области создания масштабируемых бизнес-приложений на платформах Silverlight и Azure Services.