大部分企业应用程序都包含开发人员和架构师认为可以通过工作流轻松自然表示的业务流程。从根本上讲,工作流是捕捉现实世界中实体间的交互的程序。工作流等待来自外部世界的促
大部分企业应用程序都包含开发人员和架构师认为可以通过工作流轻松自然表示的业务流程。从根本上讲,工作流是捕捉现实世界中实体间的交互的程序。工作流等待来自外部世界的促进因素,而促进因素却可能需要相当长的时间才能到来。
作为即将推出的 Microsoft .NET Framework 3.0 的重要组成部分,Windows Workflow Foundation 提供了编程模型和运行时引擎,以便基于安装了 .NET Framework 2.0 或更高版本的 Window 平台来构建支持工作流的应用程序,其最低运行要求为 Windows XP Service Pack 2 (SP2) 或 Windows Server 2003。
从软件的角度看,需要人为操作的多数是异步事件处理、状态保持以及共享服务器可用性。Windows Workflow Foundation 实际上支持任何涉及人为操作的情况。
每一个工作流都是一个声明程序,其中,每条程序语句都用称为活动的组件表示。关于 Windows Workflow Foundation 的最大误区之一在于将所有要素活动都看成是顺序相连或按状态机转换相连。实际上,Windows Workflow Foundation 对执行模型进行了活动自动控制方面的虚拟化。这使您能编写可以捕捉各种控制流模式的复合活动,范围包括多种连接和合并、状态机、图形、序列、交叉存取和非本地退出等。总之,它将使您能够通过“高保真”的复合活动对存在于现实世界中的控制流模式进行建模。然而,Windows Workflow Foundation 提供了针对以下两种模式的内置活动:顺序工作流和状态机工作流。
顺序工作流有明确的开始和结束点,从始至终,一步接一步,沿着一条可能的线路执行。状态机工作流可以被表示为一组状态,每种状态可能包括各种活动,并由事件触发。您可以定义初始和完成状态,并按照所有定义的转换过程逐个状态地执行,直至到达结束状态。
正如我所谈到的,工作流的构造块就是活动。构造这样一个工作流意味着将活动进行组合以创建所需的模型来解决遇到的问题。虽然 Windows Workflow Foundation 提供了很多内置的活动,但依然可以通过自定义活动对其进行完全扩展。了解“开箱即用”活动的目标和功能对于充分理解该平台的真实潜能十分关键。本期专栏将带您浏览安装 Windows Workflow Foundation Runtime 和 Visual Studio 2005 Designer 时涉及的标准工作流活动。
“开箱即用”活动 Windows Workflow Foundation 是 .NET Framework 3.0 (原先称为 WinFX?)的组成部分。虽然它只是一个测试版本,但我发现最新的 Community Technology Preview(社区技术预览,CTP)版非常稳定,许多公司正广泛使用该版本开发应用程序。因此,已经出现大量工具和自定义活动,一个基于新闻组和论坛(如 wf.netfx3.com)的有效的技术支持社区正日益发展壮大。如果您对工作流应用程序感兴趣,那么越早熟悉它越好。
若要查找最新的下载,请访问 Windows Workflow Foundation。安装完毕后,启动 Visual Studio 2005,打开一个 Visual Basic? 或 C# 工作流项目,查看工具箱中的内容。根据您所选择的项目类型,工具箱可能如图 1 中所示的一种。
工具箱中的内容与您的选择操作相关,根据您所选择的工作流的类型会有不同。特别是,图 1 左边的工具箱还包含您创建状态机工作流时所需的状态活动。右边的工作箱窗口只包含顺序工作流的活动。这组预定义的活动可根据目标和预期的行为分为几类。图 2 显示了可能的分类:控制流、执行、事件、Web 服务和状态机。
 图1 工具箱里的工作流活动(Click the image for a smaller view)  图2 |
控制流活动管理图形活动执行的顺序。列表列出了实现循环的条件块以及构造。执行活动包括多种构造,这些构造可终止或挂起工作流、引发异常、执行内部或外部代码,或者生成另一个工作流。多数工作流需要与主机环境同步 — 通常是 Windows Forms 应用程序或 ASP.NET 应用程序。事件组中的活动能使工作流停止以等待外部事件、处理接收的事件,或只是在执行下一步之前进行等待。最后,Web 服务和状态组将目标锁定在工作流的两种特殊功能 — 将内部引擎为公开为 Web 服务或公开为状态机。让我们检查一下框架中的主要活动,然后分组进行。
通过条件控制工作流 如果在工作流中指定了条件,运行时引擎将对条件进行计算,然后根据计算结果进行操作。两种基于条件的核心活动是 IfElse 和 Policy。IfElse 活动的执行方式如同高级编程语言中的一条典型的 If 语句。它可以包含任意多基于条件的分支,以及一个在其他条件不满足情况下执行的默认分支。而 Policy 活动表示一系列规则。在 Windows Workflow Foundation 中,一条规则包括一个条件以及一个或一个以上引发的操作。可以将规则看作 If-Then-Else 语句,其中条件对应 If 块的 Boolean 临界条件,操作定义了 Then 和 Else 子句。让我们进一步了解各个活动,然后对它们进行比较。
当被添加到一个工作流中时,IfElse 活动看起如图 3 所示。默认情况下,它有两个分支,可以通过右键单击并从上下文菜单中进行选择来添加新的分支。当运行时引擎达到一个 IfElse 活动,它开始计算从左至右进行的各分支的条件。各分支的条件被确定,第一个计算结果为“true”的分支被运行。您可以通过活动的上下文菜单中的命令来回移动分支以更改分支的计算顺序。要启用分支,需要指定有效的条件。您可以采用以下两种方法中的一种来指定条件:通过表达式或通过一段代码。
 图 3 IfElse 的实际活动(Click the image for a larger view) |
如果您选择使用表达式来指定条件,则要在设计器设置分支,并为 Visual Studio 2005 的“属性”框中的“条件”项提供一个公共存放位置。接下来,打开表达式编辑器,输入一个涉及工作流成员并且其计算值为布尔值的表达式。例如,如果工作流类具有名为 MinimumLength 的公共属性,您可以将条件设置如下:
图 4 所示的编辑器完全支持 IntelliSense? 并提供对工作流私有成员和公共成员的访问。这意味着您也可以调用 Boolean 函数。假设您有一个属性声明如下:
Public Property MinimumLength() As Integer Get Return _minLength End Get Set(ByVal value As Integer) _minLength = value End Set End Property |
 图 4 针对 IfElse 活动的 Condition Editor(Click the image for a larger view) |
