Silverlight

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

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

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

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

Cодержание

Интеграция с бизнес-службами
Вызов служб
Синхронизированные вызовы служб
Преобразование объектов сообщения
Изменение состояния Silverlight после вызовов служб
Междоменные политики
Междоменные политики для веб-служб, размещенных вне IIS
Междоменные политики для служб, размещенных в IIS
Безопасность приложений
Разбиение приложений на разделы
Производительность и другие вопросы

В первом выпуске этой серии я познакомил читателей со вариантом моделирования центра обработки вызовов и показал реализацию вывода данных на экран (всплывающие на экране сообщения) с помощью соединения через сокеты, в которой использовались асинхронные сокеты TCP, поддерживаемые Silverlight (см. «Создание бизнес-приложений уовня предприятия с помощью Silverlight, часть 1»).

Вывод всплывающего на экране сообщения был реализован имитацией диспетчера вызовов, который выбирал вызов из внутренней очереди и передавал уведомления через предварительно разрешенное подключение сокета, кэшированное в обобщенном списке на сервере. В данной статье будет выполнена реализация безопасности приложения, интеграция с бизнес-службами, реализация междоменных политик для веб-служб и разбиение приложения на разделы. Логическая архитектура приложения для центра обработки вызовов показана на рис. 1. Служба проверки подлинности будет реализована в специальной службе, в то время как бизнес-службы, ICallService и IUserProfile, будут реализованы в рамках проекта бизнес-службы, в соответствии с названием.

fig01.gif

Рис. 1. Логическая архитектура центра обработки звонков Silverlight

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

Интеграция с бизнес-службами

Интеграция со службами является одним из важных аспектов бизнес-приложения (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.

fig03.gif

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

fig11.gif

Рис. 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="https://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.

fig14.gif

Рис. 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) — советник по стратегии платформ в корпорации Майкрософт, занимающийся консультированием корпоративных клиентов в области создания масштабируемых бизнес-приложений на платформах Silverlight и Azure Services.