烧水器事件簿 II

烧水器事件簿 II

 

Written by Allen Lee

 

缘起

年前研究.NET事件机制时我写了一篇《烧水器事件簿》,然而,那篇文章存在一个很大的问题,就是Proton的逻辑并不直观。后来,Microsoft发布了Windows Workflow Foundation,我就在想,如果用Windows Workflow Foundation重写Proton,情况会否有所改善?如果有,能有多大的改善?在重写的过程中,原有的代码中哪些可以重用?哪些需要做出修改?两年后的今天,我终于有机会提笔探个究竟了。

在这篇文章里,我将会探讨下列几个问题:

  1. 状态机工作流的开发;
  2. 工作流实例和外界的通信;
  3. 工作流实例的承载(hosting);
  4. 工作流实例的持久化与状态管理。

 

烧水器状态机工作流

无疑问,无论是烧水器事件,抑或是应运而生的Proton,都是以烧水器为中心的,于是,烧水器状态便顺理成章地成为Proton的重中之重了。

烧水器的状态不外乎就两个:空闲状态和工作状态。刚买回来的时候,它的状态是空闲的,自第一个用户开始,它就不断地在两个状态之间切换:工作状态、空闲状态、工作状态、空闲状态……直到它因为老化或者损坏而不能工作为止。

下面我们来看看如何用Visual Studio 2008为烧水器创建状态机工作流描绘这个逻辑。首先,创建一个State Machine Workflow Library项目:

图 1 - 创建烧水器工作流库

接着,在工作流设计器上添加烧水器的两个状态:BoilerIdleState和BoilerWorkingState,为这两个状态各添加一个EventDrivenActivity,并分别命名为StartBoiling和CompleteBoiling,然后设置状态的转换:

图 2 - 设计烧水器工作流

其中,BoilerIdleState被设为初始状态(Initial State)。或许你已经发现了,这个状态机没有完成状态(Completed State),正如烧水器自买回来的那一刻起就不会有所谓的"用完",一般所说的"用完"只是"空闲"的一个别称,只要烧水器还能使用,我们就会一直用下去,同样的道理,烧水器状态机模拟了这个过程,这就是为什么它没有完成状态。

当烧水器处于空闲状态时,它实际上是在等别人来用,用行话说,就是监听外部的请求事件,于是,我们需要在StartBoiling里添加一个HandleExternalEventActivity,并命名为HandleBoilerRequest:

图 3 - 设计BoilerIdleState里的工作流

而当烧水器接到某个用户的请求时,它就会开始工作,根据《烧水器事件簿》里Paul他们的实验结果,烧水器工作的时间最多为30分钟,30分钟之后它将会通告工作完成,并进入空闲状态。要模拟烧水器的30分钟工作过程,DelayActivity是最合适不过了,而30分钟后的工作完成的通告就非CallExternalMetodActivity莫属了。在BoilerWorkingState里添加DelayActivity和CallExternalMethodActivity,并分别命名为Boiling和NotifyBoilerIdle。

图 4 - 设计BoilerWorkingState里的工作流

至此,我们已经勾画出Proton的核心逻辑了,然而,或者你已经注意到了,HandleBoilerRequest和NotifyBoilerIdle的右上角都有一个红色的叹号,它的出现意味着这些活动缺少一些必要的设置,而这正是下一节要处理的问题。

 

烧水器服务

顾图2,我们在烧水器工作流里定义了两个状态:空闲状态和工作状态。当烧水器接到用户的使用请求时,它会从空闲状态转到工作状态;而当烧水器从工作状态转到空闲状态时,它会向用户发出通知。在这两个状态转换的过程里,烧水器工作流和用户发生了交互,那么,这些交互是如何做到的呢?