当您试图读取 MinimumLength 的值时,运行时会结束调用属性的 get 存取器。该存取器能够回复私有成员 _minLength 的值。IfElse 活动中的最后分支可以没有条件。在这种情况下,它将作为 IF 工作流语句的 Else 分支。
指定条件的第二种方法是通过临时代码。在这种情况下,您可以为工作流类添加方法,例如:
Sub EvalCondition(ByVal sender As Object, ByVal e As ConditionalEventArgs) e.Result = ... ' Boolean value End Sub |
该函数必须与接受对象和 ConditionalEventArgs 类并回复 void 的签名相匹配。ConditionalEventArgs 类的 Result 成员将设置为布尔值,代表条件的计算结果。
IfElse 活动可以进行嵌套以表示复杂的逻辑,但逻辑的设计仍然要在工作流中进行硬编码。这种方法既有利也有弊。有利的一面在于它使您可以根据您的需求准确设计工作流并将其与其他活动整合。当您需要创建一组规则来初始化部分工作流状态时,其不利的一面就显现出来。让一连串 IfElse 活动仅仅为工作流的内部成员分配值显然有些矫枉过正。
当您真正需要控制工作流的流动并连接各种工作块时,您应该使用 IfElse 活动组合。如果您所需要的仅仅是一个编程 If 语句序列,带有附加到分支的简单代码,那么最好使用 Policy 活动。Policy 活动是一个规则集合。与各规则相关的代码仅限于设置工作流属性,调用工作流方法或针对引用的程序集中的类型的静态方法。
活动与已排序的规则集合相关联,您可以通过编辑器定义规则。每条规则都有优先级和 Active 属性。这两种属性的组合确定了是否应当评估规则,以及采用哪种优先级。另外,规则还必须指定重估行为 —“Always(始终)”(默认值)或“Never(从不)”。如果设置为“Always”,规则将根据需要进行重估,否则只进行首次评估,以后无论工作流状态如何变化,也不再更改。
然而,作为一个整体设置的规则要进行正向推理。简而言之,正向推理是指一个规则的操作导致其他相关规则被重估的能力。例如,对另一个规则测试的字段的值进行更新操作需要对所涉及的规则进行重估(除非对该规则的重估被禁用)。正向推理有三种类型。 隐式推理表示运行时引擎将判断哪些字段被一些操作修改了,然后自动对它们进行重估。只要操作显式地作用于属性,这种机制即可成功。如果有如下操作,情况又会怎样:
| IF this.MinimumLength <= 8 THEN RefreshInternalState() |
有谁能够知道 RefreshInternalState 方法将要做什么?该方法可能会触及涉及策略活动中其他规则的工作流属性。通过使用方法声明中的属性,您可以显式地表示该方法的行为:
<RuleWrite("PasswordLevel")> _ Public Sub RefreshInternalState() Me.PasswordLevel = 1 End Sub |
RuleWrite 属性表示方法将要修改指定的属性;同样,RuleRead 属性表示方法将要从指定的属性中进行读取。这样,就可以为运行时引擎提供明确清晰的信息以确保规则设置同步。
最后,您可以编写显式调用对涉及的属性进行更新的操作。例如:
IF this.MinimumLength <= 8 THEN RefreshInternalState() Update("PasswordLevel") |
规则操作中的 Update 方法会计划对包含指定属性的所有规则进行重估。
循环和重复活动 这组活动提供了典型的 While 活动以及 Replicator 活动,这些活动与典型的 For 循环有些共同点。While 活动接受条件并在每次迭代开始时对该条件进行计算。如果条件为“true”,该活动将运行指定的子活动并重复直至条件为“false”。请注意在 While 主体内允许进行单一活动。因此,您可能要使用一个复合活动(如 Sequence 或 Parallel)在循环中执行多个活动。(在此处,使用术语“交错”可能比“并行”更加准确。因为没有涉及 Parallel 活动的并发,只有同一线程内的交错。) 与 Foreach 语句类似,Replicator 活动创建并执行指定的子活动的给定数量的实例。您可以只指定一个子活动,但允许使用复合或自定义活动。您不能通过声明性属性来控制迭代数。但是,您可以为初始化事件编写一个处理程序,并针对各个需要的实例,使用初始化数据填充 CurrentChildData 集合:
Sub replicator1_Initialized(ByVal sender As Object, ByVal e As EventArgs) Console.WriteLine("Initializing ...") replicator1.CurrentChildData.Add("You are #1") replicator1.CurrentChildData.Add("You are #2") replicator1.CurrentChildData.Add("You are #3") End Sub |
前面所述的代码段规定了 Replicator 的子活动的三个实例的顺序,每个实例均使用给定的字符串进行初始化。请注意,如果您将 CurrentChildData 集合置为空,Replicator 将不运行任何子活动,并仅限于触发顶级事件,如 Initialized 事件和 Completed 事件。您可以使用对象(不一定是字符串)初始化子活动,包括自定义类的实例。Replicator 还包括标示各个子活动初始化和完成的事件。默认情况下,子实例按顺序运行,尽管通过设置 ExecutionType 属性,您可以选择并行执行。如果需要并行执行,在复制器开始并执行并行线程前,可以创建所有子活动实例。当以顺序模式执行时,只有当前一个活动结束时下一个活动才能被实例化。 如果没有通过 UntilCondition 属性设置全局条件,Replicator 将在所有复制完成后才结束,否则,将在 UntilCondition 为“true”时终止活动。值得注意的是,尽管在所有子活动已完成并且 UntilCondition 属性计算为“false”时活动将被挂起,Replicator 也从不通过类似 While 循环中的子活动进行循环。在对活动进行全局实例化后,在每个子活动完成后以及所有包括的活动都已完成后,将对 UntilCondition 进行计算。但是有时,如果条件计算为“true”,Replicator 将立即退出。
您可以将 ConditionedActivityGroup (CAG) 活动中的条件执行和循环混合在一起。CAG 包含一些子活动(可能是复合活动),并可运行这些活动,直至满足全局条件。基本上,CAG 组合了 While 和 IfElse 活动的行为。通过以下伪代码来表示内部逻辑:
While condition If child1.WhenCondition Then child1.Execute If child2.WhenCondition Then child2.Execute : If childN.WhenCondition Then childN.Execute End While |
每个子活动都有 WhenCondition 属性。根据对指定条件的计算,可以在当前的迭代中运行或跳过活动。请注意,如果子活动没有 WhenCondition 集合,子活动将只能在第一次被执行,并在后续的迭代中被跳过。如果需要,可以根据状态变化的依存关系对所有条件进行计算。
在 UntilCondition 返回“true”并立即取消所有当前正在执行的活动后,CAG 活动将终止。如果没有指定条件,当因为没有设置条件或者条件计算为“false”而没有子活动运行时,CAG 也将结束。
同步块 涉及交错活动的顺序工作流都会对共享成员的访问进行系列化。然而,操作是不同的。当两个或两个以上以交错方式运行的序列用到 Parallel 活动时,操作是不同的。请看一下图 5。While 活动包含两个序列块,然后,序列块又将 Parallel 活动和代码活动联系在一起。在内部,Parallel 活动有四个块,每个块都对定义为工作流类成员的数字执行数学运算。Parallel 活动的四个分支以交错方式执行。(每一次在工作流实例中只有一个线程。该线程在 Parallel 活动的分支间来回切换。)跨任务同步又如何呢?默认情况下,子活动内部运行的每一段代码都进行对共享成员的直接访问。
 图 5 平行任务(Click the image for a larger view) |
