工作流不能孤立存在。典型的工作流需要接收从外部世界传来的数据,并让处于外部世界的我们知道何时需要做出决策,例如批准我们团队成员到拉斯维加斯旅游的开支报告。Windows Workflow(WF)提供了各种与外部世界通信的机制。例如,WebServiceInput 与 InvokeWebService 两个 activity 都是 WF 基础 activity 库的组件。我们可以通过这两个 activity 与使用基于 WSDL 契约的远程服务进行通信。
针对本地的、进程内的通信,我们可以使用 CallExternalMethod 和 HandleExternalEvent 两个 activity。CallExternalMethod activity 允许工作流调用在宿主中注册了的本地服务的方法。HandleExternalEvent activity 则允许工作流侦听其宿主抛出的事件。本文,我们将重点关注 HandleExternalEvent activity。
并非典型的事件
我必须告诉读者一个秘密。在学习 Windows Workflow 的 Beta 2.2 版本时,我试图以各种可能的方式去破坏事件。是的,或许并非所有可能的方式,但我确信随着时间的推移,我最终会找到好几种技术。在本文中,我会告诉你我曾遇到过的问题,以及如何排除故障并解决这些问题。
首先,它有助于我们牢牢地掌握事件到达一个工作流的方式。事件是一种机制,当发生某些值得关注的事情时,我们所使用的宿主就会告知工作流,例如当开支报告被批准(或者拒绝)时,或者在邮箱中收到支票的时候。
在 Windows Forms 以及 ASP.NET 中,我们使用的事件是直接从发布者传递到订阅者的,而工作流中的事件则不相同,它们需要经历更长的传递路径。一个工作流实例存活于工作流运行时环境中,就像婴儿孕育在母体之中一般,在运行时允许工作流实例被传出和执行之前,我们必须遵循一个约定。如此做的部分原因在于工作流可能需要一段时间等待事件到达,并且,WF 运行时可能需要将工作流序列化到数据库,以达到长期存储的目的——这一特性就是所谓的钝化(passivation)。我们显然不能将一个工作流实例在内存保存三个月时间,以等待账户被关闭。
根据约定,首当其冲的就是需要定义一个契约,用它描述引入的事件与类型。
[ExternalDataExchange]<br></br> public interface IPaymentProcessingService<br></br> {<br></br> event EventHandler<paymentprocessedeventargs></paymentprocessedeventargs> PaymentProcessed;<br></br> }<br></br> [Serializable]<br></br> public class PaymentProcessedEventArgs : ExternalDataEventArgs<br></br> {<br></br> public PaymentProcessedEventArgs(Guid instanceId, double amount)<br></br> : base(instanceId)<br></br> {<br></br> _amount = amount;<br></br> }<br></br> private double _amount;<br></br> public double Amount<br></br> { get { return _amount; }<br></br> set { _amount = value; }<br></br> }<br></br> }<br></br>
在上述代码中,我们定义了一个接口,它包含了一个我们希望抛出的事件以及事件的参数。需要特别重视下列特性:
- 我们为接口声明了 ExternalDataExchangeAttribute 特性
- 事件的 args 类派生自 ExternalDataEventArgs
- 事件的 args 类是可序列化的。
当我们注册了一个 WF 的通信服务时,运行时会查找具有 ExternalDataExchangeAttribute 的接口,如果没有找到,就会抛出一个异常(一个 InvalidOperationException 异常,异常消息为“服务没有实现具有 ExternalDataExchange 特性的接口(Service does not implement an interface with the ExternalDataExchange attribute)”)。如果 WF 找到了该特性,就会为事件创建代理侦听器。这些代理可以捕获事件,然后经由它们传递到正确的工作流实例,该实例可能是存储在数据库表中并被唤醒的实例。
事件的 Args
注意,ExternalDataEventArgs 类需要一个 Guid 参数,用以识别我们通过事件希望获得到达的工作流实例。如果事件的 args 类没有派生自 ExternalDataEventArgs,那么在我们编译一个试图接收一个参数(实参)当作事件参数(形参)的工作流时,就会出现错误。Activities 有能力验证它们,并确保我们在运行时设置的所有属性值都是它们能够正确完成任务所需要的。
当我们在工作流设计器中拖动一个 HandleExternalEvent activity 时,我们需要指定 activity 侦听的接口和事件名。如果我们没有派生自正确的类,就会出现错误:“验证失败:事件 PaymentProcessed 必须是 T 派生自 ExternalDataEventArgs 的 EventHandler 类型(validation failed: The event PaymentProcessed has to be of type EventHandler where T derives from ExternalDataEventArgs)”(我想该错误的意思应该是指“是 EventHandler 类型”)。图1 演示了在设计器中配置正确的activity。
实现
我们定义了一个契约,一个事件的args 类,以及为了工作流能够接收事件所需要的所有元数据。正如Hazelwood 船长(译注:即泰坦尼克号的船长)曾经说过的那样,究竟是什么导致错误发生?
让我们从处理付款的契约实现开始。代码包含了一个难以察觉的问题,它会导致出现异常。
class PaymentProcessingService : IPaymentProcessingService<br></br> {<br></br> public void ProcessPayment(Guid id, double amount)<br></br> {<br></br> // ... do some work work work<p> // ... then raise an event to let everyone know</p><br></br> PaymentProcessedEventArgs args;<br></br> args = new PaymentProcessedEventArgs(id, amount);<p> EventHandler<paymentprocessedeventargs></paymentprocessedeventargs> evh;</p><br></br> evh = PaymentProcessed;<br></br> if (evh != null)<br></br> evh(this, args); // boom!<br></br> }<br></br> public event EventHandler<paymentprocessedeventargs></paymentprocessedeventargs><br></br> PaymentProcessed;<br></br> }<br></br>
抛出的异常为 EventDeliveryFailedException,Message 属性的内容为“由于实例 ID[GUID] 的原因,IPaymentProcessingService 接口类型的 PaymentProcessed 事件不能被传递(Event PaymentProcessed on interface type IPaymentProcessingService for instance ID [GUID] cannot be delivered)”。消息没有包含任何明显的线索,我们需要深度挖掘,以找到更多的信息。
如果我们观察一下 InnerException 属性,基本上可以寻找到问题的答案。该内部异常是一个 InvalidOperationException,它的 Message 属性值为“EventArgs 不支持序列化(EventArgs not serializable)”。该异常有些混淆视听,因为我们已经将 EventArgs 定义为可序列化了!注意在图2 中,当前异常(在2005 的debugger 中为$exception)包裹了一个内部异常,说明了错误的准确原因。
在截图中,消息的值被截断了,后面的内容为“PaymentProcessingService 类型没有被标记为可序列化(Type PaymentProcessingService is not marked as serializable)”。这说明传入到事件中的每个参数都必须是可序列化的,即使是sender 参数!我们传递了this 引用,它指向了我们定义的付款处理服务。实际上,工作流实例并不需要该服务的一个引用(如果需要调用服务的方法,可以使用CallExternalEvent activity),所以我们可以将sender 参数设置为null 或Nothing,以解决这一问题。
EventHandler<paymentprocessedeventargs></paymentprocessedeventargs> evh;<br></br> evh = PaymentProcessed;<br></br> if (evh != null)<br></br> evh(null, args);<br></br>
如果你发现事件传递失败,就应深入到内部异常中去查找导致问题出现的确切类型。事件的 args 应该包含一个对象图,而且,在它的内部会包含一个不支持序列化的类型。
配置工作流运行时
我本来应该提前介绍这一内容,因为在事件抛出我们上面所看到的异常之前,我们需要将付款服务运行在工作流运行时中,并对其进行配置。首先,我们需要将 ExternalDataExchangeService 加入到运行时中。ExternalDataExchangeService 管理宿主的本地通信服务,例如我们定义的付款处理服务。然后,我们将付款处理服务添加到外部服务的列表中。
WorkflowRuntime workflowRuntime = new WorkflowRuntime();<p> ExternalDataExchangeService dataExchangeService;</p><br></br> dataExchangeService = new ExternalDataExchangeService();<br></br> workflowRuntime.AddService(dataExchangeService);<p> PaymentProcessingService paymentProcessing;</p><br></br> paymentProcessing = new PaymentProcessingService();<br></br> dataExchangeService.AddService(paymentProcessing);<br></br> // ...<br></br>
这里是上述代码的另一个版本,它包含了一个 bug,花了我不少时间才跟踪到:
PaymentProcessingService paymentProcessing;<br></br> paymentProcessing = new PaymentProcessingService();<br></br> workflowRuntime.AddService(paymentProcessing); <span color="#00cc66">// this is WRONG!!!!!!!!</span><br></br>
我们必须将自己定义的服务添加到 ExternalDataExchangeService 中,而不能直接加入到工作流运行时中。我定义的服务本应触发一个事件,而实际上却什么都没有发生。在 degugger 中,可以看到事件为 null 值,这意味着无人订阅该事件。 ExternalDataExchangeService 能够展现传入的服务,查询像 ExternalDataExchange 特性那样的元数据,以及订阅事件。在运行时中,事件通过代理传递给工作流,如图3 所示。
总结
要生成到达工作流的事件是一件细致活儿。如果事件被触发后到达的目标却是空的,就必须确保ExternalDataExchanceService 和本地通信服务是被正确地配置和添加到运行时中。如果工作流运行时抛出了异常,则异常会在它的InnerException 属性(它可能也包含一个 InnerException 属性)中蕴含丰富的细节信息。希望这两条提示可以为开发人员节约一些时间。
关于作者
Scott Allen 住在巴尔的摩市外,他是微软的 MVP 以及 OdeToCode.com 的创始人。在最近 14 年间,Scott 参与开发了嵌入式的 Windows 和 Web 平台下的商业软件。你可以通过 scott@OdeToCode.com 与他取得联系,或者访问他的博客: http://www.OdeToCode.com/blogs/scott/ 。
评论