经验教训

优化 A 大比例软件 + 服务应用程序

Udi Dahan

本文讨论:
  • WCF 和 3 层服务模型
  • 处理偶尔连接
  • 线程处理和可用性
  • 同步的域
本文涉及以下技术:
WCF、 软件 + 服务

内容

始终设置为日期
与 2 层的问题
WCF 和 3-层
可伸缩性问题
乐园中的问题
偶尔连接影响服务器
线程和可用性
客户端 WCF
对象图依赖项
同步的域
视图控制器的交互
线程安全的数据绑定
双向的 DataBinding
客户端的储存库
性能
经验教训

设计格式,交互式桌面的客户端软件从未更容易。 使用 Visual Studio、 Windows Presentation Foundation (WPF) 和复合应用程序指南为 WPF (Prism),比以往更好地安装开发人员。

遗憾的是,它不足以。

使用一个大型的丰富客户端软件 + 服务应用程序开发偶尔连接的硬盘方式发现我的团队多线程、 智能不发生死锁或客户端生成通过数据争用的垃圾回收时远简单。 这不在该的工具集的故障但。 我们发现这些工具需要使用非常特定的模式,以获得响应、 可靠的系统。

在本文中我突出显示我们遇到,一些问题并介绍了如何我们 overcame 它们。 希望您可以利用此信息避免我们错误并进行充分利用这些工具,生成您自己的软件 + 服务应用程序时。

始终设置为日期

第一个我们所面临的挑战之一是用户始终保持与所有其他用户所进行以及其他系统中发生的事件。 此实时的系统行为允许用户为它们发生大量的数据处理事件。 以前版本的系统允许进行搜索、 排序和筛选器,但用户就无法作用于该信息时。

用户体验设计过程中许多以前版本的系统中的该网格被替换 Messenger 类似弹出消息 ("toast") 通知重要事件的用户。 其他更改启用了用户设置不同的阈值的实体的属性的超出时, 弹出 Toast 邮件。 单击时, 通常包含的 Toast 链接的打开窗体向用户显示受影响的实体,一些与一个之前和之后的视图。

通过扩展我们必须与遇到的常规请求/响应模式的方法思考,时我们主要利益相关者中附带并提醒我们: 面的事件从世界上给用户实时此能力是我们的知识工作者以支持该协作的关键,我们在构建实时企业。

以是短,它是清除我们需要某种类型的发布 / 订阅功能。 试图只轮询中央的数据库的用户的几百个支持事件两秒用户可见性不起作用 (它破坏下大约 60 用户)。

之上它所有,我们的用户填充已移动,其中一些断开连接以及为它们移动进出 Wi-Fi 区域,重新连接每隔几秒,其他脱机工作的一小时或两个同时 telecommuting,和的某些执行大量脱机工作达一周。

我们不得不 juggle 偶尔连接,数据同步并发布 / 订阅所有在同一时间。 我们了解我们无法解决所有问题,客户端或服务器端但而不是因为在一侧的任何更改需要另一端的相应更改需要集成的方法。 此文章中, 我将介绍从服务器端使用转发到客户端开始此过程。

与 2 层的问题

我们的原始部署模型是传统的 2 物理) 层模型多个丰富客户端应用程序中。 我们使用此模型的困难从数据库通知出按,向客户端。

我们的一个选项是具有与通过存储过程的提交它们的事务之后将调用 Web 服务位于该数据库服务器完成数据库的所有交互。 它是通知什么以及参数被传递到存储过程的调用的存储过程的名称传递给它们的客户端的 Web 服务的责任。

不起作用时那么好。 使用此方法的两个关键问题一个逻辑,在其他物理。

逻辑的问题是单个的存储的过程未能调用由各种代码的含义不同的操作中的路径在不同的时间。 当存储过程的激活的客户端接收到通知时,它不总是清除什么,意味着和什么 Toast 要设置为用户的弹出。

