Dino Esposito
Solid Quality Learning
适用于:
Microsoft Windows Workflow Foundation
Microsoft Windows Vista
摘要:对于需要为 Microsoft .NET 平台创建工作流驱动应用程序的开发人员而言,本文将介绍他们感兴趣的 Microsoft Windows Workflow Foundation 技术和功能。
注本文撰写的对象为 Windows Workflow Foundation beta 1。请注意,在该技术的最终版本问世之前,内容上很可能会发生更改。
有关向 Windows 平台添加工作流支持的初步知识 | |
创建第一个工作流 | |
接收和使用数据 | |
工作流运行库 | |
工作流和活动 | |
开发自定义活动 | |
计划更现实的工作流 | |
小结 |
Microsoft Windows Workflow Foundation (WWF) 是一个可扩展框架,用于在 Windows 平台上开发工作流解决方案。作为即将问世的 Microsoft WinFX 的组成部分,Windows Workflow Foundation 同时提供了 API 和一些工具,用于开发和执行基于工作流的应用程序。Windows Workflow Foundation 提供单个统一的模型,以便创建跨越多个类别应用程序的端到端解决方案,包括人力工作流和系统工作流。
Windows Workflow Foundation 是一个广泛且通用的工作流框架,并且从下到上、在每个级别都针对可扩展性进行了设计。基于 Windows Workflow Foundation 的解决方案,由得到 Microsoft .NET 代码支持且在宿主应用程序中运行的互连组件组成。就像在定制的环境中以可视方式创建 Web 页一样,您需要在可视设计器中制订特定工作流的步骤,并且添加代码隐藏工作流组件以实现规则并定义业务过程。
Windows Workflow Foundation 提供一个工作流引擎、一个 .NET 托管 API、运行库服务以及与 Microsoft Visual Studio 2005 集成的可视化设计器和调试器。可使用 Windows Workflow Foundation 来生成并执行同时跨越客户端和服务器的工作流,以及可在所有类型的 .NET 应用程序内部执行的工作流。
本文通过几个循序渐进的示例对 Windows Workflow Foundation 的进行了流畅的简介,并且说明它的工作方式。
工作流 是以活动示意图形式定义的人力或系统过程模型。活动 是工作流中的一个步骤,并且是工作流的执行、重用和创作单位。活动示意图表达规则、操作、状态以及它们的关系。Windows Workflow Foundation 工作流通过安排活动而设计,然后它编译为 .NET 程序集,且在工作流运行库和公共语言运行库 (CLR) 中执行。
Windows Workflow Foundation 主要由 .NET 驱动的运行库环境组成,该环境处理在 Visual Studio 设计器中设计和实现的特殊对象。Microsoft .NET Framework 2.0 是支持 Windows Workflow Foundation 所必需的。单独的安装程序包为 Visual Studio 2005 添加了 Windows Workflow Foundation 设计器和项目模板支持。一旦安装,就会向 Visual Studio 2005 中的标准项目列表中添加一个全新的节点,如图 1 所示。
图 1. Visual Studio 2005 中的工作流项目模板
您可以在各种选项中进行选择,其中每个选项都标识了特定类型的工作流应用程序。表 1 显示工作流项目模板的不完全列表。
表 1. Visual Studio 2005 中的工作流项目类型 | |
类型 | 说明 |
顺序工作流控制台应用程序 (Sequential Workflow Console Application) |
创建用于生成工作流的项目,该工作流包含一个默认的顺序工作流和一个控制台测试宿主应用程序。 |
顺序工作流库 (Sequential Workflow Library) |
创建用于以库的形式生成顺序工作流的项目。 |
工作流活动库 (Workflow Activity Library) |
创建一个用来创建活动的库的项目,以后可以将其作为工作流应用程序中的构造块重用。 |
状态机控制台应用程序 (State Machine Console Application) |
创建用于生成状态机工作流和控制台宿主应用程序的项目。 |
状态机工作流库 (State Machine Workflow Library) |
创建用于以库的形式生成状态机工作流的项目。 |
空工作流 (Empty Workflow) |
创建可以包含工作流和活动的空项目。 |
Windows Workflow Foundation 支持两种基本工作流样式:顺序工作流和状态机工作流。
顺序工作流 非常适合以下类型的操作,即该操作由依次执行直至最后一个活动完成的步骤的管线表示。但是,顺序工作流的执行并非完全是顺序的。它们仍然可以接收外部事件或者启动并行任务,在这种情况下,确切的执行顺序可能有所不同。
状态机工作流 由一组状态、转换和操作组成。首先,将一个状态表示为起始状态,然后,基于事件执行向另一个状态的转换。状态机工作流可以具有确定工作流结束的最终状态。
让我们假设您选择并创建了一个新的顺序工作流控制台应用程序项目。Visual Studio 2005 解决方案资源管理器将包含两个文件 — workflow1.cs 以及最初从视图中隐藏的 workflow1.designer.cs。这两个文件表示创建的工作流。Windows Workflow Foundation 工作流包含工作流模型文件和一个代码文件类。workflow1.cs 类是可在其中写入您自己的工作流业务逻辑的代码文件类。workflow1.designer.cs 类表示活动示意图的说明。该文件由 Visual Studio 2005 按照与 Microsoft Windows Forms 项目中的窗体非常类似的方式自动管理。当向工作流中添加活动时,Visual Studio 2005 会用以编程方式生成活动示意图的 Microsoft C# 代码更新设计器类。要继续使用 Windows 窗体的类比,那么工作流类似于窗体,而活动类似于控件。
我们可以为活动布局选择另一种形式的持久性 — XML 工作流标记格式。要尝试这一方法,可从项目中删除 workflow1.cs 文件并添加一个新的工作流项,如图 2 所示。
图 2. 用代码分隔添加顺序工作流项
现在,项目包含两个文件:workflow1.xoml 和 workflow1.xoml.cs。前一个文件包含表示工作流模型的 XML 工作流标记;后一个文件是一个代码文件类,并且包含工作流的源代码和事件处理程序。如果双击 .xoml 文件,会看到处于运行状态的可视工作流设计器(参见图 3)。
不存在为工作流模型的序列化选择标记或代码的运行时暗示 — 一旦该工作流编译为程序集,它们就是等效的。
工作流应用程序是完成工作(例如,发送或接收数据)的活动和管理一组子活动的执行的复合活动(例如,IfElse 和 While)的混合体。工作流可实现复杂的端到端方案,例如文档审阅、PO 批准、IT 用户管理、合作伙伴之间的信息交换、任何种类的向导或业务线应用程序。
图 3 显示一个极为简单的示例工作流,它只包含一个活动 — code1 块。
图 3. Visual Studio 2005 工作流设计器
Code 块对应于 Code 类的一个实例,并且表示工作流中的一个活动,其行为以用户定义的代码表示。后端代码是通过 Visual Studio 2005 输入的(只需双击设计器中的所选元素),这是 ASP.NET 应用程序和其他 Visual Studio 2005 项目为人熟悉的编程风格。
当双击该活动时,代码文件会打开且提供代码处理程序的存根。
private void code1_ExecuteCode(object sender, EventArgs e) { // Some code here }
当工作流运行库在处理工作流的过程中开始处理指定的活动块时,在代码处理程序中键入的任何语句都将执行。以下代码只是输出一条欢迎消息。
private void code1_ExecuteCode(object sender, EventArgs e) { Code c = (Code) sender; Console.WriteLine("Hello, from '{0}'./nI'm an instance of the {1} class.", c.ID, c.ToString()); }
除了可视化布局以外,工作流还包含 workflow1.xoml.cs 文件中保存的以下代码。
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Collections; using System.Drawing; using System.Workflow.ComponentModel.Compiler; using System.Workflow.ComponentModel.Serialization; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Design; using System.Workflow.Runtime; using System.Workflow.Activities; using System.Workflow.Activities.Rules; namespace HelloWorldWorkflow { public partial class Workflow1 : SequentialWorkflow { private void code1_ExecuteCode(object sender, EventArgs e) { Code c = (Code) sender; Console.WriteLine("Hello, from '{0}'./nI'm an instance of the {1} class.", c.ID, c.ToString()); } } }
partial 属性涉及不完全类 — 这是 .NET Framework 2.0 中的一个新概念。不完全类 是这样的一种类:它的定义可能覆盖不同的源文件。每个源文件都包含一个有头有尾的普通类定义,只不过该定义是不完整的,并且没有穷尽该类所需的逻辑。编译器将把不完全的类定义合并到该类可以编译的完整定义中。不完全类与面向对象没有任何关系;它们是用来扩展项目中类行为的源代码级别和受到程序集限制的方式。在 .NET Framework 2.0 中,不完全类是用于防止 Visual Studio 2005 在代码文件内部注入自动生成代码的手段。原始类中缺少的任何绑定代码都将由运行库通过添加不完全类进行添加。
工作流只能由 Windows Workflow Foundation 工作流运行库执行,并且工作流运行库需要外部应用程序按照若干个规则来承载它。出于测试目的,Visual Studio 2005 还向项目中添加了一个 program.cs 文件。该文件是一个简单的控制台应用程序,如下所示。
class Program { static AutoResetEvent waitHandle = new AutoResetEvent(false); static void Main(string[] args) { WorkflowRuntime workflowRuntime = new WorkflowRuntime(); workflowRuntime.StartRuntime(); workflowRuntime.WorkflowCompleted += OnWorkflowCompleted; Type type = typeof(HelloWorldWorkflow.Workflow1); workflowRuntime.StartWorkflow(type); waitHandle.WaitOne(); workflowRuntime.StopRuntime(); // A bit of feedback to the user Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine("=========================="); Console.WriteLine("Press any key to exit."); Console.WriteLine("=========================="); Console.ReadLine(); } static void OnWorkflowCompleted(object sender, WorkflowCompletedEventArgs e) { waitHandle.Set(); } }
正如您在上述代码中的粗体行中看到的那样,出于简单性的目的,Visual Studio 2005 在该控制台应用程序中硬编码工作流类的名称。要防止控制台应用程序在完成之后立即退出,您可能需要在 Main 方法的结尾添加 Console.ReadLine 调用。此刻,您已经做好生成和测试该工作流的所有准备:请按 F5 并执行。如果一切顺利,那么会看到图 4 中显示的输出。
图 4. 控制台宿主应用程序执行的示例工作流
调试工作流应用程序也很容易。实际上,需要做的所有工作就是放置断点。您可以在工作流代码文件类中的任何位置放置断点(就像通常对 C# 代码所做的那样),或者直接在设计器视图中放置断点(这确实非常有趣)。需要选择希望调试器介入的活动,按 F9 设置断点,如图 5 所示。
图 5. 工作流设计器视图中放置的断点
一旦代码流到达设置了断点的活动,Visual Studio 2005 就会将控制权移交给工作流调试器(参见图 6)。从这里开始,您可以按照预期的那样,在可视化设计器中按 F11 单步执行代码和活动。
图 6. 处于调试会话状态的工作流应用程序
让我们继续分析并修改该工作流,以使其在实例化以后接收和使用数据。有两种在实例化工作流以后使其接收数据的常规方法:参数和事件。如果选择使用参数,则需要在可视化设计器中手动定义参数名称和类型的列表。如果选择使用事件,则需要创建并添加一个自定义活动(该活动充当在工作流模型中的某个位置介入的外部源),并且传入一些数据。我们将在下文中说明基于事件的方法;现在,让我们重点说明一下参数。
如图 7 所示,Workflow1 的 Properties 窗格显示一个 Parameters 集合,您将在设计时用名称/值对进行填充。
图 7. 向工作流中添加参数
图 8 显示正在工作的参数编辑器。您可以为所需的每个参数创建一个新项并指明它的名称、类型和方向。
参数的类型可手动键入,也可从定制的对象浏览器中选择。在关闭 Workflow Parameters Editor 对话框后,立即修改代码文件以合并刚刚定义的参数。您通常添加两个公共属性,并且使它们公开 Parameters 集合的内容,如下面的代码所示。
public partial class Workflow1 : SequentialWorkflow { public string UserFirstName { get { return (string) Parameters["FirstName"].Value; } set { Parameters["FirstName"].Value = value; } } public string UserLastName { get { return (string) Parameters["LastName"].Value; } set { Parameters["LastName"].Value = value; } } : }
使用公共属性确实是一种良好的编程做法,可使代码变得更加简洁明了。它绝不是使用参数数据的要求。直接对工作流的 Parameters 集合进行调用也是完全可行的。如果用公共属性包装参数,则请随意选择属性的名称。然而,请记住,C# 中的参数名是区分大小写的。
那么是谁实际上通过这些参数提供输入数据呢?完成这一任务的是宿主应用程序。然而,需要注意的是,宿主在初始化时(此时,工作流被加载以便执行到运行库容器中)设置任何参数。为了稍微详细地说明这一点,让我们编写一个基于 Windows 窗体的示例宿主应用程序。该示例应用程序将提供几个文本框,以便用户输入名字和姓氏(参见图 9),并且将它们传递给工作流代码处理程序。为了使用这些参数,让我们重新编写代码处理程序,如下所示。
private void code1_ExecuteCode(object sender, EventArgs e) { MessageBox.Show("Welcome, " + UserFirstName + " " + UserLastName); }
该示例 Windows 窗体宿主应用程序中的关键内容是附加到 Start Workflow 按钮的单击处理程序。
图 9. 工作流宿主 Windows 窗体应用程序
窗体的代码隐藏类的全部源代码如下所示。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Reflection; using System.Workflow.ComponentModel; using System.Workflow.Runtime; using System.Workflow.Runtime.Hosting; namespace WinFormHost { public partial class Form1 : Form { private WorkflowRuntime _wr = null; private string _workflowAssembly = ""; private string _workflowTypeName = ""; public Form1() { InitializeComponent(); _workflowAssembly = "WorkflowWithParams"; _workflowTypeName = "WorkflowWithParams.Workflow1"; _wr = new WorkflowRuntime(); _wr.StartRuntime(); } private void btnStartWorkflow_Click(object sender, EventArgs e) { string assemblyName = _workflowAssembly; string typeName = _workflowTypeName; // Attempt to get type by fully-qualified name Assembly assembly = Assembly.Load(assemblyName); Type workflowType = assembly.GetType(typeName); Dictionary parameters = new Dictionary(); parameters.Add("FirstName", txtFirstName.Text); parameters.Add("LastName", txtLastName.Text); // Start the workflow Guid instanceID = Guid.NewGuid(); _wr.StartWorkflow(workflowType, instanceID, parameters); } } }
要填充参数集合,需要使用由字符串和对象组成的基于泛型的字典。项的名称是字符串,而所包含的值被配置为对象。需要向该字典中添加像工作流模型中的静态参数一样多的项 — 在此情况下,请添加 FirstName 和 LastName。这两个参数获得在 UI 文本框中键入的内容。
最后,在创建指定模型的实例的同时将执行工作流。运行库对象上的 StartWorkflow 方法具有多个重载。代码中使用的版本接受工作流类型、输入参数的集合以及系统生成的全局唯一标识符 (GUID)。
对于每个进程,只需要一个工作流运行库实例;对于每个 AppDomain,不能有一个以上的实例。这里,最需要做的事就是直接在窗体的构造函数中创建所需的实例。同一个运行库对象可以照看多种工作流实例。运行库基于实例的 GUID 来区分它们,并且为每个特定实例接收私有数据。
出于纯粹的教学目的,让我们在这一开发阶段迅速观察一下设计器和工作流标记代码。下面是 workflow1.designer.cs 源代码文件。
public sealed partial class Workflow1 : SequentialWorkflow { private void InitializeComponent() { ParameterDeclaration FirstName = new ParameterDeclaration(); ParameterDeclaration LastName = new ParameterDeclaration(); this.code1 = new System.Workflow.Activities.Code(); // // code1 // this.code1.ID = "code1"; this.code1.ExecuteCode += new System.EventHandler(this.code1_ExecuteCode); // // Workflow1 // this.Activities.Add(this.code1); this.DynamicUpdateCondition = null; this.ID = "Workflow1"; FirstName.Direction = System.Workflow.ComponentModel.ParameterDirection.In; FirstName.Name = "FirstName"; FirstName.Type = typeof(string); FirstName.Value = null; LastName.Direction = System.Workflow.ComponentModel.ParameterDirection.In; LastName.Name = "LastName"; LastName.Type = typeof(string); LastName.Value = null; this.Parameters.Add(FirstName); this.Parameters.Add(LastName); } private Code code1; }
下面是相应的工作流标记内容。
<?Mapping XmlNamespace="ComponentModel" ClrNamespace="System.Workflow.ComponentModel" Assembly="System.Workflow.ComponentModel" ?> <?Mapping XmlNamespace="Compiler" ClrNamespace="System.Workflow.ComponentModel.Compiler" Assembly="System.Workflow.ComponentModel" ?> <?Mapping XmlNamespace="Activities" ClrNamespace="System.Workflow.Activities" Assembly="System.Workflow.Activities" ?> <?Mapping XmlNamespace="RuleConditions" ClrNamespace="System.Workflow.Activities.Rules" Assembly="System.Workflow.Activities.Rules" ?> <SequentialWorkflow x:Class="WorkflowWithParams.Workflow1" x:CompileWith="Workflow1.xoml.cs" ID="Workflow1" xmlns:x="Definition" xmlns="Activities"> <SequentialWorkflow.Parameters> <wcm:ParameterDeclaration Name="FirstName" Type="System.String" Direction="In" xmlns:wcm="ComponentModel" /> <wcm:ParameterDeclaration Name="LastName" Type="System.String" Direction="In" xmlns:wcm="ComponentModel" /> </SequentialWorkflow.Parameters> <Code ExecuteCode="code1_ExecuteCode" ID="code1" /> </SequentialWorkflow>
请注意,在创建该工作流的实例以便执行时,必须显式初始化并传入 Parameters 集合中静态定义的所有参数。
宿主通过 WorkflowRuntime 类与 Windows Workflow Foundation 交互。请不要被如上所示示例宿主表面上的简单性所蒙骗,以至于您注意不到这一要点。可以让宿主负责处理很多附加、关键的方面,例如:创建一个或多个进程以及一个或多个 AppDomain;按照需要封送 AppDomain 之间的调用;设置隔离机制。出于可伸缩性的原因,宿主可能需要创建多个进程来利用一台计算机中的多个 CPU,或者在一个计算机场中运行大量工作流实例。
宿主还可以做其他事情。例如,它可以控制在工作流需要长久等待时应用的策略,侦听特定事件并将它们传达给用户或管理员,设置超时并重试每个工作流,公开性能计数器,以及写入日志信息以用于调试和诊断。
主机通过在启动时向容器注册的预定义和自定义服务来完成大多数附加任务。示例宿主没有做上述任何一件事情,并且仅限于启动工作流实例。这在很多常见情况下是可以接受的。
让我们后退一步,并且在工作流项目处于活动状态时分析一下 Visual Studio 2005 工具箱。图 11 中显示的工具箱列出了可用来设计步骤的顺序及其相互关系,以便形成工作流模型的活动。
表 2 提供每个活动的简短说明,以及这些活动适用于哪些方案。
表 2. Windows Workflow Foundation 构造块 | |
活动 | 说明 |
Code |
使您能够向工作流中添加 Microsoft Visual Basic .NET 或 C# 代码以执行自定义操作。但是,这些代码不应该用对 Web 服务等外部资源的依赖性来阻塞工作流。 |
Compensate |
使您能够在发生错误时调用代码来撤消或者补偿已经由工作流执行的操作。通常,对于现在已被取消的操作,您可能希望向先前已经获得成功通知的用户发送电子邮件。 |
ConditionedActivityGroup (CAG) |
使您的工作流能够基于特定于每个活动的准则有条件地执行一组子活动,直到针对 CAG 整体满足完成条件。子活动相互独立并可能并行执行。 |
Delay |
使您能够控制工作流的定时以及将延迟内置到工作流。您可以在 Delay 活动上提供超时,以便工作流在恢复执行之前暂停。 |
EventDriven |
代表一系列其执行由事件触发的活动。第一个子活动必须能够等待外部事件。可行的首要子活动是 EventSink 和 Delay。在这种情况下,Delay 用作超时。 |
EventSink |
在向 WorkflowRuntime 注册的数据交换服务引发指定事件时,使工作流能够从该服务接收数据。 |
ExceptionHandler |
使您能够处理指定类型的异常。ExceptionHandler 活动是其他活动的包装,在指定的异常发生时,这些活动实际执行所需的任何工作。可根据情况指定一个用于存储异常的本地变量,并且使其可以在代码隐藏中使用。 |
IfElse |
使您的工作流能够有条件地执行多个可供选择的分支之一。可在每个分支上放置一个条件,而条件为真的第一个分支将执行。无需在最后一个分支上放置条件,因为它被视为“else”分支。 |
InvokeMethod |
使您的工作流能够调用接口上的方法,以便将消息从工作流发送到向 WorkflowRuntime 注册的数据交换服务。 |
InvokeWebService |
使您的工作流能够调用 Web 服务方法。您需要指定要使用的代理类(使用 WSDL),以及您想要调用的方法的名称。同步和异步调用都受到支持。 |
InvokeWorkflow |
使您的工作流能够调用或启动另一个工作流(可达到任意深度)。例如,被调用的工作流可以调用第三个工作流,该工作流又可以调用第四个工作流,等等。递归调用不受支持。受支持的调用模型是发后不理。 |
Listen |
使工作流能够等待(可能存在的)多个事件之一,或者在指定的超时间隔之后停止等待,并且基于结果分支。可向每个分支中添加一个或多个由事件驱动的活动。只有第一个满足条件的分支被执行;其他分支都不会运行。 |
Parallel |
使您的工作流能够相互独立地执行两个或更多个操作。该活动在继续执行之前会等待这些操作终止。 |
Policy |
使您能够表示或执行规则集合。该活动不在工具箱中;要访问它的功能,必须创建自定义活动并使用派生。 |
Replicator |
使您的工作流能够创建给定活动的任意多个实例,并且顺序或同时执行它们。 |
SelectData |
使您的工作流能够通过在外部数据源对象上定义的方法查询外部数据。当触发 SelectData 活动时,关联的方法将在宿主线程内部执行。该方法返回的值被传递给工作流。 |
Sequence |
使您能够协调一组子活动的连续执行。该序列在最后一个子活动完成之后完成。 |
SetState |
使您的状态机工作流能够指定向新状态的转换。 |
State |
表示状态机工作流中的状态。 |
StateInitialization |
在 State 活动中,用作在状态转换时执行的子活动的容器。 |
Suspend |
挂起工作流的操作,以便能够在发生某个错误条件时进行干预。当工作流实例挂起时,将记录错误。可指定一个消息字符串来帮助管理员诊断发生了什么事情。与当前实例关联的所有状态信息都被保存,并且这些信息会在管理员继续执行时恢复。 |
Terminate |
使您能够在发生任何异常情况时立即结束工作流的操作。如果是在 Parallel 活动内部调用,则所有分支都被突然终止,而无论它们的当前状态如何。当工作流终止时,会记录错误,并提供一个消息以帮助管理员弄清楚发生了什么事情。 |
Throw |
使您能够引发指定类型的异常。使用该活动等效于在用户代码中引发异常的代码处理程序。该活动是引发 .NET 异常的声明性方式。 |
TransactionalContext |
事务上下文 是用于对活动进行分组的块。该活动主要用于事务性执行、补偿和异常处理,可以根据情况进行同步。通过同步事务性上下文,可确保对活动中共享数据的任何访问都将正确地序列化。 |
UpdateData |
使您的工作流能够通过在外部数据源对象上定义的方法更新数据存储区。当 UpdateData 活动被触发时,关联的方法将在宿主线程内部执行。 |
WaitForData |
使您的工作流能够从外部数据源对象接收信息。当传入的数据修改绑定数据源的状态时,该活动被触发。传入的数据是通过绑定数据源服务接收的。 |
WaitForQuery |
使外部应用程序能够在您的工作流中查询数据。该活动将在从宿主收到查询之前一直等待。来自外部应用程序的查询使用绑定数据源服务上的方法提交给工作流。 |
WebServiceReceive |
使作为 Web 服务本身公开的工作流能够接收 Web 服务请求。 |
WebServiceResponse |
使作为 Web 服务本身公开的工作流能够响应 Web 服务请求。 |
While |
使您的工作流能够在一个条件被满足时执行一个或多个活动。在每次迭代之前,都评估该条件。如果为真,则所有子活动都会执行;否则,该活动完成。可指定声明性条件或代码条件。 |
活动表示使用 Windows Workflow Foundation 进行工作流编程的声明性方法。使用活动,可在设计时创作工作流模型并将值分配给每个活动的属性。如果选择带有代码分隔功能的工作流项,则最后的结果会作为 XML 标记保存到具有 .xoml 扩展名的工作流标记文件中。否则,创作的模型将作为对工作流对象模型的一系列调用持久保存在设计器生成的 C# 或 Visual Basic .NET 类文件中。前一种方法类似于 ASP.NET 页,而后一种方法类似于 Windows 窗体应用程序所采用的方法。
Visual Studio 2005 隐藏了这两种方法之间的大多数差异。您总是以可视方式设计工作流,并且 Visual Studio 2005 透明地将您的工作持久保存为两种不同格式中的一种。如果您选择采用“仅代码”解决方案(没有 XOML 和代码分隔),则可调整设计器代码以使其变得更加灵活一些。例如,可让它从配置文件或数据库中读取参数的默认值。如果选择采用工作流标记和代码分隔,则在工作流的代码及其模型之间产生巧妙的分隔。
是否可以用编程方式修改工作流模型?在设计时,可在 Visual Studio 中以编程方式对工作流做您可以做的所有事情。在运行时,对活动集合进行动态更新也是可以的,而这为您提供了对正在运行的工作流实例进行更改的能力。动态更改由在设计时未知的业务更改激发,或由首先修改然后完成业务过程的业务逻辑需要激发。在任何情况下,它都应该只涉及有限的更改 — 完善而不是重新设计。
动态更新适用于应用程序上下文中的单个工作流实例。同一工作流类型的将来实例将不会受到更改的影响。对工作流实例的动态更新可以从工作流实例本身中进行,也可以从您的应用程序代码外部进行。
Windows Workflow Foundation 框架支持 Web 服务互操作性,这包括能够将工作流作为 Web 服务向 ASP.NET 客户端和其他工作流公开。Windows Workflow Foundation 支持将工作流作为在 Microsoft IIS 6.0 上运行 ASP.NET 的 Web 服务器或服务器场上的 ASP.NET Web 服务公开。
Windows Workflow Foundation 框架活动集包含 WebServiceReceive 和 WebServiceResponse 活动,这使工作流能用作 Web 服务终结点。
要想作为 Web 服务公开,工作流必须包含 WebServiceReceive 活动,以便从客户端获得传入的调用。快捷菜单命令将工作流作为 Web 服务发布,如图 12 所示。
Windows Workflow Foundation 中可扩展性的要点是创建自定义活动,因为这使您可以扩展用于生成工作流模型的构造块集。
让我们研究一下活动的内部体系结构,方法是开发一个自定义活动来发送电子邮件。Windows Workflow Foundation 为自定义活动提供一个现成的 Visual Studio 2005 模板。它的名称为 Workflow Activity Library。该模板创建一个可任意重命名的 C# 文件 — 例如,可将其重命名为 SendMailActivity。活动是从父类继承的普通类。可从任何现有活动(无论它是内置的活动,还是您自己创建或从第三方供应商购买的活动)派生您的活动。显然,父类向新的组件中添加了预定义的行为。要完全从头开始生成活动,请让其从 Activity 派生。下面的代码示例显示新类的主干。
public partial class SendMailActivity : System.Workflow.ComponentModel.Activity { public SendMailActivity() { InitializeComponent(); } protected override Status Execute(ActivityExecutionContext context) { : } }
正如您可以猜到的那样,Execute 方法是该组件的核心 — 即完成该组件的核心任务的位置。
在开发之后,活动就被放到工具箱中,以供拖放操作将其拖放到新的工作流应用程序中。尽管属性列表不是必需的,但不带属性的活动几乎没有任何用处。要添加属性,您需要在设计器中选择正在开发的活动,然后单击 Properties 窗格上的 Activity Properties 项(参见图 13)。
向活动中添加属性与向工作流中添加参数并无太大的不同。必须做的工作就是为每个需要的属性配置名称和属性。图 14 显示如何向 SendMail 活动中添加 To 属性。
为了完成所有工作,我们添加了其他属性(如 From、Subject、Body 和 Host),以便用户可以完整地配置要发送的电子邮件。当您添加属性时,向导会修改包含活动的逻辑在内的 C# 代码隐藏文件。
最后一个步骤是使 Execute 方法变得充实一些,以指示它在执行该活动时发送电子邮件。
protected override Status Execute(ActivityExecutionContext context) { MailAddress toAddress = new MailAddress(To); MailAddress fromAddress = new MailAddress(From); MailAddressCollection addresses = new MailAddressCollection(); addresses.Add(toAddress); MailMessage msg = new MailMessage(fromAddress, toAddress); msg.Subject = Subject; msg.Body = Body; SmtpClient mail = new SmtpClient(Host); mail.Send(msg); return Status.Closed; }
如果在工作流解决方案的内部开发活动项目,则工作流文档将自动查找工具箱中列出的新活动,如图 15 所示。否则,必须通过右键单击工具箱来添加它。
图 16 说明 SendMail 活动确实有效。
让我们看一下如何组合表 2 中列出的一些活动,从而解决一项更为现实的任务。假设有这样一个业务应用程序,其中,订单在完成之前可能要经历多个状态。在典型的方案中,有一些根据当前状态指示订单中可能发生某些事件的规则。例如,可以处理或更新未完成的订单,但不能将其取消或发送。
当事件发生时,状态机工作流将转换订单的状态。例如,当订单未完成并且 BeingProcessed 事件发生时,状态机工作流会将订单转换到正确的状态。图 17 显示示例订单状态机工作流的关系图。
让我们首先创建一个状态机工作流。您将使用 State 活动对订单的可能状态进行建模。然后,通过使用 EventDriven 活动指定可以从每个状态发生的事件。通过自定义服务产生的外部事件将转换订单的状态。要执行转换,需要使用 SetState 活动。创作工作流后,需要使用 Windows 窗体宿主应用程序来检验它。
工作流通过专门为此目的建立的服务与外部世界通信。该服务会引发工作流内的事件驱动活动将挂钩到的事件。同样,该服务公开了供该工作流调用的公共方法并向主机发送数据。方法和事件在接口中定义。该接口也称为数据交换服务。每当工作流与外部组件交互(进行输入和输出)时,您都需要该服务。
数据交换服务是常规的 .NET 类库,它最起码包含一个接口定义以及一个实现该接口的类。该接口是为您希望表示的任务定制的。在此情况下,状态机表示订单的生存期,该接口包含五个事件。
[DataExchangeService] public interface IOrderService { event EventHandler OrderCreated; event EventHandler OrderShipped; event EventHandler OrderUpdated; event EventHandler OrderProcessed; event EventHandler OrderCanceled; }
[DataExchangeService] 属性将 IOrderService 标记为数据交换服务接口,以便工作流运行库知道它将用来与工作流实例交换数据。在此情况下,宿主将向一串 EventDriven 活动引发事件,从而向工作流实例发送数据。如果需要,可通过 InvokeMethod 活动从工作流实例内部调用 IOrderService 接口中的方法。
该接口中的事件声明使用泛型,这是 .NET Framework 2.0 中的一项非常热门的新功能。EventHandler 类是一个委托,它表示用于处理事件的函数的原型。在 .NET Framework 1.x 中,EventHandler 按如下方式定义。
void EventHandler(object sender, EventArgs e)
要使该事件传递自定义数据结构(例如,OrderEventArgs),必须创建一个新的委托并使用它来替代 EventHandler。下面是一个示例。
delegate void OrderEventHandler(object sender, OrderEventArgs e)
该模式在 .NET Framework 2.0 中仍然有效。然而,.NET Framework 2.0 中泛型的出现使您无需显式定义(和实例化)新的委托类即可获得相同的结果。您将使用 EventHandler 委托的泛型版本,其中,事件数据的类型是参数。
事件传递 OrderEventArgs 类型的客户端数据,该类型是一个从 Windows Workflow Foundation WorkflowMessageEventArgs 类派生的自定义类,后者在同一个程序集中按如下方式定义。
[Serializable] public class OrderEventArgs : WorkflowMessageEventArgs { private string _orderId; public OrderEventArgs(Guid instanceId, string orderId) : base(instanceId) { _orderId = orderId; } public string OrderId { get { return _orderId; } set { _orderId = value; } } }
下一步,需要定义一个实现该接口的类。该类所引发的公共方法与接口中引发的事件一样多。
public class OrderService : IOrderService { public OrderService() { } public void RaiseOrderCreatedEvent(string orderId, Guid instanceId) { if (OrderCreated != null) OrderCreated(null, new OrderEventArgs(instanceId, orderId)); } public void RaiseOrderShippedEvent(string orderId, Guid instanceId) { if (OrderShipped != null) OrderShipped(null, new OrderEventArgs(instanceId, orderId)); } public void RaiseOrderUpdatedEvent(string orderId, Guid instanceId) { if (OrderUpdated != null) OrderUpdated(null, new OrderEventArgs(instanceId, orderId)); } public void RaiseOrderProcessedEvent(string orderId, Guid instanceId) { if (OrderProcessed != null) OrderProcessed(null, new OrderEventArgs(instanceId, orderId)); } public void RaiseOrderCanceledEvent(string orderId, Guid instanceId) { if (OrderCanceled != null) OrderCanceled(null, new OrderEventArgs(instanceId, orderId)); } public event EventHandler OrderCreated; public event EventHandler OrderShipped; public event EventHandler OrderUpdated; public event EventHandler OrderProcessed; public event EventHandler OrderCanceled; }
现在,需要用订单服务编译该程序集,并重新切换到状态机工作流项目。在该工作流项目中,首先需要添加对新创建程序集的引用。接下来,需要添加四个 State 活动并且按如下方式命名它们: WaitingForOrderState、OrderOpenState、OrderProcessedState、OrderCompletedState。
表 3 表示工作流的状态关系图。每个状态都有一些能够导致向另一个状态进行转换的事件。
表 3. 订单的示例状态机 | ||
状态 | 受支持的事件 | 转换到 |
WaitingForOrderState |
OrderCreated |
OrderOpenState |
OrderOpenState |
OrderUpdated |
OrderOpenState |
OrderProcessed |
OrderProcessedState |
|
OrderProcessedState |
OrderUpdated |
OrderOpenState |
OrderCanceled |
Terminate 活动 |
|
OrderShipped |
OrderCompletedState |
|
OrderCompletedState |
要实现该关系图,需要向每个 State 活动中添加与该表中受支持事件相同数目的 EventDriven 块。例如,名为 WaitingForOrderState 的 State 活动将包含单个 EventDriven 活动(该活动的名称可以是任意的,例如 OrderCreatedEvent)。如图 18 所示,EventDriven 活动嵌入一个 EventSink 活动和一个 SetState 活动,以便捕获外部事件并转换到新的状态。
图 18. OrderCreatedEvent EventDriven 活动的内部视图
在 EventSink 活动的 Properties 窗格上,可选择自己喜欢的数据交换服务(在此情况下为 IOrderService 接口)以及要预订的事件名称。如果单击 EventSink 活动 Properties 窗格上的 InterfaceType 项,则 Visual Studio 2005 将提供该项目可用的数据交换服务列表。选择服务后,EventName 属性反映由该服务所公开事件的列表。可选择自己感兴趣的事件并继续。对于 OrderCreatedEvent 活动,可选择 OrderCreated 事件。
SetState 活动将状态机转换到由其 TargetState 属性指示的新状态。图 18 中的 SetState 活动设置为 OrderOpenState。
可对表 3 中的所有状态和事件接收器重复执行上述操作。最后,您的工作流应该如图 19 所示。
图 19. 最终完成的订单状态机
最后一个步骤涉及到生成 Windows 窗体应用程序以测试该工作流。用户界面包含一个用于跟踪所有未完成订单的列表视图,以及用于创建新订单的文本框和按钮。其他按钮将用来更新、处理和终止该订单。
状态机工作流在 Form_Load 事件中初始化。状态机工作流的初始化要比顺序工作流复杂一些,尤其是当您希望能够跟踪状态更改的时候。下面的代码示例显示如何初始化工作流运行库。
private void StartWorkflowRuntime() { // Create a new Workflow Runtime for this application _runtime = new WorkflowRuntime(); // Register event handlers for the WorkflowRuntime object _runtime.WorkflowTerminated += new EventHandler(WorkflowRuntime_WorkflowTerminated); _runtime.WorkflowCompleted += new EventHandler(WorkflowRuntime_WorkflowCompleted); // Create a new instance of the StateMachineTrackingService class _stateMachineTrackingService = new StateMachineTrackingService(_runtime); // Start the workflow runtime _runtime.StartRuntime(); // Add a new instance of the OrderService to the runtime _orderService = new OrderService(); _runtime.AddService(_orderService); }
StateMachineTrackingService 在运行库之上工作,并且用跟踪工作流中状态更改的功能来扩展它。数据交换服务的实例还被添加到运行库中。
当用户单击以创建新订单时,将执行下面的代码。
private Guid StartOrderWorkflow(string orderID) { // Create a new GUID for the WorkflowInstanceId Guid instanceID = Guid.NewGuid(); // Load the OrderWorkflows assembly Assembly asm = Assembly.Load("OrderWorkflows"); // Get a type reference to the OrderWorkflows.Workflow1 class Type workflowType = asm.GetType("OrderWorkflows.Workflow1"); // Start a new instance of the state machine with state tracking support StateMachineInstance stateMachine = _stateMachineTrackingService.RegisterInstance(workflowType, instanceID); stateMachine.StateChanged += new EventHandler(StateMachine_StateChanged); stateMachine.StartWorkflow(); _stateMachineInstances.Add(instanceID.ToString(), stateMachine); // Return the workflow GUID return instanceID; }
首先,代码实例化工作流实例并注册状态更改的事件处理程序。请注意,使用 .NET Reflection 来获得类型信息并不是绝对需要的,但这可以大大提高灵活性。普通的旧运算符 typeof 也可以很好地将工作流实例的类型传递给工作流运行库。
图 20 显示正在工作的示例应用程序。按钮是基于所选工作流实例的状态而启用的。
当用户单击给定按钮时,通信接口上的相应事件将被引发并被工作流的事件接收器捕获。例如,对处于挂起状态的工作流实例而言,其 Order Processed 按钮的单击事件将按如下方式处理。
private void btnOrderEvent_Click(object sender, EventArgs e) { // Get the name of the clicked button string buttonName = ((Button)sender).Name; // Get the GUID of the selected order Guid instanceID = GetSelectedWorkflowInstanceID(); // Get the ID of the selected order string orderID = GetSelectedOrderID(); // Disable buttons before proceeding DisableButtons(); // Determines what to do based on the name of the clicked button switch(buttonName) { // Raise an OrderShipped event using the Order Local Service case "btnOrderShipped": _orderService.RaiseOrderShippedEvent(orderID, instanceID); break; // Raise an OrderUpdated event using the Order Local Service case "btnOrderUpdated": _orderService.RaiseOrderUpdatedEvent(orderID, instanceID); break; // Raise an OrderCanceled event using the Order Local Service case "btnOrderCanceled": _orderService.RaiseOrderCanceledEvent(orderID, instanceID); break; // Raise an OrderProcessed event using the Order Local Service case "btnOrderProcessed": _orderService.RaiseOrderProcessedEvent(orderID, instanceID); break; } }
该工作流中引发的事件由图 21 中所示的 EventDriven 活动捕获。
EventSink 活动捕获该事件,并且通过转换到 SetState 活动所设置的状态来处理它。工作流中的状态更改由附加的状态跟踪服务检测,并通过 StateChanged 事件报告给宿主,如上述代码清单所示。
您可以在 http://msdn.microsoft.com/workflow 找到本文中讨论的全部示例的完整源代码,以及更多的工作流内容。
Windows Workflow Foundation 旨在成为新的和现有的 Microsoft 产品的工作流框架,它向所有需要为 .NET 平台创建工作流驱动应用程序的开发人员提供了 WinFX 的强大功能和 Visual Studio 2005 的易用性。
Windows Workflow Foundation 为工作台带来的主要好处是统一的工作流模型和一组能够取代很多专用库的工具。在这方面,Windows Workflow Foundation 对于目前工作流产品的供应商也具有重要意义,因为采用 Windows Workflow Foundation 则意味着他们不必再维护其低级别的代码,并且可以集中力量去完成更高级别的任务。
Windows Workflow Foundation 是一种面向多种特定应用程序和需要的工作流技术。Windows Workflow Foundation 因而成为一种广泛的框架,它是为提高每个级别的可扩展性而设计的。这种形式的可扩展性的最佳示例是自定义活动和可插接的运行库服务。自定义活动使您可扩展可用来创作工作流的构造块集。可更改持久性存储和跟踪等运行库服务以适应应用程序的环境,并可使应用程序将数据持久存储到 Microsoft SQL Server 或其他供应商的数据库中。
Windows Workflow Foundation 的 Visual Studio 2005 扩展将允许对工作流进行可视化建模和直接代码访问。
还可以在其他设计环境中承载可视化设计器,从而使设计器提供商可以将可视化建模功能嵌入到其自己的环境中,并且提供应用程序用户所熟悉的用户体验。
本文仅仅讨论了所有 Windows Workflow Foundation 技术和功能中的一些粗浅知识,提供了有关其工作方式、内部原理的概述和一些有代表性的示例代码。
关于作者
Dino Esposito 是一位居住于意大利罗马的培训师和顾问。作为 Wintellect 团队的成员,Dino 专门研究 ASP.NET 和 ADO.NET,并且花费大部分时间在欧洲和美国进行教学和咨询活动。值得一提的是,Dino 为 Wintellect 管理 ADO.NET 课件,同时为 MSDN Magazine 撰写 Cutting Edge 专栏。