Silverlight

使用 Silverlight 构建业务线企业级应用程序,第 1 部分

Hanu Kommalapati

本文将介绍以下内容:

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

代码下载位置:MSDN 代码库
在线浏览代码

目录

Silverlight 基础:CoreCLR
Silverlight 运行时
应用程序方案
使用套接字服务器推送通知
异步 I/O 循环
Silverlight 中的模式对话框
推送通知实现
TCP 服务的跨域访问
TCP 服务的跨域策略
总结

最近我为休斯敦一家大型公司的管理层演示了 Silverlight 的商业潜质,但反响并不强烈。此次演示展现了深度缩放、画中画、高清晰视频和高清晰动画,本来应该很容易吸引受众。通过调查,我了解到虽然图片十分精彩,但对于在实际中使用 Silverlight 构建以数据为中心的企业级业务线 (LOB) 应用程序,演示的指导意义很小,因此受众的兴趣不大。

目前,企业级应用程序需要跨网络边界(通常要通过 Internet)安全递送 LOB 信息,而且还需要使用适应业务环境的基于角色的 UI 和数据裁剪。在客户端运行 Silverlight 并在服务器上运行 Microsoft .NET Framework 3.5 就能出色地构建此类可伸缩且安全的 LOB 应用程序。在沙箱中运行的轻型 Silverlight 运行时为与后台管理数据服务集成提供了框架库。为了使用 Silverlight 构建可靠的应用程序,架构师和开发人员需要理解 Silverlight 编程模型及其在实际应用程序环境中的框架功能。

本文的主要目的是讨论 LOB 方案并从头构建一个应用程序,全程解说 Silverlight 开发的方方面面。我将讨论的解决方案是一个呼叫中心应用程序,其逻辑体系结构如图 1 所示。在本期内容中,我的侧重点是屏幕弹出通知、异步编程模型、Silverlight 对话框和跨域 TCP 策略服务器的实现。在第 2 部分中,我将讨论应用程序安全性、Web 服务集成、应用程序分区等。

fig01.gif

图 1 Silverlight 呼叫中心逻辑体系结构

Silverlight 基础:CoreCLR

开始之前,让我们先温习一下 Silverlight 的基础知识。我先深入介绍 Silverlight 运行时,以便您可以更好地理解 Silverlight 能够提供的功能。CoreCLR 是 Silverlight 使用的虚拟机。它类似于为 .NET Framework 2.0 及更高版本提供强大功能的 CLR,也包含相似的类型加载和垃圾收集 (GC) 系统。

CoreCLR 采用非常简单的代码访问安全性 (CAS) 模型——它比桌面 CLR 更简单,Silverlight 只需要在应用程序级别强制实施安全策略。这是因为作为独立于平台的 Web 客户端,它不能依靠任何特殊的现行企业或机器策略,而且也不应该允许用户更改现有策略。但也有一些例外情况,比如 OpenFileDialog 和 IsolatedStorage(存储配额更改),在这种情况下,Silverlight 需要用户的明确同意才能打破沙箱的默认规则集。OpenFileDialog 用于访问文件系统,而 IsolatedStorage 的作用是访问名义上隔离的存储并提高存储配额。

对于桌面应用程序,每个可执行程序都加载一个 CLR 副本,并且操作系统进程仅包含一个应用程序。每个应用程序都有一个系统域、一个共享域、一个默认域和多个显式创建的 AppDomains(请参阅“JIT 和运行:深入 .NET Framework 内核了解 CLR 如何创建运行时对象”)。CoreCLR 中也存在相似的域模型。就 Silverlight 而言,几个应用程序(可能来自不同的域)将在同一个操作系统进程中运行。

在 Internet Explorer 8.0 中,每个选项卡都在其自身独立的进程中运行;因此在同一个选项卡内托管的所有 Silverlight 应用程序都将在相同的 CoreCLR 实例上下文中运行,如图 2 所示。由于每个应用程序可能来自不同的源域,所以出于安全原因考虑,每个应用程序都将被加载到其自身的 AppDomain 中。可以说 CoreCLR 实例的数量与当前托管 Silverlight 应用程序的选项卡数量相等。

与桌面 CLR 类似,每个 AppDomain 均将获得自已的静态变量池。每个特定于域的池将在 AppDomain 启动进程期间初始化。

fig02.gif