物理问题是我们需要公开 Web 服务在数据库服务器上的将调用其自身 Web 服务每个客户端。 这是快速 vetoed 为 gaping 的安全漏洞,安全专业人员以及在规定审计是 (rightly 关注其 origins 无法一定跟踪的数据的实时做出决策的用户的。

WCF 和 3-层

若要解决与 2 层解决方案,我们意识到我们需要为更明确有关我们的服务合同逻辑的问题,说明运行该存储的过程和它的参数的名称是显式要什么,而不是为什么它发生。

如我们开始设计我们 Windows Communication Foundation (WCF) 服务合同,它成为清除没有足够只是集中适当的命名和我们的服务方法的范围。 我们需要是显式有关通知的合同,服务器回调到客户端的方法。 WCF 回调约定启用我们相同数量的关注的服务器到客户端邮件作为常规的服务合同。 (要阅读更多有关 WCF 的回调合同,请参阅摘自 10 月 2006 的 Juval Lowy 的文章" 需了解关于单向的调用,回调和事件.")

每个命令 / 事件对了,如下所示模型:

[ServiceContract(CallbackContract = 
  typeof(ISomethingContractCallback))] 
interface ISomethingContract {
  [OperationContract(IsOneWay = true)] 
  void DoSomething();
}

interface ISomethingContractCallback {
  [OperationContract(IsOneWay = true)] 
  void SomethingWasDone();
}

若要以便 WCF 服务回调到不调用当前方法的客户端我们必须在客户端的进程单独服务并将该过程部署到自己的服务器实质上将移动到 3 层部署。

尽管它建议我们放服务作为数据库,我们的数据库管理员而言,该服务可能需要离开的宝贵的资源从而 hurting 数据库的性能的相同。 我们无状态的 WCF 服务的单独层的额外的好处是我们可以轻松地扩展它到更多的计算机而无需任何与该数据库服务器 tinkering。

遗憾的是,不一样容易,因为我们认为。

可伸缩性问题

我们遇到两种类型的可伸缩性问题逻辑和物理,为之前。

逻辑的可伸缩性问题将表现本身作为系统获得更多的功能。 我们刚开始开发,我们必须只包含其上的所有方法在单个服务器合同和单个回调合同与其上的所有相应方法。 我们快速遇到过多的开发人员 Chefs 的砖墙壁 stirring 相同缓冲器。 自己服务 / 回调约定对移动每个命令 / 事件对后, 该问题已解决。 但解决方案还引入了另一个、 多恶劣的问题。

当将决定了多个服务 / 回调约定对,我们认为主要问题将会遇到合同的 unmanageable 数。 实际不转出到是许多问题,业务应用程序域之间模块性很缓解的。 问题是"对"。

一个命令可以通知的多种并通知的一种可能导致许多类型的命令的。 我们之前清除一对一映射是过于简单,并需要发展到多多映射。 不只是我们在系统中缩放的命令和通知的数量,我们需要调整它们之间的关系的数量。 它查找像受 WCF 回调约定的一对一表示未将能够使我们向前。

我们遇到一个实际的可伸缩性问题横我们 WCF 服务层时。 我们需要某种方法来共享的客户端回调约定订阅者列表。 如我们曾中提供的指导 Juval Lowey 2006 年 10 月 MSDN 杂志 》 文章我们创建包括出版物 / 子服务相关的基础结构片段。 我们发现单个 pub / 子服务没有能够处理需要向实际上 constituting 在 Publisher 中的瓶颈,到订阅服务器的通信的所有订户推从所有的发行商的通知的数量。 而且,不同的通知有不同的优先级,这难弯曲支持基础结构。

之后将来回主题几次,pub / 子服务我们提出的最佳结构是每个逻辑业务域具有 pub / 子结构的一段。 因为每个域也有的命令和 clumped 周围的通知集,提供为一个很好的全新边界。 此分区解决许多我们以前有时自动创建第一级的 pub / 子结构扩展优先级问题。

乐园中的问题

WCF 的优点之一是它对各种技术 (如 HTTP 和 TCP 上的多个绑定的支持。 我们最初选择工作使用 WS HTTP 以来大多数功能丰富,但不考虑该决策的所有含义。

如我们转出系统到我们的用户,所有内容似乎准备好,但的感觉了短所在。 后有关晚一小时我们开始"服务器已拒绝连接"从我们的服务台朋友们获取调用。

我们快速检查,并看到所有服务器都未设置,运行,但没有似乎执行很多。 CPU 使用情况是低、 相同的内存、 IO,和其他任何内容。 我们检查数据库的死锁和其他 nasties,但所有清除有,过。

因此,我们在调试模式下的另一个服务器进程启动并开始监视以查看它的形式。 不需要长时间是否显示本身出现此问题。

每隔 10 到 20 秒已发布到客户端的事件的线程将大约 30 秒挂起。 您可以假设这为我们的可伸缩性。 因为我们相当是接近推荐使用的线程池大小,我们支持的客户端的我们已耗尽线程。 此时,WCF 启动拒绝接受来自客户端的新连接。

我们可能认为我们的客户端导致阻止服务器的应用程序中有一个错误。

我们运行捕获为在特定的客户端计算机,导致了问题 (使用我们挑选出服务器日志中的地址)。 我们说这些计算机的用户,以查看是否在有遇到服务器问题的时间大致在应用程序中任何奇怪行为。 未报告任何在普通。 但它们进行有趣的观察值:"应用程序往往停留的方式 Outlook 执行我的 WiFi 连接剪切出时间附近的权利"。

偶尔连接影响服务器

我们启动从多个客户端和服务器计算机中收集日志,并反复看到相同的行为。 每当客户端脱机 30 秒会阻止调用它。

此计数器以将 IsOneWay 属性我们了解上运行该 OperationContract。 所有我们事件通知方法返回 void,没有任何理由服务器线程被阻塞。

为我们 drilled 到 WsHttpBinding 的行为的更深入我们开始了解单向操作合同中的工作方式。 服务器是通过调用其代理服务器上, 一个单向方法,如果该代理服务器以前连接,并且其缓存中有基本信道通知客户端对象,使得使用它们,并尝试通过 HTTP 调用客户端。 即使调用是单向,基础通道等到可以建立一个 HTTP 连接,并且可以发送 HTTP 请求。 它不等待 HTTP 响应。 遗憾的是,如果出于某种原因,客户端是脱机,HTTP 连接将放弃之前等待默认的 HTTP 超时时间 (30 秒)。

我们意识到我们无法解决此问题,需要查找将断开连接的客户端的表面可靠的替代绑定。 我们发现 Microsoft 消息队列 (MSMQ) 绑定中的答案,特别是 NetMsmqBinding。 幸运的是,使用 WCF,交换出另一个绑定不很了不起。

验证我们服务器线程已不获取阻塞个客户端连接的入和签出了时之后, 我们打开我们 sights 返回给客户端。

线程和可用性

在设计与请求/响应方式中的服务器交互的丰富客户端时线程安全不过多的我们一个问题。 我们开发了的服务器获得更多的功能,并成为更强大,它们以响应所花的时间量以及增长。 这已知当应用程序挂起,直到服务器响应时,会导致可用性问题。 常见的解决方案是,以便它异步调用服务器更改客户端。 此时,开发人员了解从服务器的回调都在后台线程上处理的时, 用户界面控件倾向于在其引发异常。 最后,代码已更改,使服务器回调将切换到前台线程,并再次一切正常。

是,在客户端在从多个服务器接收通知的无限数据流时, 这些模式不提供我们也。

由于在前台线程上处理到达客户端的每个通知,并且所有的用户界面交互也已完成前台线程,这两个任务将最终不断地抵御控件。 您可以实际看到不稳定跳在屏幕上,为您尝试将其移动的鼠标。 键入到文本框时, 字符会填写只是为不正常。

我注意到这时花费一些时间,在我们的负载测试实验室。 我记得查看显示弹出很高兴与我们的体系结构如何工作时的实时的所有各种通知客户端。 我鼠标单击该 toasts 之一中的链接,它保留获取粘滞为我移动它。 我是无线鼠标作为其电池骰子的方式的提醒的。 但鼠标是常规的有线的 USB 鼠标。

我 winced。 我们从一个发布仅两周。 我们功能的测试被传递,并负载测试未显示系统可以处理所有我们引发它。 唯一的问题已系统没有负载完全功能,可用性 nightmare 进行更好的说明。

客户端 WCF

前回到图形板我们的客户端上,我们看到的任何内容),可能会阻止紧急重写。 我们通过指导 MSDN 中如何在智能客户端环境中 (请参阅"使用 WCF pored 使用 Windows Communication Foundation 中编写智能客户端") 和编写更多证明概念代码比过之前,但以某种方式执行任何操作涉及所有基。 它看起来我们已阻塞死锁摇滚之间不可用的硬盘位置。

promising 的一种策略基于封装与 Windows 窗体同步环境 (引用) 方式封送即使如果它们访问在后台线程,它们处理回 UI 线程调用交互的安全控件。

由于到达客户端不是每个通知包括更新 UI,我们就能大量可用性中通过在后台线程上处理的大多数客户端的通知。 有时,通知涉及到更新 UI 时, 安全控件将确保在正确的线程上进行实际的用户界面呈现。 technologically,它查找类似实线解决方案。

将转发,并实现所需的安全控件和窗体后,我们花经过很好长负载测试在后台运行时执行功能测试在测试实验室中的时间。 我们必须采取客户端上的所有日志记录,并使异步也,以便我们可以保持高的日志级别不降低用户界面。

在此财务应用程序,多个 Traders 将协作上实现各种目标的佣金的风险并返回单个的投资组合有时协作在单个的贸易。 时我们模拟此行为的多个测试,域之一中也它们必须十分负返回。 而这并不一定意外本身 (让我们面临着,如果测试人员可以真正执行一个 trader 的工作,它们不会测试人员) 事实是这样不同做我们注意其他域的结果。 我们以前的体验开发此系统告诉我们我们 witnessed 某些异常时, 它就意味着我们编程中我们进行一些假设证明为 false。

筛选通过日志的多,我们所查找的当前中,一个运行时财务收益发生一个调节。 非常,这是所有太明确没有返回进入红色部分的一个清除点。 如我们曾通过围绕该点的日志条目来回我们方式时间,Nothing stood 出。 所有查找几乎像那样返回正时。

我们 DBA 的 old-time 的 UNIX 黑客之一匹配我的与某些 regex (正则表达式) 中,他找到一个小时核心差异。 因为我们已经 3 小时到,快速 conceded,他与一个 grin resurfaced 以后,45 分钟。

它是一个上下文开关,查看在最坏的可能时间和它最终导致数据争用。 一个线程设置的已为 (大约 90 万日元的贸易,并另已设置为 1 百万美元一次的一个属性。 遗憾的是,贸易最终在 1 百万日元或 $ 11,000 美元的状态。 该说明财务返回调节。

防止多个线程同时使用相同的对象不火箭 surgery。 每个线程只需要锁定对象之前使用它 (请参见 图 1 )。 这需要客户端代码以确保我们已锁定所有我们需要大量的全面传递。 然后,它需要大量测试,以解决出锁定超过我们需要我们带来的死锁。

图 1 锁定对象

//Thread 1
void UserWantsToDoSomethingToTrade(
    Guid tradeId, double value, Currency c) {
  Trade t = InMemoryStore.Get(tradeId);
  lock(t) {
    t.Value = value;
    t.Currency = c;
  }
}

//Thread 2
void ReceivedNotificationAboutTrade(
    Guid tradeId, TradeNotification tn) {
  Trade t = InMemoryStore.Get(tradeId);
  lock(t) {
    t.Value = tn.Value;
    t.Currency = tn.Currency;
  }
}

我们 (非常 reluctantly) 放弃的操作之一是数据绑定到用户可编辑视图的我们内存中的对象。 我们无法让用户锁定窗体被打开,因为,可以阻止后台线程等待该对象处理其他的通知的持续时间的对象。

使用 trepidation 的公平金额,我们将系统通过的测试,以前电池等待,不可避免的"其他内容"我们不知道的有关将敲打其英尺的项目。 再次。

根据我们的测试人员的 Traders 工作系统,查找像上述问题已被解决。 更深入和更复杂的方案是通过运行的更多的用户和更多类型的用户交互具有相同的投资项目组合,更改风险配置文件,执行假设的预测和历史记录的比较。 在测试真正 banged 系统从每个方向上,它保留。 半 disbelieving 这可能实际上是它,我们回好消息的业务。

他们认为我们没有。 我意味着,我们跟踪记录没有特别令人印象深刻点上。 120%延迟传递往往 erode 这样的信任。 但它们在新系统的 dire 需要这样的星期二我们滚到试用版。

并在星期四,我们回滚出。

对象图依赖项

一个测试运行的该盈利能力时非常不同其他测试运行,甚至财务初学者如开发人员和测试人员需要注意。 如果违反了多 delicate 投资和贸易规则,但那些没有即时或大规模的影响,初学者看它。 专家执行操作。

我不会进入 nitty gritty 财务规则此处,但技术问题我们的单个对象锁定策略是太简单的规则涉及多个对象。

是棘手。

看时我们调用的代码被锁定,并更新一个对象,, 该对象可以引发将由多个其他对象处理的事件。 这些对象的每个可以选择更新自身,并引发它自己此进程 percolating 出影响许多对象的事件。 当然,调用代码不知道这些 ripples 距离将转使这些对象将不已被锁定。 再一次,我们有多个线程更新锁没有相同的对象。

没有讨论的放弃事件和其他松散耦合的通信机制,以便我们可以有某些线程安全但不必重新实现所有我们复杂规则我们因此长正确日期) 的想法没有.NET 的优势了太多要注意。

它既不只是域对象。 控制器对象未能获取调用回也,更改其状态,太,禁用和启用工具栏按钮和弹出 Toast,菜单,您命名它。

我们需要一个单个的全局锁定,但无法使用的 UI 的存储进行我们慎这样的解决方案。 也是即使这样的锁存在,我们就必须检查以确保锁执行并相应地发布在系统中的每一行代码的问题,但每个单个维护可不只对软件此版本发布和之后的修补程序。

如果您做,damned 如果不,是 damned 的案例。

这就时我们发现同步的域。

同步的域

同步域以声明方式提供线程访问对象的自动的同步。 作为支持.NET 远程处理基础结构的一部分引入了此类。 开发人员希望以指示类是可以同步其对象的访问必须有类继承 ContextBoundObject 并将其标记与在 SynchronizationAttribute 如下所示:

[Synchronization]
public class MyController : ContextBoundObject {
  /// All access to objects of this type will be intercepted
  /// and a check will be performed that no other threads
  /// are currently in this object's synchronization domain.
}

按预期方式,所有此神奇功能提供一些性能开销都在对象创建和访问。 另一个令人讨厌 caveat 是同步域中所涉及的类不能具有泛型的方法或属性,虽然它可以调用和否则请使用从其他类的泛型。

到目前为止项目中, 当我们实际上会耗尽其他所有选项时我们为它一个快照。

我们计划有一个同步域的所有逻辑将都运行。 这意味着控制器对象、 域对象和客户端 WCF 对象将需要同步的域中。 实际上,必须在同步域之外的唯一对象是在 Windows 窗体本身、 它们的控件并任何其他 Visual GUI 元素。

有趣的事情我们发现是同步域中的不是所有对象都必须继承 ContextBoundObject 或已应用的 SynchronizationAttribute。 而,我们只需要为同步域的边界上的对象执行此操作。 这意味着所有域对象未能都保留以前一样,大的性能提升。

控制器类要求有点多加注意。

视图控制器的交互

在模型-视图-控制器 (MVC) 模式的我们使用,控制器交互与仅在 UI 线程上的视图。 于安全控件上述,在该控制器可以调用后台线程上的视图,这是不同的方法。

我们还知道从服务器接收到不是每个通知所需更新该的 UI,它是控制器对象做出该决定的。 因此,控制器需要处理在后台线程上的通知,并且如果更新 UI 是必需,将为负责切换线程。

小一点需要注意尽管,是始终以异步方式切换线程,否则为您可能会发生死锁您的系统或导致严重的性能问题 (从体验说)。

稍后,我们创建一个封装在线程处理并调用保持应用程序代码简单 (请参阅控制器基类 图 2 ).

图 2 控制器基本类

[Synchronization]
public class MyController : ContextBoundObject {
  // this method runs on the background thread
  public void HandleServerNotificationCorrectly() 
   {
  // RIGHT: switching threads asynchronously
  Invoker.BeginInvoke( () => CustomerView.Refresh() );

  // other code can continue to run here in the background

  // when this method completes, and the thread exits the
  // synchronization domain, the UI thread in the Refresh
  // method will be able enter this or any other synchronized object.
   }

   // this method runs on the background thread
   public void HandleServerNotificationIncorrectly()
   {
  // WRONG: switching threads synchronously
  Invoker.Invoke( () => CustomerView.Refresh() );

  // code here will NOT be run until Refresh is complete

  // DANGER! If Refresh tries to call into a controller or any other
  //         synchronized object, we will have a deadlock.
   }

   // have the main form injected into this property
   public ISynchronizeInvoke Invoker { get; set; }

   // the view we want to refresh on server notification
   public ICustomerView CustomerView { get; set; }
}

我们真正回,需要的一件事情尽管,是数据绑定。 可以几乎不说要做 MVC,是否您的模型是字符串、 double 和 ints 的集合。

线程安全的数据绑定

我们之前必须使用数据绑定的问题是我们的视图对象保留对允许更新相同的对象,因为已被后台线程更新 UI 线程模型对象的引用。 如我们开始将放在位置 ViewModel 模式,很多事情变得更加简单,只有独立的控件,这些对象的结构所做的所有差异。

重新将数据绑定然后,我们下一步是控制器对象将克隆传递到其视图像下面这样:

public class CustomerController : BaseController {
  // this method runs on the background thread
  public void CustomerOverdrawn(Customer c) {
    ICustomerOverdrawnView v = this.CreateCustomerOverdrawnView();

    v.Customer = c.Clone(); // always remember to clone

    this.CallOnUiThread( () => v.Show() );
  }
}

尽管此技术上工作,但没有几个问题它。 第一个问题是可维护性,如何未能我们确保所有开发人员记住克其域对象传递到视图之前? 第二个问题是更多技术域对象引用,因此只克隆一个不意味着其他在克隆相互。

以是短我们需要为深度的域对象克隆它们被传递到视图。 也是重要我们并没有移动任何一个这种复杂性到自己的视图。 我们的需要是为了创建泛型代理视图的在 CreateCustomerOverdrawnView,和该代理服务器检查所有的方法调用的并属性 setter 的域对象的参数执行深入的克隆,然后将该克隆传入视图本身。

有这么多的技术,您能够执行此代理,并且每个执行不同的操作。 某些使用面向方面的编程技术,其他人更直接挂钩但自己的策略不重要。 只需知道您需要创建一个代理。 在的代理中包括深入克隆方法和用于存放工作集克隆的随附的词典。 图 3 显示了我们的解决方案。

图 3 的视图中克隆对象

// dictionary of references from source objects to their clones
// so that we always return the same clone for the same source object.
private IDictionary<object, object> sourceToClone = 
  new Dictionary<object, object>();

// performs a deep clone of the given entity
public object Clone(object entity) {
  if (entity.GetType().IsValueType)
  return entity;

  if (entity is string)
  return (entity as string).Clone();

  if (entity is IEnumerable) {
    object list = 
      Activator.CreateInstance(entity.GetType()) as IEnumerable;
    MethodInfo addMethod = entity.GetType().GetMethod("Add");

    foreach (object o in (entity as IEnumerable))
      addMethod.Invoke(list, new object[] {Clone(o)});

    return list;
  }

  if (sourceToClone.ContainsKey(entity))
    return sourceToClone[entity];

  object result = Activator.CreateInstance(entity.GetType());
  sourceToClone[entity] = result;

  foreach(FieldInfo field in 
    entity.GetType().GetFields(BindingFlags.Instance | 
    BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | 
    BindingFlags.Public)) field.SetValue(result, 
    Clone(field.GetValue(entity)));

  return result;
}

此方法经过所有属性和根据需要复制它们在该对象的字段。 如果词典中找到对具有以前复制的对象的引用,它不会再次,克隆,但返回第一个克隆。 以这种方式,该方法将创建的给定图形对象的阻止视图在后台线程上使用的对象的访问的镜像。

所有这样做后,我们视图未能现在绑自由地定到给定的域对象。 我们可能不再指望是双向数据绑定的域对象自动更新绑定的视图。

双向的 DataBinding

在常规数据绑定方案,绑定对象发生更改时它将引发让知道它要刷新该视图的事件。 这确实有助于分离系统,如处理与服务器进行通信的部分不需要更新任何视图,这些更新域对象时。 我们新线程安全体系结构中客户端 WCF 对象没有更新的域对象所绑定到视图的同一个实例,因为我们丢失的一些双向数据绑定的优点。

了解在多线程环境中我们使双向数据绑定上放弃至关重要。 如果在后台线程更新域对象绑定到 UI,INotifyPropertyChanged (引用) 的行为将导致后台线程直接更新 UI 和应用程序崩溃。

控制器现在必须执行的知道哪种视图需要更新时。 是例如当用户有打开显示一个挂起的贸易的详细信息窗体,并在客户端接收通知对贸易,相关控制器应更新窗体。 下面是如何我们它最初:

public class TradeController : BaseController {
  public void Init() {
    Commands.Open<Trade>.Activated += (args => {
      TradeForm f = OpenTradeForm(args.Trade);

      args.Trade.Updated += this.CallOnUiThread(
        () =>  f.TradeUpdated(args.Trade)
      );
    });
  }
}

此代码通过打开窗体,并显示请求的贸易处理一个贸易的泛型的打开命令。 此外指定在更新贸易时窗体传递更新的贸易。

我们 well-pleased 使用此干净、 简单,和简单的代码。

这就是之前我们意识到它有一个错误中。

当用户打开其第二个商业 (或之后的任何交易) 任何以前的交易将被更新时,窗体将显示更新的交易,即使该用户不关心它不再。 我们需要更加小心在我们的回调的处置:

public class TradeController : BaseController {
  public void Init() {
    Commands.Open<Trade>.Activated += (args => {
      TradeForm f = OpenTradeForm(args.Trade);

      Delegate tradeUpdated = this.CallOnUiThread(
        () =>  f.TradeUpdated(args.Trade)
      );

      args.Trade.Updated += tradeUpdated;

      f.Closed += () => args.Trade -= tradeUpdated;
    });
  }
}

此处区别在于我们要保持上委托的引用,以便我们可以取消订阅此贸易更新窗体关闭时。 修复的错误。

除了为少一点。

此代码在单线程模式运行系统中正确但在我们多线程客户端的适应克隆的战争环境,引用不总是您的想法。

贸易对象 eventargs 中的来自某个位置,命令为特定的激活。 在 UI 线程上的用户激活命令作为用户执行某些操作的结果,在这种情况下双击在网格中的交易。 但如果在贸易显示在网格中,表示一个控制器必须为视图的并且进程中贸易将已被复制。

以是短后从服务器有关的贸易将不使用直接在网格中克隆对象,Updated 的事件在控制器订阅上面任何通知将永远不会获得引发。 在这种情况下,显示在贸易的详细信息窗体不会获取刷新。

内容是缺少该解决方案。

客户端的储存库

我们控制器需要某种方法来查找出实体的特定实例会更新时,但不依赖于对象引用。

每个实体具有标识符,而在客户端可以查询这些实体内存中的存储库,控制器可以使用它获取基于来自用户界面的标识符的实体权威的引用。

下面是我们贸易控制器看起来像使用存储库。

public class TradeController : BaseController {
  public void Init() {
    Commands.Open<Trade>.Activated += (args => {
      TradeForm f = OpenTradeForm(args.Trade);
      Delegate tradeUpdated = this.CallOnUiThread(
        (trade) =>  f.TradeUpdated(trade) );
      this.Repository<Trade>.When((t => t.Id == args.Trade.Id))
       .Subscribe(tradeUpdated);
      f.Closed += 
       () => this.Repository<Trade>.Unsubscribe(tradeUpdated);
    });
  }
}

我们控制器正在使用具有提供用户界面标识符订阅特定贸易实例上的更改的存储库。 填数游戏的最后一条只是将客户端 WCF 对象使用的相同的存储库更新客户端域对象中。

性能

将所有代码段放入位置,重写我们的客户端应用程序的重要组成部分之后, 引入支持框架,和通过我们性能测试,我们发现仍了有问题。

重负载很多对象必须更新在相同的时间大致大通知客户端处理会导致用户界面变得缓慢。 我们检测显示大通知,越长同步域已持有的后台线程在此期间用户无法执行要求控制器逻辑的任何操作。

我们尝试优化该的客户端代码,以便图表将较小采用克隆,较少时间,并且所有其他我们可能想重构域对象。 我们在客户端上的任何帮助。

这就时初级 Server 开发人员的一个分支最 (有点 hesitantly),备选我们可以更改服务器代码来发布多个通知消息。 而不是将所有更改的实体放在一封邮件,我们可以做每个邮件,或任何其他,一个实体可能甚至使其可配置。

并其进行世界上的所有差异。

因为之间处理一个通知和客户端上的后台线程 retreated 从同步的域下这允许获取中并执行一些操作在用户的身份在 UI 线程。 花有点长是较大的通知的处理一个客户端上一次的邮件无法完全可以接受。

经验教训

当我们启动出此项目的开发时,我们假定将像任何其他富客户机 / 数据库系统。 我们会遇到两了结构和技术挑战是大于任何其他我们会看到。 处理低级别的线程发出两个服务器,并了解如何偶尔连接客户端,影响可伸缩性,和维护的可接受级别的用户交互,同时避免死锁和争用条件,没有必须单击同时只需右处理整个的内容的很多部分。 所有支持的技术已存在,都在如何我们整理它们。

最后,如我们观看过我们的用户通过实时协作在各地,我们理解它无法任何其他方式。 其系统依赖请求的用户的公司排序,和只分组的信息将无法争用。 软件 + 服务、 偶尔连接的客户端和多演变意味着过更高的效率和盈利率组织能够使在飞跃。

我们的利益相关者是正确的。 能够面的事件从世界上向实时用户确实是以支持协作,实时的企业的知识工作者的关键。

完成项目后我是有点关心所有模式和我要提取的技术不都作为我在我中转换到多个线条的业务样式应用程序开发。 我已获得告诉我是 pleasantly 感到惊讶。

更多的个人注释我可以告诉您执行任何操作不会更好的开发人员的职业与业务线经理有关您的应用程序 raving,自己开发的更有趣。 试一试吧。

Udi Dahan 是 The Software Simplist、 MVP 和连接技术顾问使用 WCF、 WindowsWF 和"奥斯陆"。 他将提供咨询服务,擅长面向服务的、 可伸缩,和安全的.NET 体系结构设计中的培训、 mentoring,和高端体系结构。 Udi 可以联系到他的博客: www.UdiDahan.com.