根据图 5 所示的简单架构,可能会出现每个分支都作为一个原子操作来运行的情况。这纯属巧合,运行严格依据各操作的复杂性和持续时间。如果每个分支由多个活动组成,情况会怎样?SynchronizationScope 提供了声明性和明确的同步存取建模方法,它可以实现对一组活动中一个给定工作流实例内的共享状态进行同步存取。
SynchronizationScope 活动内运行的工作流部分是一种不能中断的原子操作。此处没有事务语义,SynchronizationScope 活动中包含的操作不能回滚。在所有基于活动(这些活动具有并行执行路径,如 Parallel、Replicator 和 CAG)的解决方案中,SynchronizationScope 活动起着关键作用。
图 6 可以帮助您快速体验 SynchronizationScope 活动的作用,该图是图 5 中工作流的修改版本。blockMultiply 活动对延迟交错的两个代码块进行计数。blockDivide 活动包含一个未同步的代码块。如果您将 blockMultiply 的内容放到同步范围之外,blockDivide 将可能在 blockMultiply 的延迟阶段执行。根据要进行的更新的种类不同,这可能会成为一个严重问题。SynchronizationScope 活动可确保工作流部分的连续性。
 图 6 平行任务与同步范围(Click the image for a smaller view) |
异常处理 正如其他基于 .NET 的应用程序一样,工作流能够引发和捕获异常。特别是,您可以使用 Throw 活动来引发一个特定的异常。Throw 活动需要几个设置 — 要引发的异常的类型,存储异常对象实例以进行进一步自定义的工作流类成员。只要在工作流项目中引用了类型,您就可以使用自定义的异常类型。
要捕获工作流活动引发的异常,您要添加 FaultHandler 活动。工作流设计器提供了一个用于您添加的错误处理程序的容器。每个处理程序一定是某个工作流的一部分,一旦捕获异常,该程序即执行。添加 FaultHandler 活动后,您可以定义要捕获的异常的类型。您可以通过 Fault 属性访问异常对象。值得注意的是,要可视化错误处理程序,您必须在 Visual Studio 2005 设计器中切换视图并通过单击设计器视图底部的选项卡列表来选择“View Faults(查看错误)”选项卡。
事务 .NET Framework 2.0 包括一个轻松而有效处理事务的对象 — 无论参与对象的数目和类型以及范围如何,也无论事务是本地的还是分布式的。该对象被命名为 TransactionScope,一般情况下,您可以根据如下模式进行使用:
Using ts As New TransactionScope ... ts.Complete End Using |
TransactionScope 对象保证在出现故障的情况下,事务既可以被提交也可以被回滚,更为重要的是,它可以确定您需要一个本地事务还是一个分布式事务,并登记所有必要的资源。当代码到达一个无法本地运行的点时,相应地,TransactionScope 提升至分布式事务处理协调器 (DTC)。实施 ITransaction 接口的所有对象可以随一个事务一起登记。该列表包括所有标准 ADO.NET 2.0 数据提供程序,并且 Microsoft 消息队列以兼容模式工作。
值得注意的是对于分布式事务,TransactionScope 和企业服务之间存在一些差别。TransactionScope 属于专门针对基于 .NET 的应用程序而设计的事务框架 — System.Transactions。在内部,System.Transactions 命名空间的类有时会以为 DTC 和 COM+ 委派一些工作作为结束。为什么在这一点上 TransactionScope 至关重要?TransactionScope Windows Workflow Foundation 活动仅仅是 TransactionScope .NET 类实例的一个工作流包装程序。
当您为工作流添加 TransactionScope 活动时,您可以设置要完成的事务的超时时间,并设置所需的隔离级别。事务范围内组合的所有活动形成了能够实现典型 ACID 架构的工作单位。在所有子活动都成功完成后将提交事务,工作流继续进行到下一步。如果在范围内引发异常,TransactionScope 活动自动回滚。编写事务性工作流并不要求您显式地处理提交和回滚语义 — 如果您要提交事务和引发异常来中止事务,您只需顺其自然。该活动将可以轻松管理其他任何事情。
请注意,您不能嵌套两个或两个以上的 TransactionScope 活动。同样地,您不能使用 Suspend 活动挂起事务内的工作流。然而,您可以通过 Listen 或 EventHandlingScope 活动,将事件处理程序整合到事务中。在这种情况下,工作流主机应当包括持久性服务,否则,如果工作流试图将它的状态保存为空闲状态,会引发异常。 此外,相关的活动,如 CompensatableTransactionScope 还支持补偿。补偿是在有后续业务预期的情况下,从逻辑上撤消已完成的事务的过程。补偿与回滚不同,因为它是一种当工作流中发生违反规则的情况时,取消已成功完成和提交的事务所产生的影响的有效方法。在两个帐户间进行资金划转的事务是一个典型的回滚示例。第一个调用将资金从一个帐户中取出,第二个调用将相同数额的资金划转至另一个帐户。只要数据库支持两阶段提交模型,如果在事务中引发异常,回滚可恢复一致的状态。
设想一下一个订单处理工作流,其中一个事务被用于通过信用卡进行支付。第一步,首先从信用卡中提取资金来支付货款。然后,在交易成功完成后,该货物不能再被销售。如果违反了业务规则,需要按照合适的规定对交易结果进行补偿 — 一般是将资金划入签帐卡。
通过右键单击 CompensatableTransactionScope 活动,您可以切换到事务的“补偿”视图并添加所有补偿事务影响所需的活动。在图 7 中,OrderScope 事务与补偿活动规定相关联,该规定要求在出现业务异常的情况下,应将已提取的资金退回。正如我前面谈到的,工作流中的异常是通过异常处理程序捕获的。对于某一特定异常(如 ProductDiscontinued),您可以调用一个 Compensate 活动,该活动已绑定到其影响作用已被取消的事务活动。一般情况下,Compensate 活动会触发适用于任何可被补偿的活动(即,适用于实施 ICompensatableActivity 接口的活动)的补偿机制。与 CompensatableTransactionScope 一起,CompensatableSequence 也实施了本接口并能够用于非事务性的补偿情况。不过,您也可以编写支持补偿的自定义活动。
 图 7 事务与补偿活动(Click the image for a larger view) |