图 2 每个 Silverlight 应用程序都将在其自身的 AppDomain 中运行

Silverlight 应用程序不能创建其自身的自定义应用程序域,该功能已保留供内部使用。有关 CoreCLR 的更详细讨论,请参考下面来自 CLR 团队的“CLR 全面透彻解析”专栏:“使用 CoreCLR 编写 Silverlight”和“Silverlight 2 中的安全性”。

Silverlight 运行时

Silverlight 针对多种需要不同程度框架和库支持的应用程序而设计。例如,简单的应用程序可能只需要播放几个字节的音频文件就能帮助读出某个字典网站上单词的发音,或者只需要显示一个标题栏广告。但是,企业级 LOB 应用程序则需要考虑安全性、数据隐私、状态管理、与其他应用程序和服务的集成以及分析支持等等。与此同时,Silverlight 还需要保持较小的运行时,以便在较慢的链路上通过 Internet 进行部署时不会出现问题。

这些需求彼此间有冲突,但 Silverlight 团队通过将框架划分为图 2 中所示的层级来化解矛盾。CoreCLR + Silverlight 运行时合称为“插件”,所有用户都可以在运行应用程序之前安装该插件。对于大多数以消费者为中心的应用程序来说,该插件已足够满足需要。如果某个应用程序需要使用 SDK 库(WCF 集成或 DLR 运行时,如 Iron Ruby)或自定义库,那它必须将这些组件封装到 XAP 程序包中,以便使 Silverlight 能够了解如何在运行期间解析所需的类型(请参阅本期领先技术专栏了解更多有关 XAP 的信息)。

Silverlight 运行时(不算 CoreCLR 库,如 agcore.dll 和 coreclr.dll)大小约 4MB,它包含应用程序开发人员所需的必要库。其中包括以下基本库:mscorlib.dll、System.dll、System.Net.dll、System.Xml.dll 和 System.Runtime.Serialization.dll。支持浏览器插件的运行时通常安装在 C:\Program Files\Microsoft Silverlight\2.0.30930.0\ 目录下。当计算机下载并安装 Silverlight 时将在 Web 浏览会话中创建该目录。

在同一台机器上构建和测试应用程序的开发人员会使用运行时的两个副本:一个通过插件安装,另一个通过 SDK 安装。后者位于 C:\Program Files\Microsoft SDKs\Silverlight\v2.0\Reference Assemblies 目录下。Visual Studio 模板将使用此副本作为编译时引用列表的一部分。

沙箱会阻止 Silverlight 应用程序与大多数本地资源的交互,这一点对任何典型的 Web 应用程序都适用。默认情况下,Silverlight 应用程序无法访问文件系统(除独立存储外)、无法建立套接字连接、无法与连接到计算机的设备交互,也不能安装软件组件。这显然会对能够在 Silverlight 平台上构建的应用程序类型有所限制。但是,Silverlight 具备开发数据驱动的企业级 LOB 应用程序所需的全部功能,这类应用程序需要与后端业务进程和服务集成。

应用程序方案

我将在这里构建的 LOB 应用程序演示了一种第三方呼叫控制体系结构,该结构中的一台中央服务器接入专用交换分机 (PBX) 基础结构以集中控制电话。因为我关心的是作为 UI 表层的 Silverlight,所以对电话集成就一带而过。我将使用简单的呼叫模拟器生成传入呼叫事件。该模拟器将把代表呼叫的数据包加入呼叫管理器的等待队列,这将触发本项目的核心进程。

我的假想方案要求呼叫中心应用程序以独立于平台的方式在 Web 浏览器内部运行,但同时还需要能够提供足以媲美桌面应用程序的丰富用户交互。Silverlight 是自然而然的选择,因为 ActiveX 在非 Windows 客户端环境中并不流行。

让我们看一下该应用程序的体系结构。您需要实现推送通知、事件集成、业务服务集成、缓存、安全性以及与云服务的集成。

推送通知 这是一项必需的功能,因为系统需要捕获传入呼叫事件并传递由呼叫方输入的交互式语音响应 (IVR) 数据以完成“屏幕弹出通知”,或使用传入呼叫信息填充 UI 屏幕。此外,还应该为用户提供接听或拒绝呼叫的机会。

