WinFX 工作流:使用 Windows Workflow Foundation 的声明性模型简化开
Don Box和 Dharma Shukla
本文基于 WinFX 的预发布版本。文中包含的所有信息均有可能变更。
本文讨论:
|
本文使用以下技术: |
本页内容
工作流 101 | |
工作流内部 | |
复合活动 | |
活动执行内部 | |
正常活动执行 | |
上下文和状态管理 | |
我们所处的位置 |
WinFX 包括一项名为 Windows Workflow Foundation 的技术,该技术使程序可以表示为声明性的、长期运行的过程,这些过程被称作工作流。与传统的 Microsoft? .NET Framework 程序不同,基于工作流的程序通常在声明性的可扩展应用程序标记语言(Extensible Application Markup Language,XAML)文档中指定,该文档根据特定于域的活动指定程序的结构。这些活动通常在传统的基于公共语言运行库 (CLR) 的编程语言(如 C# 或 Visual Basic?)中实现。
WinFX? 提供了一组涵盖了大部分控制流构造的通用活动,不过用户完全可以忽略它们,再编写一整套新的活动,使之准确地适用于要解决的问题域。更常见的情况是,工作流程序将 WinFX 提供的活动用于基本的控制流和程序结构,并且将自定义的用户定义的活动用于特定于域的功能。
除了支持用基于 XAML 的复合方法创建程序之外,基于工作流的程序还获益于一组更丰富的运行时服务,而不是从传统的基于 CLR 的程序中获益。WinFX 工作流运行库可以寄宿在任何 CLR AppDomain 中。运行库允许从内存中删除工作流(一种被称为钝化的技术)并在以后重新加载和恢复,无需开发人员编写明确的状态管理逻辑。工作流运行库还提供用于处理错误以及补偿事务的通用工具,以允许将自动或自定义的撤消逻辑指定用于长期运行的工作单元。此外,您还可以利用管理服务,它们允许通过事件、跟踪或查询工作流状态来检查给定工作流程序的状态。
工作流 101
在 WinFX 中,工作流可以使用声明性 XAML 或任何使用针对 .NET 的语言的命令性代码来表示。使用 XAML 使得在可视化设计器中开发和修改工作流无需传统的 C# 或 Visual Basic 代码。工作流可以完全用 XAML 定义,或者使用 XAML 以及一个包含 C# 或 Visual Basic 代码的代码隐藏文件定义。纯 XAML 工作流可以直接在运行时加载,而无需单独的编译步骤,因此具有优势。为了证明这一点,在本文中需要的位置我会使用 XAML。在所有示例中,我们提供的 XAML 都可以用 C# 或 Visual Basic 表示,尽管这样会更加冗长。
创建一个工作流程序也就是产生该程序的正确 XML 描述。对于某一类用户而言,在文本编辑器中创建并编辑 XML 是一种合适的(有时是理想的)解决方案。对于大部分更喜欢可视化设计器的用户而言,WinFX 包括一个可视化工作流设计器,它可以嵌入到任何基于 Windows 的应用程序中(如图 1 所示)。再强调一次,出于说明的目的,本文以基础 XAML 窗体的形式显示工作流程序,但在所有示例中,都可以使用工作流设计器创建和编辑 XAML。
要允许开发人员使用 C# 或 Visual Basic 创建并编辑工作流,工作流设计器可以在 Visual Studio? 2005 中使用,包括与 Visual Studio 项目系统和调试器集成。与 Visual Studio 调试器集成使您可以使用熟悉的 F5/F10/F11 键绑定,在单个调试会话中调试工作流可视化和底层 C# 或 Visual Basic 代码(如图 2 所示)。
工作流内部
基本上,工作流是一个特定于域的程序语句(称之为活动)树。活动的概念是工作流体系结构的核心 — 您应该将活动视为特定于域的操作码,将工作流视为根据这些操作码编写的程序。
WinFX 提供了一系列可以广泛应用的活动,本文讨论其中的一部分。WinFX 还允许您用 XAML 或与 CLR 兼容的语言编写自己的特定于域的活动。WinFX 活动只是从 System.Workflow.ComponentModel.Activity 派生(直接或间接)的一个 CLR 类型。例如,以下 C# 代码定义一个工作(但无用)的 WinFX 活动:
using System; using System.Workflow.ComponentModel; // bind an XML namespace to our CLR namespace for XAML [assembly: XmlnsDefinition( "http://schemas.example.org/MyStuff", "MyStuff.Activities")] namespace MyStuff.Activities { public class NoOp : Activity {} }
要使用该活动,可以用 XAML 编写一个简单的工作流,如以下代码行所示:
<my:NoOp xmlns:my="http://schemas.example.org/MyStuff" />
该工作流由只包含一个活动的树组成。由于我们的活动不执行任何操作,因此我们加载或运行该工作流时不会发生任何事情。更准确地说,工作流运行库加载活动树,并调用根活动上的 Execute 方法。因为该活动没有重写 Execute 方法,所以不发生任何事情。
要更好地处理基于工作流的程序的运行方式,可以定义一个较为有趣的活动,如图 3 所示。该活动有两个明显的特征。其一,它定义了一个名为 Text 的公共属性,该属性可以在基于 XAML 的工作流中初始化。更重要的是,它重写了 Execute 虚方法,并通过向控制台打印 Text 属性进行响应。该活动就绪后,我们就可以编写执行某些任务的工作流了:
<my:WriteLine Text="Hello, world from WinFX." xmlns:my="http://schemas.example.org/MyStuff" />
该代码运行后,工作流程序打印出字符串“Hello, world from WinFX.”
与所有工作流程序一样,前面的工作流未指定运行环境。相反,是由宿主环境来决定(例如,SharePoint?、ASP.NET、自定义应用程序服务器或 Windows? 外壳程序)加载并运行工作流。工作流运行库在任何环境中都是可嵌入的。运行库通过 System.Workflow.Runtime.WorkflowRuntime 类向宿主环境公开。导致工作流激活的是一个两阶段过程:首先,实例化 WorkflowRuntime 并调用 CreateWorkflow 方法,将 XAML 定义作为 XML 传递,或者传递编译的活动类型,从而在内存中加载程序。WorkflowRuntime.CreateWorkflow 返回一个 WorkflowInstance 类型的对象,它是内存中工作流程序实例的句柄。接下来,为了开始实际的工作流执行,只需调用 WorkflowInstance.Start。以下代码显示如何承载工作流运行库,并从其 XAML 定义运行工作流:
// create and start an instance of the workflow runtime WorkflowRuntime runtime = new WorkflowRuntime(); runtime.StartRuntime(); // get an XML reader to the XAML-based workflow definition XmlReader xaml = XmlTextReader.Create("myworkflow.xaml"); // create a running instance of our workflow WorkflowInstance instance = runtime.CreateWorkflow(xaml); instance.Start();
WorkflowInstance.Start 调用创建运行工作流所需的初始数据结构,然后向宿主环境返回控件。工作流表示的实际工作将由运行库使用主机提供的线程或 CLR 线程池进行异步安排。
复合活动
前面显示的 WriteLine 活动是原子活动的一个示例,即该活动将其所有执行逻辑作为不透明代码包含。WinFX 工作流编程模型还支持复合活动的概念,它是根据一个或多个子活动实现其执行逻辑的活动。复合活动从 CompositeActivity 类派生,并且有一个很有名的属性 (Activities),该属性包含了一些子活动。在 XAML 中,这些子活动表示为复合活动的子元素。
WinFX 包括一组简化工作流生成的常用复合活动。我们要研究的第一个活动是 SequenceActivity,如下所示:
<SequenceActivity xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow" xmlns:my="http://schemas.example.org/MyStuff" > <my:WriteLine Text="One"/> <my:WriteLine Text="Two"/> <my:WriteLine Text="Three"/> <my:WriteLine Text="Four"/> </SequenceActivity>
运行时,该工作流编写以下控制台输出:
One Two Three Four
SequenceActivity 依次执行每个子活动。为了支持条件执行,WinFX 为名为 IfElseActivity 的条件执行提供了一个活动。
IfElseActivity 包含一个或多个子活动,每个子活动将一个布尔表达式绑定到一系列活动,当且仅当该布尔表达式值为 true 时执行这些活动。图 4显示一个使用 IfElseActivity 的简单示例。
该代码对应于图 5 中所示的可视化设计器视图。请注意,该 XAML 文件使用 x:Class 属性来表示文件正在定义一个新类型 (MyNamespace.MyWorkflow),而不是现有类型的一个实例。使用 x:Class 使我们拥有了一个包含 C# 或 Visual Basic 代码的代码隐藏文件,如图 4所示。
图 5 MyWorkflow.xaml 的可视化表示
在本示例中,我们定义了两个条件表达式(Is05 和 Is06),它们由工作流定义引用,将标准的 XAML 符号用于基于符号名编写方法。
IfElseActivity 的语义很简单。每个子 IfElseBranchActivity 有一个条件表达式,该表达式的值决定执行哪个分支。条件表达式的值为 true 的第一个IfElseBranchActivity 将获得运行。如果条件表达式的值都不为 true,则选择没有条件的最后一个分支(这就是本例中工作流的情况)。上面显示的工作流等同于以下 C# 代码:
if (DateTime.Now.Year == 2005) Console.WriteLine("Circa-Whidbey"); else if (DateTime.Now.Year == 2006) Console.WriteLine("Circa-Vista"); else Console.WriteLine("Unknown era");
尽管基于工作流版本的程序更加冗长,但也更透明、更易于更改(尤其是对于不使用 C# 的程序员),并且可以利用工作流运行库的服务,如程序挂起、脱水和补偿。
使用 IfElseActivity 的工作流示例将 CodeConditions 用于其布尔表达式。表达式还可以用纯 XAML 编写,无需依赖单独的代码隐藏文件(语法细节超出了本文的讨论范围)。使用基于 XAML 的表达式使工作流中使用的条件可以用声明性格式表示,该格式适用于可视化设计器,但不适用于特定于给定编程语言的单独的文本代码编辑器。
除排序和条件外,WinFX 还包括为迭代建模的活动:WhileActivity。与 IfElseBranchActivity 一样,WhileActivity 有一个 Condition 属性和一组子活动。图 6 显示一个将 WhileActivity 以及代码隐藏文件用于该活动的简单工作流。
通过使用该代码,工作流将打印十遍“Hello, WinFX”消息。请注意,我们在该示例中使用了 WinFX 提供的 CodeActivity,以使底层 IncrementCounter 方法在 while 循环体的结尾处执行。
除了模仿传统命令式编程语言的三种基本复合活动以外,WinFX 还提供了一些更特殊的活动,这些活动支持向前链接的规则赋值、事件-条件-活动 (ECA) 工作流,以及并行。后者使用 WinFX ParallelActivity 表达最为直接。考虑以下 XAML 片断,它创建了执行的两个并行分支:
<ParallelActivity xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow" xmlns:my="http://schemas.example.org/MyStuff" > <SequenceActivity> <my:WriteLine Text="One"/> <my:WriteLine Text="Two"/> </SequenceActivity> <SequenceActivity> <my:WriteLine Text="Three"/> <my:WriteLine Text="Four"/> </SequenceActivity> </ParallelActivity>
运行时,两个顺序分支计划以并行方式运行,ParallelActivity 的执行逻辑决定其顺序。有了该 XAML,以下输出即为合法的:
One Three Two Four
以下也为合法输出:
Three One Four Two
ParallelActivity 不保证分支之间准确的执行顺序。但是,直到所有分支都执行完成后,ParallelActivity 才能结束。这意味着,给定图 7 中所示的 XAML 片断,输出的第一行始终为“Zero”,第六行也就是最后一行输出始终为“Five”。干预输出也是不确定的。
活动执行内部
Windows Workflow Foundation 的一个主要体系结构原则是,特定的程序语义始终是单个活动的责任,决非工作流运行库的责任。该体系结构希望开发人员和域专家定义适用于给定应用程序域的自定义活动。要保证特定于域的语义在运行库外,活动以及工作流运行库要通过定义完备的协定进行通信,该协定使用 Activity 基类上的虚方法表示。对于任何作为 WinFX 的一部分提供的活动而言,工作流运行库将其与您创建的活动同等对待。事实上,工作流运行库使用相同的协定与 SequenceActivity 以及自定义活动(如本文使用的 WriteLine 活动)进行交互。
要了解活动与运行库的相关方式,我们看一下每个活动必须实现的状态机。该状态机由 ActivityExecutionStatus 枚举反映,如图 8 所示。此外,Activity 基类还公开了发生转换时引发每个状态的 CLR 事件。
图 8 活动状态机
每个活动其实就是其生命周期中任何给定时刻的六个状态之一 — 初始化、执行、取消、故障、补偿以及关闭。虚线转换表示最终转换到关闭状态,在该状态之后,活动将不再进一步转换。通常情况下,活动状态转换会直接受工作流运行库的影响,或受请求工作流运行库计划其子级转换的父活动的影响。在这两种情况下,工作流运行库配合(或强制)活动从一个状态转换到另一个状态。
工作流运行库和活动之间的协定根据 Activity 基类上以下受保护的虚方法表示:
void Initialize(IServiceProvider provider) ActivityExecutionStatus Execute(ActivityExecutionContext aec) ActivityExecutionStatus HandleFault(ActivityExecutionContext aec) ActivityExecutionStatus Cancel(ActivityExecutionContext aec)
此外,作为工作流运行库活动协定的一部分,可补偿的活动需要实现具有 Compensate 方法的 ICompensatableActivity:
namespace System.Workflow.ComponentModel { public interface ICompensatableActivity { ActivityExecutionStatus.Compensate(ActivityExecutionContextaec) } }
除 Initialize 方法(主机第一次启动工作流时同步调用该方法)之外,所有方法都返回当其将控件返回给运行库时活动的状态。对于可以快速同步执行的操作而言,活动将返回下一个状态(通常为关闭)的枚举值:
protected override ActivityExecutionStatus Execute( ActivityExecutionContext aec) { DoWorkSynchronously(); // indicate that we're now in the Closed state return ActivityExecutionStatus.Closed; }
相反,排入异步工作队列中的活动必须表明它们仍在执行:
protected override ActivityExecutionStatus Execute( ActivityExecutionContext aec) { EnqueueAsynchronousWork(); // indicate that we're still executing return ActivityExecutionStatus.Executing; }
然后,活动负责通知运行库何时转换到关闭状态。这通常是为了响应运行库本身引发的某个事件:
// Activity's event handler that is registered with the runtime void LastStageOfAsyncWork(object sender, EventArgs e) { // grab a reference to the runtime ActivityExecutionContext aec = (ActivityExecutionContext)sender; // inform the runtime that we're now // closed aec.CloseActivity(); }
该事件处理程序演示了活动开发的一个重要方面。运行库每次调用活动时,它都提供对 ActivityExecutionContext 类型本身的引用。该引用是唯一一个用于所使用活动的运行库的 API。更重要的是,传递给您的引用仅在执行 CLR 方法时有效。一旦您的方法将控件返回给运行库,该上下文引用就不再有效。如果您为了将来使用而错误地在域中缓存了引用,运行库将强制其引发异常。
尽管运行库/活动协定本身具有异步性,但运行库决不会并发调用给定的活动。并且,确保任何给定的活动在指定时刻最多执行一个方法调用。这极大地简化了用 C# 或 Visual Basic 这样的语言编写活动的任务。
正常活动执行
在正常情况下,一个活动经历了两次转换:从初始化到执行,以及从执行到关闭。当工作流运行库能够聚合执行活动所需的资源以及计划活动上的 Execute 方法时,发生第一个转换(从初始化到执行)。如果活动的 Execute 方法返回 ActivityExecutionStatus.Closed,则同步发生第二个转换(从执行到关闭);如果Execute 方法返回 ActivityExecutionStatus.Executing,则在活动上的后续事件处理程序运行并调用 ActivityExecutionContext.CloseActivity 以将状态转换传递给运行库时,将异步发生第二个转换。
复合活动的 Execute 方法通常要求工作流运行库计划其子活动的执行,并且支持异步更改子活动的状态。然后,该活动至少在它的一个子活动处于执行状态时仍保持执行状态。
要更好地了解正常活动执行协议,我们可以看看 SequenceActivity 的实现。为简单起见,我们只关注活动的正常执行路径(初始化到执行再到关闭),这样只需研究 Execute 方法。
在 Execute 方法的实现中,SequenceActivity 同意它第一个子活动的关闭事件,并且请求工作流运行库计划它第一个子活动的执行,方法是调用运行时上下文上的 ExecuteActivity 方法,如图 9 所示。
对 ExecuteActivity 的调用使得计划程序使用标准的初始化-执行-关闭顺序运行该子活动。计划了该子活动后,工作流运行库将使子活动处于执行状态,并且调用子活动的 Execute 方法。最后,当子活动进入关闭状态时,Closed 事件的事件处理程序 (SequenceActivity.OnChildClosed) 将由运行库调用,同时传递一个新的 ActivityExecutionContext 作为第一个参数。图 10 显示 SequenceActivity 的事件处理程序定义。
在该事件处理程序中,我们检查是否有剩余的子活动。如果有,我们使用与前面完成例程相同的事件处理程序方法来计划下一个子活动。如果没有其他的子活动要运行,我们通过调用 CloseActivity 方法来通知运行库已准备转换到关闭状态。
上下文和状态管理
细心的读者会注意到,活动通过 ActivityExecutionContext (AEC) 与运行库进行交互。从概念上看,AEC 作为为一个连续的执行环境,活动在其中执行,活动的对象状态自动在该环境中得到管理。工作流运行库允许活动在正常活动执行期间创建新的持续执行环境。需要其子活动执行多次的活动需要为每次执行创建新的 AEC。WhileActivity 是有关此主题的一个规范示例,它模仿了标准的 C while 循环。
图 11 显示为 WhileActivity 的每个迭代生成的多个 AEC。假设程序中的 WhileActivity 迭代三次,则 WhileActivity 中的三个 SequenceActivity 实例在不同的 AEC 中动态执行,每个实例具有自己的状态。通过赋予 while 循环的每个迭代它自己的 AEC,运行库能够在运行补偿逻辑时撤消每个迭代的执行。
图 11 为迭代生成的活动执行上下文
工作流可以长时间运行。在任何给定的时刻,工作流实例都可以被认为是空闲的,这意味着运行库针对工作流的任何活动都没有可运行的工作。当工作流实例空闲时,其状态可以序列化为主机提供的存储,并且可以从内存中删除。该过程被称作钝化 (passivation)。工作流实例的序列化状态可以用来重新激活内存中的实例,此时实例恢复其状态并且可以执行任务。重新激活通常发生在主机检测到新的可用工作时,方法通常是注意到外部事件的发生。
考虑以下简单的工作流程序,它使用内置的 DelayActivity 使得工作流在序列中间处于空闲状态:
<SequenceActivity xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="http://schemas.example.org/MyStuff" > <my:WriteLine Text="One"/> <my:WriteLine Text="Two"/> <DelayActivity TimeoutDuration="00:10:00" /> <my:WriteLine Text="Three"/> <my:WriteLine Text="Four"/> </SequenceActivity>
在这些代码片断中,当 DelayActivity 执行时,它设置一个计时器订阅并返回 ActivityExecutionStatus.Executing。此时,工作流实例没有可运行的工作,因此被认为是空闲的。在空闲时,工作流运行库将试图使用主机提供的持续服务来保持工作流实例的状态。初始化运行库时,宿主环境可以提供抽象 WorkflowPersistenceService 类型的的实现。如果宿主提供了该类型的一个具体实现(通过 WorkflowRuntime.AddService 方法),则运行库将调用主机的 SaveWorkflowInstanceState 方法,假定主机有机会将状态保存到持续存储中。每个工作流实例由唯一的运行库生成的 GUID 识别,存储使用它来识别实例的持续状态以供将来检索。
运行库在空闲时保存工作流实例以提高可靠性。默认情况下,将实例保存在内存中。主机可以将工作流实例从内存中删除,方法是将运行时范围的 UnloadOnIdle 属性设置为 true,或者对指定实例调用 WorkflowInstance.Unload 方法。通过在运行库上注册 WorkflowIdled 事件,主机可以检测到实例何时空闲,如下所示:
WorkflowRuntime runtime = new WorkflowRuntime(); runtime.StartRuntime(); runtime.WorkflowIdled += delegate(object s, WorkflowEventArgs e) { ... };
要强制已保存的工作流从主机的持续服务重新加载到内存中,可以调用 WorkflowRuntime.GetWorkflow 方法,传入实例唯一的 GUID。
我们所处的位置
对于 Windows Workflow Foundation 提供的丰富的工作流编程模型和功能,我们仅仅触及到了皮毛。Windows Workflow Foundation 提供了一种根据活动声明性地、更自然地表达应用程序语义的方式,包括程序控制流、事务、并发、同步、异常处理,以及与其他应用程序交互。除了那些在基础 CLR 中可用的服务之外,它还提供了一组丰富的服务,包括原子程序持久性、补偿事务,以及程序状态的运行时检查。工作流运行库可以寄宿在任何 CLR 应用程序域中,因此可以嵌入到任何应用程序或应用程序容器中。您可以从 Windows Vista and WinFX Beta 下载 WinFX 测试版,并且现在开始编写活动。
Don Box 是 Microsoft 互连系统部门的一名架构师,他致力于编程模型和深入研究以支持生成与其他程序通信的程序。在加入 Microsoft 之前,Don 领导了一小批来自多个国家的 IUnknown 崇拜者。您可以访问 www.pluralsight.com/blogs/dbox 以获得有关 Don 的信息。
Dharma Shukla 在 Microsoft 是 Windows Workflow Foundation 小组的首席开发人员,他负责工作流编程模型以及开发人员工具。他现在还在编写一本有关 Windows 工作流的书籍。您可以访问 www.dharmashukla.com 以获得有关 Dharma 的信息。