BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Windows Workflow中的HandleExternalEvent Activity

| 作者 Scott Allen 关注 0 他的粉丝 ,译者 张逸 关注 9 他的粉丝 发布于 2008年4月25日. 估计阅读时间: 16 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

工作流不能孤立存在。典型的工作流需要接收从外部世界传来的数据,并让处于外部世界的我们知道何时需要做出决策,例如批准我们团队成员到拉斯维加斯旅游的开支报告。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]
public interface IPaymentProcessingService
{
event EventHandler PaymentProcessed;
}
[Serializable]
public class PaymentProcessedEventArgs : ExternalDataEventArgs
{
public PaymentProcessedEventArgs(Guid instanceId, double amount)
: base(instanceId)
{
_amount = amount;
}
private double _amount;
public double Amount
{ get { return _amount; }
set { _amount = value; }
}
}

在上述代码中,我们定义了一个接口,它包含了一个我们希望抛出的事件以及事件的参数。需要特别重视下列特性:

  • 我们为接口声明了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
{
public void ProcessPayment(Guid id, double amount)
{
// ... do some work work work

// ... then raise an event to let everyone know
PaymentProcessedEventArgs args;
args = new PaymentProcessedEventArgs(id, amount);

EventHandler evh;
evh = PaymentProcessed;
if (evh != null)
evh(this, args); // boom!
}
public event EventHandler
PaymentProcessed;
}

抛出的异常为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 evh;
evh = PaymentProcessed;
if (evh != null)
evh(null, args);

如果你发现事件传递失败,就应深入到内部异常中去查找导致问题出现的确切类型。事件的args应该包含一个对象图,而且,在它的内部会包含一个不支持序列化的类型。

配置工作流运行时

我本来应该提前介绍这一内容,因为在事件抛出我们上面所看到的异常之前,我们需要将付款服务运行在工作流运行时中,并对其进行配置。首先,我们需要将 ExternalDataExchangeService加入到运行时中。ExternalDataExchangeService管理宿主的本地通信服务,例如我们定义的付款处理服务。然后,我们将付款处理服务添加到外部服务的列表中。

WorkflowRuntime workflowRuntime = new WorkflowRuntime();

ExternalDataExchangeService dataExchangeService;
dataExchangeService = new ExternalDataExchangeService();
workflowRuntime.AddService(dataExchangeService);

PaymentProcessingService paymentProcessing;
paymentProcessing = new PaymentProcessingService();
dataExchangeService.AddService(paymentProcessing);
// ...

这里是上述代码的另一个版本,它包含了一个bug,花了我不少时间才跟踪到:

PaymentProcessingService paymentProcessing;
paymentProcessing = new PaymentProcessingService();
workflowRuntime.AddService(paymentProcessing); // this is WRONG!!!!!!!!

我们必须将自己定义的服务添加到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/

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

容易忽略的bug by Yang jeffrey

这个bug还是很容易犯的哦。我那天也是一不小心把服务加载到runtime上来。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT