Silverlight

生成行处业务企业应用程序使用 Silverlight,Part 2

Hanu Kommalapati

本文讨论:

  • Silverlight 的运行时环境
  • Silverlight 异步编程
  • 跨域策略
  • 示例企业应用程序
本文涉及以下技术:
Silverlight 2

代码下载可从 MSDN 代码库
浏览代码联机

内容

与业务服务的集成
服务调用
同步的服务调用
邮件实体转换
服务调用后的 Silverlight 状态更改
跨域策略
跨域策略的 Web 服务托管之外的 IIS
IIS 内部托管的服务的跨域策略
应用程序安全性
应用程序分区
工作效率和高级

在此系列的第一个的部分我引入呼叫中心方案,并通过使用异步 TCP 套接字 Silverlight 支持的在连接套接字显示一个屏幕填充 (屏幕 pop) 实现 (请参阅"生成行处业务企业应用程序使用 Silverlight,第 1 部分").

屏幕弹出实现通过模拟调用拾取从内部队列调用推送到通过以前接受的套接字连接缓存在泛型列表在服务器上的通知的调度程序。这里我将结束通过实现应用程序安全、 集成和业务服务和 Web 服务和应用程序分区的实现跨域策略。呼叫中心应用程序的逻辑结构如 图 1 所示。身份验证服务将该业务服务、 ICallService 和 IUserProfile 时实用程序服务中实现,将会实现该业务服务项目,顾名思义。

fig01.gif

图 1 Silverlight 呼叫中心的逻辑结构

即使在关系图显示事件流实用程序服务的时间,可下载的演示中不包括此功能。事件捕获服务功能的实现将类似于业务服务实现。但是,不严重错误的业务事件可以缓存到独立的存储和转储到在批模式下的服务器。我将开始与业务服务的实现讨论并结束与应用程序分区。

与业务服务的集成

与服务集成的业务线 (LOB) 应用程序的重要方面并 Silverlight 将提供足够的组件,用于访问基于 Web 的资源和服务。HttpWebRequest、 WebClient 和 Windows Communication Foundation (WCF) 代理服务器基础结构是基于 HTTP 的交互的常用的网络组件的一些。此文章中, 我将使用 WCF 服务与后端业务流程集成。