事件流 在典型的 Web 应用程序中,因为 Web 服务器会执行大量业务流程,所以它了解业务事件的所有信息。但就富 Internet 应用程序 (RIA) 而言,业务流程的实现会在 Web 浏览器中运行的应用程序和实现业务 Web 服务的服务器之间共享。这意味着在 Silverlight 应用程序中生成的业务事件和技术事件需要通过一组特定的 Web 服务发送到服务器。

本解决方案案例中的业务事件示例发生在用户 (rep) 拒绝呼叫 ("rep rejected the call") 或接受呼叫 ("rep accepted the call") 时。典型的技术事件是“Connection to Call Manager TCP server failed”(连接到 Call Manager TCP 服务器失败)和“Web service exception”(Web 服务异常)。

业务服务集成 与任何其他 LOB 应用程序一样,此呼叫中心解决方案需要与存储在关系数据库中的数据集成。我将使用 Web 服务作为集成载体。

缓存 为了获得更好的用户体验,我将在本地内存和磁盘上缓存信息。缓存的信息可能包括用于指明用户提示程序脚本的 XML 文件和其他不会经常更改的引用数据。

安全应用程序 安全性是此类应用程序的基本要求。安全性包括身份验证、授权、工作和休息期间的数据隐私性,以及基于用户配置文件的数据裁剪。

与云服务集成 与基于云的基本服务(如存储服务)的集成需要特殊的服务器端基础结构。这样就能针对责任性和服务级别严密地监控和调节云服务的使用。

我将在本文的第 2 部分中介绍与业务服务的集成、应用程序安全性、Web 服务的跨域策略和应用程序分区。

使用套接字服务器推送通知

屏幕弹出是呼叫中心应用程序将呼叫上下文从电话基础结构传递到代理屏幕的基本要求之一。传递的呼叫上下文可能包括呼叫客户讲述(适用 IVR 系统)或键入的任何信息。

通知可以采用以下两种方式发送给浏览器中的 Silverlight 应用程序:通过客户端轮询或服务器推送。轮询非常容易实现,但它可能不会是呼叫中心方案的最佳选择,因为在该方案中电话事件和客户端应用程序之间的状态同步需要非常精准。正是出于此原因,我将使用 Silverlight 套接字实现推送通知。

Silverlight 的重要功能之一是与 TCP 套接字的通信。出于安全考虑,Silverlight 只允许连接 4502 到 4532 之间的服务器端口。这是沙箱中实现的众多安全策略之一。另一个重要的沙箱策略是 Silverlight 不能是侦听器,因此它不能接受入站套接字连接。基于上述原因,我将创建侦听端口 4530 的套接字服务器,并维护连接池,其中每个连接代表一个活动呼叫中心用户。

Silverlight 套接字运行时还在服务器上强制对所有套接字连接实施跨域可选策略。当 Silverlight 应用程序代码试图打开到许可端口号上某个 IP 端点的连接时(该过程对用户代码不透明),运行时将使用端口号 943 建立到具有相同 IP 地址的 IP 端点的连接。此端口号已硬编码到 Silverlight 的实现中,无法通过应用程序配置或由应用程序开发人员更改。

图 1 显示策略服务器在体系结构中的位置。当调用 Socket.ConnectAsync 时,消息流序列如图 3 所示。按照设计,消息 2、3 和 4 对用户代码完全不透明。

fig03.gif

图 3 Silverlight 运行时自动为套接字连接请求跨域策略

我需要在与呼叫管理器服务器相同的 IP 地址上建立一个策略服务器。我可以在单个操作系统进程中建立这两个服务器,但为简单起见,我将在两个独立的控制台程序中实现它们。这些控制台程序可以轻松地转换为 Windows 服务,并能针对故障转移识别群集,以提供可靠性和可用性。

异步 I/O 循环

.NET Framework 3.5 为套接字引入新的异步编程 API;它们是以 Async() 结尾的方法。我将在服务器上使用的方法是 Socket.AcceptAsync、Socket.SendAsync 和 Socket.ReceiveAsync。异步方法通过使用 I/O 完成端口为高吞吐量服务器应用程序执行了优化,并通过可重用的 SocketAsyncEventArgs 类有效地接收和发送缓冲区管理。

由于不允许 Silverlight 创建 TCP 侦听器,所以其 Socket 类仅支持 ConnectAsync、SendAsync 和 ReceiveAsync。Silverlight 仅支持异步编程模型,这一点不仅适用于套接字 API,而且适用于所有网络交互。