答案是Windows Workflow Foundation提供的本地通讯服务(Local Communication Services),它使得工作流实例和宿主应用程序之间的交互成为可能。之前我们分别在StartBoiling和CompleteBoiling里放置的HandleBoilerRequest和NotifyBoilerIdle就是为此而做的准备。然而,要使这两个世界连接起来,我们还需要定义一个充当"通讯标准"的接口,这个接口必须打上ExternalDataExchangeAttribute,里面定义的方法可以关联到CallExternalMetodActivity,而事件则可以关联到HandleExternalEventActivity,值得提醒的是,充当事件数据的类必须继承自ExternalDataEventArgs类,并打上SerializableAttribute。下面是IBoilerService接口的定义:

代码 1 - 烧水器服务接口

不难看出,NotifyBoilerIdle方法将会关联到NotifyBoilerIdle;而BoilerRequest事件则会关联到HandleBoilerRequest。选中StartBoiling里的HandleBoilerRequest,并在属性窗口里设置InterfaceType和EventName这两个属性:

图 5 - 设置HandleBoilerRequest

接着选中CompleteBoiling里的NotifyBoilerIdle,并在属性窗口里设置InterfaceType和MethodName这两个属性:

图 6 - 设置NotifyBoilerIdle

还记得HandleBoilerRequest和NotifyBoilerIdle的右上角都有一个红色的叹号吗?当你做好关联后,这个叹号就会消失了。

假如我们要在多处使用HandleBoilerRequest和NotifyBoilerIdle,但有不希望每次都重复乏味的手动关联,那么可以考虑使用wca.exe生成Communication Activity。wca.exe会自动搜寻指定的程序集里打上ExternalDataExchangeAttribute的接口,它会为接口里的每个事件创建一个HandleExternalEventActivity的派生类,为接口里的每个方法创建一个CallExternalMetodActivity的派生类。

图 7 - 使用wca.exe生成Communication Activity

接着,我们来实现烧水器服务:

代码 2 - 烧水器服务实现

需要说明的是,BoilerService必须打上SerializableAttribute,里面定义的NotifyBoilerIdle方法和BoilerRequest事件是给烧水器工作流使用的,而BoilerIdle事件和NotifyBoilerRequest方法则是给宿主应用程序使用的。

至此,我们已经为烧水器工作流,以及它和宿主应用程序之间的交互提供了基础条件,是时候让它们动起来了。

 

搭建程序主体

先,创建一个Windows 应用程序:

图 8 - 创建宿主应用程序

并添加Windows Workflow Foundation和BoilerWorkflowLibrary的引用:

图 9 - 添加引用

接着,布置一下Proton的主界面:

图 10 - 简陋的用户界面

Proton的用户界面简陋非常,皆因本文的侧重点并非丰富的用户交互,故一切从简,以免喧宾夺主。

Proton启动的时候需要初始化用户队列、烧水器工作流和用户界面上面的一些元素。我打算重用《烧水器事件簿》里的Enrollee类、EnrolleeQueue类和EnrolleePriority枚举,其中Enrollee类需要稍稍修改,去掉不再需要的代码:

代码 3 - Enrollee类

下面来看看初始化烧水器工作流的代码:

代码 4 - 初始化烧水器工作流

宿主应用程序和工作流实例之间的通讯是通过本地通讯服务来实现的,在这里就是我们之前创建的烧水器服务,但我们不能直接把它关联到工作流运行时,而是通过ExternalDataExchangeService这个中介来关联。换句话说,我们把ExternalDataExchangeService关联到工作流引擎,然后把BoilerService关联到ExternalDataExchangeService。一切就绪后就启动工作流运行时,然后创建并启动烧水器工作流。

当用户点击Boil按钮时,Proton将会禁用这个按钮,防止重复点击多次,从用户队列里抽取第一个用户,设置用户界面上的相关元素,然后就调用烧水器服务发送使用请求:

代码 5 - 开始烧水

当烧水器使用完毕后(水开了),Proton将会通过消息框发出通知,并更新用户界面上的相关元素:

代码 6 - 订阅烧水器空闲通知

最后,你可以抓住窗口关闭的机会释放工作流运行时所占用的资源:

代码 7 - 资源清理