大部分我们使用与后端数据源集成应用程序开发过程的 Web 服务 ; 使用 Silverlight WCF Web 服务访问不是更不同是应用传统程序 (如 ASP.NET,Windows Presentation Foundation (WPF) 或 Windows 窗体应用程序。差异是绑定支持和异步编程模型。basicHttpBinding 和 PollingDuplexHttpBinding,Silverlight 将只支持。请注意 HttpBinding 是最可互操作的绑定。为此我将为所有的集成,本文中使用它。

PollingDuplexHttpBinding 允许回调合同以 Push Notifications over HTTP 的使用。我的呼叫中心可以使用此绑定的屏幕弹出通知。但是,实现要求的 HTTP 连接,从而独占两个同时 HTTP 连接允许浏览器如 Internet Explorer 7.0 之一的在服务器上的缓存。这可能导致性能问题,如所有 Web 内容都需要通过一个连接序列化。Internet Explorer 8.0 允许每个域的六个并发连接,并将解决这些性能问题。(使用 PollingDuplexHttpBinding 的强制通知可能是为将来的项目主题广泛的可用 Internet Explorer 8.0 时)。

返回到应用程序。当代理接受一个呼叫时,屏幕弹出过程填充呼叫方信息屏幕在本例的调用方订单详细信息。呼叫方信息应包含唯一地标识后端数据库顺序的必要信息。对于此演示方案将假设订单编号说交互式语音响应 (IVR) 系统中。Silverlight 应用程序将调用 WCF Web 服务订单编号作为唯一标识符。服务合同定义和实现如 图 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 实现。 为简明起见,我会将不使用在业务实体的任何数据库,但将内存中的列表对象用于存储客户、 订单和用户对象。 (未显示此代码下载但提供) 的在 DataUtil 类封装访问这些内存中的列表对象。

fig03.gif

图 3 的安全问题的代理脚本

Silverlight 消耗的 WCF 服务终结点需要访问 ASP.NET 管道,和因此需要 CallService 实现上的 AspNetCompatibilityRequirements 属性。 <servicehostingenvironment/>此项在 Web.config 文件中设置匹配。

如前面提到,Silverlight 仅支持 basicHttpBinding 和 PollingDuplexHttpBinding。 如果您使用 WCF 服务的 Visual Studio 模板,它将配置终结点绑定到的之前,必须将手动更改为 basicHttpBinding Silverlight 可以为代理生成添加服务引用的 wsHttpBinding。 ASP.NET 承载兼容性更改和绑定的更改将自动将执行自动如果 CallService.svc 添加到 AdvBusinessServices 项目使用启用了 Silverlight 的 WCF 服务 Visual Studio 模板。

服务调用

具有实现 Silverlight 可调用服务,现在很时间创建服务代理,并使用它们来在用户界面绑定到后端服务实现。 您可以只用于 WCF 服务的代理服务器可靠地使用生成服务引用 | 在 Visual Studio 中的添加服务引用序列。 在我的演示在代理服务器是到命名空间 CallBusinessProxy 生成的。 Silverlight 将只允许异步调用,网络资源,并服务调用也不例外。 当客户调用进入时, Silverlight 客户端将侦听通知,并显示一个接受 / 拒绝对话框。

一旦调用被代理接受,过程中的下, 一步是调用 Web 服务检索对应于调用这种情况的代理脚本。 对于此演示,我将仅使用一个脚本 图 3 中显示。 显示的脚本包含问候语以及安全问题的列表。 代理将确保向前移动与协助之前解答了的问题的最小数量。

代理脚本检索通过访问此种 ICallService.get­AgentScript() 提供与输入订单号。 与由 Silverlight Web 服务堆栈执行异步编程模型一致,GetAgentScript() 进行 CallServiceClient.BeginGetAgentScript()。 进行服务调用,时,您需要提供一个回调处理程序 get­AgentScriptCallback,如 图 4 所示。

图 4 服务调用和 Silverlight UI 更改

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 UI 线程上运行的调用和排队异步请求,并立即返回到下一个语句。 将由于代理脚本没有必须等待直到您缓存之前触发回调用,脚本和数据其绑定到 UI。

成功完成服务的调用触发器 GetAgentScriptCallback,它检索代理脚本,填充全局的变量并通过数据绑定到适当的用户界面元素的代理脚本调整 UI。 时调整 UI,在 GetAgentScriptCallback 将确保更新它通过 Dispatcher.CheckAccess() 的使用在 UI 线程上。

UIElement.Dispatcher.CheckAccess() 将比较工作线程的 UI 线程 ID,并将返回如果两个线程相同,则该参数为 True ; 否则,它返回 False。 get­AgentScriptCallback 执行工作线程时 (实际上,因为这将始终执行辅助线程上可以只是调用 Dispatcher.BeginInvoke),CheckAccess() 将返回 False,并且在用户界面会更新通过调度通过 Dispatcher.Invoke() 匿名委托。

同步的服务调用

由于 Silverlight 网络环境的异步特性,它是几乎不可能使一个异步服务对 UI 线程调用,并等待其完成的更改基于调用的结果应用程序状态的目的。 在 图 4 ,_notifyCallPopup_OnAccept 需要检索订单的详细信息输出消息转换为一个客户端实体并将其保存到一个全局变量以线程安全的方式。 要完成此,一个可能被想编写处理程序代码,如下所示:

CallServiceClient client = new CallServiceClient();
client.GetOrderDetailsAsync(orderNumber);
this._orderDetailDownloadHandle.WaitOne();
//do something with the results

但当它点击 this._orderDetailDownloadHandle.WaitOne() 语句时,此代码将冻结应用程序。 这是因为在 WaitOne() 语句阻止任何发出的邮件接收从其他线程在 UI 线程。 相反,您可以安排工作线程执行服务调用,等待调用完成,完成服务输出,在辅助线程上完整的张贴内容处理。 此方法如 图 5 所示。 若无意使用阻止 UI 线程中的呼叫我包装在自定义的 SLManualResetEvent ManualResetEvent 并为 UI 线程调用 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,但可以绑定可以在 UI 线程上执行的处理程序。 此模式允许多个服务执行以并行调用,并等待完成之前进行进一步处理聚合结果,请使用 SLManualResetEvent.WaitOne() 的所有调用。

邮件实体转换

在 GetAgentScriptCallback 还转换输出消息实体 (也称为 DataContracts) 从服务代表客户端使用语义的一个客户端的实体。 是例如服务器端邮件实体的设计可能不关心数据绑定而特别 multiuse 特性服务的密切注意将有提供广泛的使用,不仅呼叫中心。

此外,是一个很好的做法,不必具有与邮件实体紧密离合器,因为客户端的控件中不会对邮件实体。 方法转换为客户端实体的邮件实体不向 Silverlight,只适用,但想避免设计时紧密耦合时通常适用任何 Web 服务使用者。

我决定将实体转换器的实现非常简单,没有 exotic 嵌套泛型、 lambda 表达式或取反值的控件的容器。 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 的一致性,并且必须是单例,以避免垃圾回收改动。

进行相应的服务调用时,我保留重复使用同一个实例。 我还可以与下面的类定义为服务交互所需大输入的邮件 (这通常是事务性服务调用的情况) 创建 ServiOutputEntityTranslator:

public abstract class ServiOutputEntityTranslator
{
  public abstract object ToServiceOutputEntity(ClientEntity  
                                                      clientEntity);
}

如果发现上述的返回值,是函数的"对象",因为我不控制邮件实体 (我可能,此演示中但不是在实际情况下) 的基类。 将相应的转换器实现类型安全。 将为演示的简单性,我不要保存任何数据回在的服务器以便此演示不会包括任何转换器用于将客户端实体转换为邮件实体。

服务调用后的 Silverlight 状态更改

Silverlight Visual 状态更改才能执行通过代码执行 UI 线程上。 因为该服务的异步执行始终返回结果上调用回调处理程序,该处理程序将是正确的位置对该应用程序的 Visual 或非 Visual 状态进行更改。

如果有多个服务试图以异步方式修改共享的状态,应在非可视化状态更改交换以线程安全的方式。 它始终建议之前修改 UI,以检查 Deployment.current.Dispatcher.CheckAccess()。

跨域策略

与媒体应用程序和应用程序显示横幅广告,不同真正的企业级 LOB 应用程序需要与各种服务的承载环境集成。 是例如则调用引用在相应的中心应用程序是典型的企业应用程序的。 Web 站点上承载该应用程序从不同的域访问状态套接字服务器的访问 LOB 数据和它的屏幕弹出、 基于 WCF 的 Web 服务可能会下载其他的 XAP 程序包 (压缩 Silverlight 部署程序包)。 它将使用另一个域的传输检测数据。

Silverlight 沙箱不默认情况下允许网络访问原始域之外的其他任何域与您的 advcallclientweb 看到 图 1 中上一步。 Silverlight 运行库检查自愿中策略当应用程序访问原始域之外的其他任何域。 下面是需要支持客户端的跨域策略请求的服务承载方案的典型列表:

  • 在服务进程 (或简单的控制台应用程序) 中承载的 Web 服务
  • Web 服务位于 IIS 或其他 Web 服务器
  • 在服务进程 (或控制台应用程序) 中承载的 TCP 服务

我将讨论跨域策略实现 TCP 服务上个月,因此将重点宿主在自定义进程,并在 IIS 的 Web 服务。

虽然简单实施 IIS 中承载的 Web 服务端点的跨域策略,但在其他情况下将要求策略的要求和响应的特性的知识。

跨域策略的 Web 服务托管之外的 IIS

为有效的状态管理,可能有一个可能需要在 IIS 外部操作系统进程中承载服务的情况。 对这样的 WCF 服务的跨域访问,过程需要在 HTTP 端点的根目录的主机策略。 当跨域 Web 服务调用时,Silverlight 将会获得 clientaccesspolicy.xml 请求的 HTTP。 如果 IIS 中承载服务,client­accesspolicy.xml 文件可以被复制到 Web 站点的根目录中,并 IIS 将执行服务文件中的其余部分。 本地计算机上承载自定义,http://localhost:<port>/clientaccesspolicy.xml 应是有效的 URL。

由于呼叫中心演示不使用任何自定义的托管的 Web 服务,我将使用控制台应用程序中简单的 TimeService 说明这些概念。 控制台将公开 representational 状态传输 (REST) 终结点使用 Microsoft.NET Framework 3.5 的新的 REST 等功能。 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 属性这意味着不会包装该响应的 BodyStyle.bare 默认值。

服务实现非常简单 ; 它只是传输在 Silverlight 客户端请求的响应 clientaccesspolicy.xml。 策略文件名称可以是您自己选择的并且可以调用它您喜欢的任何。 策略服务的实现如 图 7 所示。

现在,我们必须配置为 REST 样式提供的 HTTP 请求 IPolicyService。 控制台应用程序 (ConsoleWebServices) app.config 如 图 8 所示。 有几个软件有关特殊配置需要注意: ConsoleWebServices.IPolicyServer 终结点的绑定必须被设置为 webHttpBinding。 此外,IPolicyService 终结点行为应配置与 WebHttpBehavior 配置文件中所示。 在 PolicyService 基址应被设置为根 URL (如在 http://localhost:3045/),并且端点地址应保留为空 (如。

图 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 配置将类似于 IIS 对应的 URL 中。 是例如 HTTP 可能看起来类似于下面的默认上的 IIS 承载 TimeService 终结点的 URL: 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 文件复制到 Web 服务的承载该站点的根目录。 如 图 1 中所看到,Silverlight 应用程序将位于 adv­callclientweb (localhost:1041) 上,并从 AdvBusinessServices (localhost:1043) 访问业务服务。 Silverlight 运行库需要部署在 adv­BusinessServices Web 站点的根目录,与 图 9 所示的代码的 clientaccesspolicy.xml。

图 9 Clientaccesspolicy.xml IIS 托管 Web 服务

<?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 Web 站点模板,并稍后添加 WCF 终结点,测试 Web 服务器将映射到项目的名称在虚拟目录 (如"/ AdvBusinessServices")。 这应更改为"/"项目的属性页中,以便在 clientaccesspolicy.xml 可以提供从根目录。 如果您不更改此,在 clientaccesspolicy.xml 不将在该的根,并且在访问该服务时 Silverlight 应用程序还会使服务器错误。 请注意这将不是创建使用 WCF Web 服务项目模板的网站的一个问题。

使用 PasswordBox 图 10 Login 控件

<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 应用程序的关键要求之一是身份验证 ; 才能调用中心代理开始差值他将进行身份验证的用户 ID 和密码。 在 ASP.NET Web 应用程序,这可以轻松地将通过利用成员资格提供程序和服务器端 ASP.NET 登录控件。 在 Silverlight 中, 有两种方法强制执行身份验证: 身份验证,外部和内部的身份验证。

外部身份验证非常简单且为类似于 ASP.NET 应用程序的身份验证实现。 使用此方法,显示 Silverlight 应用程序之前身份验证将发生在基于 ASP.NET 的网页中。 Silverlight 应用程序加载或通过自定义 Web 服务调用 (以提取身份验证状态信息) 应用程序后被加载之前验证上下文传送到 Silverlight 应用程序通过 InitParams 参数。

Silverlight 应用程序是更大的 / HTML 基于 ASP.NET 的系统的一部分时,此方法将有其位置。 但是,如果在其中 Silverlight 是应用程序的主驱动程序的情况下,很自然执行 Silverlight 中的身份验证。 我将使用 Silverlight 2 PasswordBox 控件捕获密码,并使用 ASP.NET AuthenticationService WCF 终结点来验证用户的凭据进行身份验证。 AuthenticationService、 ProfileService,和 RoleService 是新的一部分 namespace—System.Web.ApplicationServices—which 是.NET Framework 3.5 的新。 图 10 显示了为此目的创建的登录控件的 XAML。 登录控件调用 ASP.NET AuthenticationService.LoginAsync() 使用用户 ID 和输入的密码。

fig11.gif

图 11 登录自定义 Silverlight 控件

的呼叫中心, 图 11 中, 所示的登录屏幕不复杂,但此演示的作用。 我将实现处理程序处理在控件内 LoginCompleted 事件,以便它是独立显示无效的登录的邮件和密码重置 dialogues 的复杂的实现。 一个成功的登录时事件 OnSuccessfulLogin 将会触发通知父控件 (Application.RootVisual 在这种情况下) 以显示第一个应用程序屏幕填充用户信息。

位于内部 Silverlight 主页 LoginCompleted (ctrlLoginView_OnSuccessfulLogin) 处理程序将调用配置文件如 图 12 所示,业务 Services 网站上承载服务。 默认情况下的 AuthenticationService 未映射到任何.svc 终结点,将因此,我将映射.svc 文件在实际的实现如下所示:

<!-- AuthenticationService.svc -->
<%@ ServiceHost Language="C#" Service="System.Web.ApplicationServices.  
    AuthenticationService" %>

图 12 使用状况内,Page.xaml Login.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 调用 Web 服务。 像所有 AJAX 调用服务 AuthenticationService 服务需要访问 ASP.NET 运行库环境。 我提供此访问由 <system.servicemodel> 节点正下方的设置。 为了使身份验证服务会调用由 Silverlight 登录进程 (或由 AJAX 调用),web.config 必须能将设置根据中说明为" 如何: 启用 WCF 身份验证服务." 服务将自动配置用于 Silverlight 如果在创建使用 Silverlight 类别中启用了 Silverlight 的 WCF 服务模板。

图 13 显示了重要元素编辑的配置所必需的身份验证服务。 除了在服务配置,我还替换存储身份验证信息的 aspnetdb SQL Server 配置设置。 Machine.config 定义需要 asp­netdb.mdf 嵌入到网站的 App _ Data 目录的 LocalSqlServer 设置。 此配置设置会删除默认设置,并指向附加到 SQL Server 实例的 aspnetdb。 可以轻松地更改该将指向在单独的计算机上运行的数据库实例。

图 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>

若要保留的登录控件封装并保持离合器与父控件的设计时松散,登录过程成功完成的是由触发 OnSuccessfulLogin 事件传递的。 在 application.RootVisual (这是页类) 将执行显示第一个屏幕,在成功登录时所需的业务流程。 成功的登录名是该的 registrationView 如 图 12 userProfile_GetUserCompleted 方法所示后显示在第一个屏幕。 此视图显示之前,我将通过调用 CallBusinessProxy.UserProfileClient.GetUserAsync() 检索用户信息。 请注意异步服务调用类似于我稍后将讨论该业务服务集成。

应注意以前的配置不能使用安全套接字层 (SSL),; 您必须修改生产系统的生成时使用 SSL。

fig14.gif

图 14 OrderDetails.xaml 控件填充的订单明细

应用程序分区

导致 Silverlight 应用程序启动时间的因素之一是程序包的初始的大小。 XAP 程序包的大小,准则是不同页面粗细为 Web 应用程序的。 带宽是一个有限的资源。 Web 应用程序在严格的响应时间需要则请特别注意 Silverlight 应用程序启动时间。

除了处理时间花费在显示第一个 UserControl 之前应用程序包的大小都有直接影响应用程序的此重要质量。 为了提高启动速度,您必须避免单一 XAP 文件可以增长到数万兆字节的复杂的应用程序的大小。

Silverlight 应用程序可以被划分成 XAP 文件的集合 ; 单独的 DLL ; 或单独的 XML 文件、 图像和具有识别的 MIME 类型的任何其他类型。 在呼叫中心方案说明精确的应用程序分区,我将部署 OrderDetail Silverlight 控件作为单独 DLL (AdvOrderClientControls.dll) 一起 AdvCallCenterClient.xap 到 ClientBin 目录 AdvCallClientWeb 项目的 (请参阅上一步 图 1 )。

DLL 将下载 preemptively 工作线程上当代理接受传入的呼叫时。 您曾在 图 4 —ThreadPool.Queue­UserWorkItem (ExecuteControlDown­load) 的调用) 负责此。 一旦调用方回答安全问题,我将使用反射创建 OrderDetail 控件的实例并在屏幕上显示它之前将其添加到控件树。 图 14 包含订单详细信息填充显示 OrderDetail.xaml 控件加载到控件树。

DLL 包含控件部署到同一网站作为呼叫中心客户端即属于同一的应用程序的 DLL 的典型的 OrderDetail 因此我不会遇到任何跨域问题在这种情况下。 但是,此可能不是与服务,因为 Silverlight 应用程序可以访问部署在包括本地文件夹的多个域和体系结构关系图所示群中的服务 (再次,回头 图 1 )。

ExecuteControlDownload 方法 (请参见 图 4 ) 在后台工作线程上运行,并使用 WebClient 类下载该 DLL。 WebClient,默认,假定下载的源域中发生,并因此只使用相对 URI。

OrderDetailControlDownloadCallback 处理程序接收 DLL 流,并创建程序集使用 ResourceUtility.GetAssembly() 图 15 所示。 因为程序集的创建必须在 UI 线程上发生的我将发送该 GetAssembly() 的程序集全局变量到 UI 线程 (线程安全) 分配:

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 函数内的资源的代码。 UI 线程有机会创建程序集之前,这样的代码将过早处理的已下载的流。 我将使用反射创建 OrderDetail 用户控件的实例并将其添加到面板如下所示:

_orderDetailContol = ClientGlobals.advOrderControls_dll.CreateInstance
                  ("AdvOrderClientControls.OrderDetail") as UserControl;
spCallProgressPanel.Children.Add(_orderDetailContol);

ResourceUtility 中 图 15 此外显示了各种实用工具函数,以便从 XAML 和 XML 文档已下载的流和默认程序包中提取 UIElement。

工作效率和高级

我已了解了 Silverlight 从传统的企业应用程序角度接触几个应用程序的体系结构方面。 Silverlight 套接字的强制通知的实现是一个 enabler 的 LOB 应用场景如呼叫中心。 随着即将发布的 Internet Explorer 8.0 这必定包含每个主机的六个并发 HTTP 连接,使用双工 WCF 绑定时通过 Internet 的强制通知实现将更强大。 与 LOB 数据和流程集成就那样简单,因为它是传统桌面应用程序中。

这将是巨大的生产力提升 AJAX 与其他 Rich Internet 应用程序 (RIA) 平台进行比较时。 Silverlight 应用程序可以使用 WCF 身份验证和授权端点由 ASP.NET 提供的最新版本中得到保护。 我希望使用 Silverlight 的 LOB 应用程序开发此小研究将引导您超过媒体和广告方案的 Silverlight。

Hanu Kommalapati 是一个平台策略顾问 Microsoft,和此角色他建议在构建 Silverlight 和 Azure 服务平台上的可缩放的业务线应用程序的企业客户。