由于我将同时在服务器和客户端上使用异步编程模型,所以让我们先熟悉一些设计模式。一种重复设计模式是 I/O 循环,它适用于所有 Async 操作。首先,我将介绍典型的同步执行套接字 accept 循环,如下所示:

_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);
 }

同步 accept 非常直观且易于编程和维护,但该实现无法真正具备服务器可伸缩性,因为每个客户端连接都有专用的线程。如果连接很频繁,则几个连接就容易导致该实现达到峰值。

为了使 Silverlight 能够在浏览器运行时环境中正常工作,它应该尽可能少干预资源。前面所示的 "socket accept" 伪代码中的所有调用都会阻塞其执行线程,并因此对可伸缩性产生负面影响。因此,Silverlight 在阻塞调用方面限制很多,实际上它只允许与网络资源进行异步交互。异步循环需要调整思维模式,您可以假想它是一个不可见的信箱,并且它任何时候都包含至少一个请求以便使循环能够工作。

图 4 显示了一个 receive 循环(代码下载中包含更完整的实现)。在该实现中并没有类似于前面同步套接字 accept 伪代码中所示的 while (true) 无限循环编程结构。习惯使用这种编程结构对于 Silverlight 开发人员来说至关重要。为了使 receive 循环能够在已接收并处理完消息后继续接收数据,队列中至少应包含一个对已连接套接字关联 I/O 完成端口的请求。典型的 async 循环如图 5 所示,它适用于 ConnectAsync、ReceiveAsync 和 SendAsync。在使用 .NET Framework 3.5 的服务器上可以将 AcceptAsync 添加到此列表。

图 4 使用 Silverlight 套接字的 Async Send/Receive 循环

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 中所示的 receive 循环中,ReceiveAsync 是可重入方法 ReceiveAsync(SocketAsyncEventArgs recvArgs) 的包装函数,此方法将对套接字 I/O 完成端口的请求进行排队。.NET Framework 3.5 中引入的 SocketAsyncEventArgs 在 Silverlight 套接字实现中具有相似的作用,它可以在多个请求之间重复使用,从而避免垃圾收集改动。回调例程负责提取消息、触发消息处理事件并将下一个 receive 项加入队列以便继续循环。

为了处理接收不完整消息的情况,ReceiveCallback 会在将另一个请求加入队列之前调整缓冲区。NetworkMessage 包装在 ReceiveArgs 的实例中,并被传递给外部事件处理程序以便处理接收到的消息。

将部分消息(如果有)复制到缓冲区开头后,每次完成 NetworkMessage 接收后都会重置缓冲区。服务器上也使用相似的设计,但实际实现可从循环缓冲区中受益。

为实现“呼叫接受”方案,您需要创建一个可扩展的消息体系结构,该结构应使您能够序列化和反序列化包含任意内容的消息,而无需为每则新消息重写序列化逻辑。

fig06.gif

图 6 序列化 NetworkMessage 类型的布局

此消息体系结构非常简单:每个 NetworkMessage 的子对象在实例化时使用合适的 MessageAction 声明其签名。由于源代码级别的兼容性,NetworkMessage.Serialize 和 Deserialize 实现可以在 Silverlight 和 .NET Framework 3.5(服务器上)上工作。序列化的消息布局如图 6 所示。

不必在消息开始处插入长度信息,您可以使用 "begin" 和 "end" 标记和适宜的转义序列。将长度信息编码到消息中能够大大简化缓冲区处理。

每个序列化消息的前四个字节都将包含它后面序列化对象的字节数。Silverlight 支持位于 Silverlight SDK 的 System.Xml.dll 内的 XmlSerializer。代码下载中包含有序列化代码。您会注意到它对子类(如 RegisterMessage)或其他消息(包括 UnregisterMessage 和 AcceptMessage)没有任何直接依赖关系。一系列 XmlInclude 注释将帮助序列化程序在序列化子类时正确地解析 .NET 类型。

NetworkMessage.Serlialize 和 Deserialize 的使用如图 4 中 ReceiveCallback 和 SendAsync 所示。在 receive 循环中,实际消息处理由 NetworkClient.OnReceive 事件附加的事件处理程序完成。我可以在 CallNetworkConnection 内处理消息,但如绑定接收处理程序来处理消息,则有助于通过在设计阶段将处理程序与 CallNetworkConnection 分离提高可扩展性。