调用其他代码 此外,还有一些活动可调用和执行代码。这些活动包括:Code、InvokeWorkflow、InvokeWebService 和 CallExternalMethod。Code 活动最灵活,可以表示您在工作流某一点插入的自定义代码块。Code 活动触发您在工作流中处理的 ExecuteCode 事件,并能使其运行您所需要的代码。代码将包含在工作流中并与其一起进行编译。Code 活动可以调用外部程序集,只要您在 ExecuteCode 事件处理程序中加入所有能够加载程序集和选择方法的必要代码。
InvokeWorkflow 活动可以引用工作流和异步调用工作流。您可以传递输入参数,但要注意 InvokeWorkflow 活动要在被启动的工作流开始执行之前完成。您无法将当前工作流的执行与外部工作流的执行同步,也不能处理当前工作流中的输出参数。
InvokeWebService 活动可以通过代理类同步调用 Web 服务方法。使用 Code 活动并调用 Web 服务,可以对活动的行为进行模拟。
最后,CallExternalMethod 活动可用于调用本地服务方法。本地服务是对于实施以 ExternalDataExchange 属性修饰的接口的工作流可用的任何类。另外,代表本地服务的类必须作为外部数据交换服务添加到工作流运行时。
Dim service As New ExternalDataExchangeService runtime.AddService(service) Dim localService As New YourService() service.AddService(localService) |
您可以配置 CallExternalMethod 活动以调用由本地服务实施的接口的方法。为什么 CallExternalMethod 仅限于调用已知接口的方法?原因在于活动不仅仅是代码执行者,而是旨在与 HandleExternalEvent 活动一起实施与本地服务的双向交流。借助 CallExternalMethod 活动,通过将 HandleExternalEvent 活动加入工作流,您可以调用服务方法,处理服务引发的事件。
总结 活动是工作流的构造块。通过在 Visual Studio 2005 设计器中撰写活动,您可以创建工作流解决方案。与 ASP.NET 中的服务器控件和 Windows Forms 中的控件类似,工作流活动是解决方案的精华所在并形成了开发人员的主要工具箱。Windows Workflow Foundation 提供了一些随机活动。在此,我要回顾一下涵盖事务、并行和条件执行、循环以及异常处理的活动。wf.netfx3.com 上提供了解一些其他免费活动。将它们挑出来!