至此,Proton已经可以运行起来了,但每当烧水器进入工作状态时,由于DelayActivity的缘故,烧水器工作流的实例就要盘踞于内存等待30分钟之久,这对于本地Windows应用程序似乎没什么大不了,但如果我想用 .NET Remoting/WCF或者ASP.NET把它开发成网络上的应用并部署在服务器上,内存的浪费就会变得不容忽视了,而工作流持久化服务正是为了解决这类问题而来的。

 

配置工作流持久化服务

Windows Workflow Foundation自带了一个工作流持久化服务的实现,它可以把工作流实例持久化到SQL Server里。在开始之前,请检查你的装备是否齐全:

  • SQL Server 2005 Express Edition
  • SQL Server Management Studio Express
  • SqlPersistenceService_Logic.sql
  • SqlPersistenceService_Schema.sql

打开SQL Server Management Studio Express并连接数据库引擎:

图 11 - 连接数据库引擎

在SQL Server Management Studio Express里创建一个数据库,并命名为SqlPersistenceStore:

图 12 - 创建新的数据库

依次执行装备列表里的SqlPersistenceService_Logic.sql和SqlPersistenceService_Schema.sql,它们位于Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN文件夹里,执行的时候请确认选中SqlPersistenceStore数据库:

图 13 - 确认目标数据库是SqlPersistenceStore

这两个文件分别在SqlPersistenceStore数据库里创建工作流持久化服务所需的表和存储过程:

图 14 - 工作流持久化服务所需的表和存储过程

配置好数据库后就可以向工作流运行时添加工作流持久化服务了:

代码 8 - 向工作流运行时添加工作流持久化服务

它应该在工作流运行时的创建和启动之间执行:

代码 9 - AttachPersistenceService的位置

这样,当烧水器工作流没有展开实质性的工作时,工作流持久化服务就会让它在数据库里休息一会。

 

衍生问题

长期看,任何一个烧水器都难逃"退休"的命运,我们没有理由要求所买的烧水器可以代代相传,因此,当烧水器"退休"时,工作流持久化服务在数据库里储存的数据也应该删除。工作流持久化服务会在两种情况下删除这些数据,第一种情况是工作流顺利完成所有工作,但我们之前定义的烧水器工作流是一个没有完成状态的状态机,只要宿主没有退出,或者使用了工作流持久化服务,它就有可能永远运行下去,那么我们是否要为此添加一个BoilerRetiredState,并把它设为烧水器工作流的完成状态呢?如果是的话,我们是否要在用户界面上添加一个Retire按钮来触发向这个状态的转换呢?另一种情况是工作流遇到未处理异常不得不中止,或者用户在工作流运行的中途发出中止的命令,烧水器工作流在BoilerWorkingState里不可能遇到异常,由于DelayActivity的缘故,它正在数据库里面休息,然而,现实的烧水器却有可能在工作中突然报废,此时用户就应该通知Proton中止烧水器工作流,并舍弃储存在数据库里的状态信息,这次,Terminate按钮可能更易接受。

在正式的系统里,用户可能不会满足于知道队列里有多少个用户在等候,他们可能希望知道排队的人是谁、自己排在哪里、前面有什么人等等,而管理员则希望拥有把队列里的某个(些)人踢出去的权力、在工作流还在运行的时候应用新出台的管理规则等等,而这一切将有可能会导致一个复杂度远大于现在使用的EnrolleeQueue类的队列管理系统出现。

作为资源管理系统,Proton的责任就是管理烧水器的使用,然而,所有问题最终都会回归到人的问题,有时这些系统能够提高协作的效率,有时则会令事情变得更加复杂,究竟是哪种情况,最终还是取决于使用系统的人。

 

参考书目

  1. Programming Windows Workflow Foundation: Practical WF Techniques and Examples using XAML and C# by K, Scott Allen
  2. Microsoft Windows Workflow Foundation Step by Step (Pro Step By Step Developer) by Kenn Scribner
posted @ 2008-01-06 08:53  Allen Lee  阅读(3998)  评论(13编辑  收藏  举报