图 7 显示了 Silverlight 应用程序 RootVisual,它将启动 CallNetworkClient(如图 4 所示)。所有的 Silverlight 控件都连接单个 UI 线程,而且只有当代码在该 UI 线程的上下文中执行时才能更新 UI。Silverlight 的异步编程模型在线程池的工作线程上执行网络访问代码和处理程序。所有 FrameworkElement 派生类(如 Control、Border、Panel 和大多数 UI 元素)都会继承 Dispatcher 属性(来自 DispatcherObject),它可以在 UI 线程上执行代码。

图 7 中,MessageAction.RegisterResponse 会通过匿名委派使用代理的呼叫中心转移详细信息更新 UI。委派执行后得到的更新 UI 如图 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 中的模式对话框

当呼叫中心用户登录时,会要求他通过向呼叫中心服务器注册开始转移。服务器上的注册过程将保存按照用户号码索引的会话。此会话将用于后续屏幕弹出和其他通知。呼叫中心应用程序注册过程的屏幕转换如图 89 所示。我将使用显示网络发送进度的模式对话框。典型的企业 LOB 应用程序使用弹出式对话框,可随意选择模式或非模式对话框。由于 Silverlight SDK 中没有内置的 DialogBox,您将看到如何在 Silverlight 中开发一个这样的对话框供在本应用程序中使用。

在 Silverlight 之前,没有什么简单的方法能够构建模式对话框,这是因为没有防止键盘事件进入 UI 的简便方法。鼠标交互可以通过设置 UserControl.IsTestVisible = false 间接禁用。从 RC0 开始,设置 Control.IsEnabled = false 可以防止 UI 控件接收任何键盘或鼠标事件。我将使用 System.Windows.Controls.Primitives.Popup 在现有控件顶部显示对话框 UI。

图 10 显示了基本的 SLDialogBox 控件及抽象方法 GetControlTree、WireHandlers 和 WireUI。这些方法将被子类重写,如图 11 所示。Primitives.Popup 需要一个控件实例,该实例不能是 Popup 所要附加的控件树的一部分。在图 10 的代码中,ShowDialog(true) 方法将以递归方式禁用整个控件树,因此其中包含的所有控件都将无法接收任何鼠标和键盘事件。由于我的弹出式对话框必须是交互式对话框,所以 Popup.Child 应在新的控件实例中设置。子类中的 GetControlTree 实现将充当控件工厂,并提供适合对话框 UI 需求的新用户控件实例。

图 10 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 皮肤

//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 以实例化编译到应用程序包中的 Silverlight UserControl,也可以使用 XamlReader.LoadControl 通过可扩展应用程序标记语言 (XAML) 文件创建控件。通常,在已编译处理程序能够在运行期间连接的皮肤中,可以轻松实现对话框。图 11 显示包含 btAccept 和 btReject 按钮的 XAML 皮肤。如果在 Microsoft Expression Studio 或 Visual Studio 设计任务之后将类属性 (<userControl class="AdvCallCenter.NotifyCallPopup"…>…</UserControl>) 保留在 XAML 中,LoadControl 方法将抛出一个异常。必须删除所有 UI 事件处理程序属性才能使用 LoadControl 成功执行解析。

要创建皮肤,您可以将 Silverlight UserControl 添加到项目中、在 Expression 中设计它并从 XAML 文件删除附加到控件的 "class" 属性和事件处理程序名称(如果有)。Click 处理程序可以是子弹出类的一部分,如图 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 程序包中,可以将其作为 XML 文件添加到 Silverlight 项目,然后将扩展名更改为 XAML。扩展名为 XAML 的文件的默认生成操作是将其编译到应用程序 DLL 中。由于我希望将这些文件作为文本文件打包,所以应该从“Properties”(属性)窗口设置以下属性:

  • 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,并会在模拟器将消息加入 ProcessMessage 实现内的 _callQueue 队列时得到通知。通过利用我在前面讨论过的弹出式对话框,客户端将显示 Accept/Reject(接受/拒绝)通知,如图 14 所示。下面是负责显示图 8 中所示实际通知对话框的代码行:

ClientGlobals.notifyCallPopup.ShowDialog(true);  

fig14.gif

图 14 传入呼叫通知

TCP 服务的跨域访问

真正的企业级 LOB 应用程序需要与多种服务托管环境集成,这一点与媒体和广告展示应用程序不同。例如,呼叫中心应用程序在网站中托管(advcallclientweb 托管于 localhost:1041)、使用位于另一个域 (localhost:4230) 中的状态套接字服务器完成屏幕弹出,并通过域 (localhost:1043) 中托管的服务获得 LOB 数据。它还将使用另一个域传输规范数据。

默认情况下,Silverlight 沙箱不允许通过网络访问除源域——advcallclientweb (localhost:1041)——以外的任何其他域。当检测到这样的网络访问时,Silverlight 运行时会检查由目标域建立的可选策略。下面是需要支持客户端跨域策略请求的典型服务托管方案列表:

  • 在云中托管的服务
  • 在服务进程中托管的 Web 服务
  • 在 IIS 或其他 Web 服务器中托管的 Web 服务
  • HTTP 资源,如 XAML 标记和 XAP 程序包
  • 在服务进程中托管的 TCP 服务

尽管为 IIS 中托管的 HTTP 资源和 Web 服务端点实现跨域策略并不复杂,但其他情况下可能需要了解有关策略请求/响应语义的知识。在本文的第 1 部分中,我将简要实现 TCP 屏幕弹出服务器所需的策略基础结构,即图 1 中的 Call Manager。其他跨域方案将在本文的第 2 部分中讨论。

TCP 服务的跨域策略

Silverlight 中的任何 TCP 服务访问都将被视为跨域请求,而服务器需要在相同的 IP 地址上实现一个绑定到端口 943 的 TCP 侦听器。图 3 中所示的策略服务器就是为该目的实现的侦听器。该服务器为流出声明性策略实现请求/响应过程,在允许客户端上的网络堆栈连接屏幕弹出服务器(图 3 中的 Call Manager)之前,Silverlight 运行时需要使用这些策略。

为简单起见,我将在一个控制台应用程序中托管呼叫管理器服务器。在实际实现时,该控制台应用程序可以方便地转换为 Windows 服务。图 3 显示与策略服务器的典型交互;Silverlight 运行时将从端口 943 连接该服务器并发送策略请求,该请求包含一行文本:"<policy-file-request/>"。

基于 XML 的策略产生图 3 中所示的方案。套接字资源可以指定一组端口,范围从 4502 到 4534。限制成该范围是为了将攻击目标降至最少,从而缓解防火墙配置中偶然漏洞可能带来的风险。因为呼叫中心服务器(图 1 中的 Call Manager)在端口 4530 上侦听,所以套接字资源可以按照如下方式配置:

<access-policy>
   <policy>
     <allow-from> list of URIs</allow-from>
     <grant-to> <socket-resource port="4530" protocol="tcp"/></grant-to>
  </policy>     
</access-policy>

您还可以通过指定 port="4502–4534" 将 <socket-resource> 配置为允许所有许可的端口号。

为节省时间,我将重用呼叫管理器服务器的代码实现策略服务器。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 中所示的代码示例在多个 TCP accept 间重复使用 SocketAsyncEventArgs。为使其能够工作,e.AcceptSocket 应该在 AcceptAsyncCallback 中设置为 null。此方法可以防止服务器出现 GC 改动,从而满足高可扩展性需求。

总结

推送通知的实现是呼叫中心应用程序的重要元素,也是弹出屏幕的基础。与 AJAX 或类似框架相比,Silverlight 使屏幕弹出窗口的实现简单了许多。由于服务器编程和客户端编程模型相似,因此我可以重复使用某些源代码。在呼叫中心中,我可以同时在服务器和客户端实现中使用消息定义和接收缓冲区抽象。

我将在本系列文章的第 2 部分中实现内部部署的 Web 服务集成、安全性、云服务集成和应用程序分区。我希望这些工作能够对您遇到的某些 LOB 方案有所启迪,并期待您的反馈意见。

衷心感谢 Microsoft 的 Dave Murray 和 Shane DeSeranno 为 Silverlight 套接字实现细节提供指导,并感谢呼叫中心领域专家 Robert Brooks 就屏幕弹出窗口给出的意见。

Hanu Kommalapati 是 Microsoft 的平台战略顾问,目前他为企业级客户提供在 Silverlight 和 Azure 服务平台上构建可伸缩业务线应用程序方面的咨询。