WinFX工作流:使用WF的断言(declarative)模型简化开发
WinFX中包括了被称为WWF的新技术,可以将程序表述为断言――也称为工作流的长时执行过程。不象传统的.Net程序,基于工作流的程序通常使用断言式的XAML(Extensible Application Markup Language)文档来表述,在文档中通过一组领域特定的活动来描述程序的结构。这些活动通常使用传统的基于CRL的编程语言来实现,如C#和VB。
WinFX中提供了一些预定义活动的集合以满足大部分程序的需要,但开发者可以随意的越过它们,完全重写新的活动集合以更好地符合问题领域的需要。当然,更可能的是工作流程序使用WinFX预定义的活动来实现基本控制流程和程序结构,而在实现问题领域特定的功能时使用自定义活动。
为了支持以XAML方式实现应用,工作流程序还可以使用比传统CRL程序功能更强大的运行时功能集。WWF运行时可以嵌入到任决的CRL应用程序域中。不需要开发者单独实现状态管理逻辑,运行时允许从内存中移除工作流(称之为钝化)及稍后重载到内存中并恢复执行。运行时还提供了错误处理和冲正的常用处理功能,你可以使用这些自动处理能力,也可以实现自己的Undo逻辑。此外,你还可以通过接收事件、跟踪、查询等管理功能来查看特定工作流实例的状态。
1、 工作流的内部
基本上,工作流是由一组称之为“活动”的与领域相关的程序段组成的树形结构。活动的概念是与工作流架构对应的,你可以将活动视为领域特定的伪代码语句,而工作流就是用这些伪代码语句写成的程序。
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 方法,但什么也看不到(因为我们的活动没有实现任何操作)。
为了更好地理解基于工作流的程序是如何工作的,我们考察一个更有趣的例子:
namespace MyStuff.Activities
{
public class WriteLine : Activity
{
string text;
public string Text
{
get { return text; }
set { text = value; }
}
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext aec)
{
Console.WriteLine(this.Text);
return ActivityExecutionStatus.Closed;
}
}
}
这个活动有两个显著特征:其一是它定义了一个可以由XAML工作流初始化的公有属性Text ,其二,更重要的是重载了Execute虚拟方法并在其中将Text 属性的值打印到控制台。有了这个活动,现在我们可以写一个作某些事的工作流了:
<my:WriteLine Text="Hello, world from WinFX."
xmlns:my="http://schemas.example.org/MyStuff"
/>
当这个工作流执行时,将在控制台上输出字符串“Hello, world from WinFX.”。
象所有的工作流一样,之前的工作流并不指定其执行的环境,相反,它取决于加载并执行工作流的宿主程序的环境(比如:SharePoint®, ASP.NET, 自定义应用服务器, 或是Windows® shell)。工作流运行时可以嵌入到任何执行环境中,它通过System.Workflow.Runtime.WorkflowRuntime类为宿主程序提供操作接口。使一个工作流执行需要两个步骤:首先是加载到内存中,并以XAML文件或是编译好的活动类型库作为参数调用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的线程池中。
2、 复合活动
上面例子中的WriteLine 活动是一个原子活动,也即其执行逻辑视作整体一次性执行的活动。在WinFX工作流编程模型中也支持复合活动的概念,也即其执行逻辑由一或多个子活动具体实现的活动。复合活动从类继承,并有一个公有属性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复合活动包含一个或多个子活动,每一个子活动都绑定了一个布尔表达式,当布尔表达式为真时,相应的子活动将执行。下面是一个使用IfElseActivity复合活动的简单例子:
<!-- MyWorkflow.xaml -->
<SequenceActivity
x:Class="MyNamespace.MyWorkflow"
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"
>
<IfElseActivity>
<IfElseBranchActivity>
<IfElseBranchActivity.Condition>
<CodeCondition Condition="Is05"/>
</IfElseBranchActivity.Condition>
<my:WriteLine Text="Circa-Whidbey"/>
</IfElseBranchActivity>
<IfElseBranchActivity>
<IfElseBranchActivity.Condition>
<CodeCondition Condition="Is06"/>
</IfElseBranchActivity.Condition>
<my:WriteLine Text="Circa-Vista"/>
</IfElseBranchActivity>
<IfElseBranchActivity>
<my:WriteLine Text="Unknown Era"/>
</IfElseBranchActivity>
</IfElseActivity>
</SequenceActivity>
所附的代码文件如下:
using System;
using System.Workflow.ComponentModel;
using System.Workflow.Activities;
namespace MyNamespace
{
public partial class MyWorkflow : SequenceActivity
{
void Is05(object s, ConditionalEventArgs e)
{
e.Result = (DateTime.Now.Year == 2005);
}
void Is06(object s, ConditionalEventArgs e)
{
e.Result = (DateTime.Now.Year == 2006);
}
}
}
下面是与上面代码对应的可视设计器中的视图。应该注意在这个XAML文件中使用了x:Class 属性表示文件定义了一个新类型(MyNamespace.MyWorkflow),而不是创建一个现有类型的实例。使用x:Class 属性也使得我们可以使用XAML附属的C#或VB代码文件。
在这个例子中,我们定义了两个在定义工作流时可以使用标准XAML基于符号名引用的条件表达式(Is05和Is06)。
IfElseActivity 复合活动的语义是相当简单的,其每一个分支的IfElseBranchActivity活动都有一个对应的条件表达式用于确定需要执行哪个分支。按顺序的第一个条件表达式为真的分支活动将被执行,如果没有条件表达式为真并且最后一个分支活动没有提供条件表达式的话,则这个分支将被执行(有些象程序语言的case语句)。这个工作流的执行看起来就象是以下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#程序员),并且通过使用工作流运行时提供的服务有更多的优势,比如挂起、脱水(dehydration)、冲正等。
这个IfElseActivity复合活动的例子使用了条件函数作为其条件表达式,但条件表达式也可以不用写在附带的代码文件中而写成纯XAML(此处不作更多地讨论)。使用XAML表达式还可以使工作流中的条件在可视设计器中以断言形式显示,而不是必需到语言编辑器中才能查看。
除了顺序和条件复合活动之外,WinFX还提供了一个循环复合活动WhileActivity,象条件复合活动一样,WhileActivity复合活动也有一个条件属性,并且有一个子活动的集合,下面是一个使用WhileActivity复合活动的简单的工作流例子:
<!-- MyWorkflow.xaml -->
<SequenceActivity x:Class="MyNamespace.MyWorkflow"
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"
>
<WhileActivity>
<WhileActivity.Condition>
<CodeCondition Condition="LoopTest"/>
</WhileActivity.Condition>
<my:WriteLine Text="Hello, WinFX"/>
<CodeActivity ExecuteCode="IncrementCounter" />
</WhileActivity>
</SequenceActivity>\
其条件表达式代码文件:
using System;
using System.Workflow.ComponentModel;
using System.Workflow.Activities;
namespace MyNamespace
{
public partial class MyWorkflow : SequenceActivity
{
int count;
void LoopTest(object s, ConditionalEventArgs e)
{
e.Result = count < 10;
}
void IncrementCounter(object s, EventArgs e)
{
count++;
}
}
}
执行这个工作流将在控制台上输出“Hello, WinFX”十次。注意在这个工作流中,我们还使用了WinFX提供的CodeActivity活动使我们可以在循环体中执行代码中的IncrementCounter 方法。
除了这三个模拟传统语言控制结构的复合活动,WinFX还提供了更多强大的复合活动以支持前身链规则演算式(forward-chaining rule evaluation)、事件-条件-活动(ECA)工作流及并发。下面是一个使用复合活动创建两个并行执行分支的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 复合活动的执行逻辑调度下并行执行的,所以可能有以下输出:
One
Three
Two
Four
也可能是这样的:
Three
One
Four
Two
ParallelActivity复合活动的子活动没有明确的执行顺序,但是直到所有子活动都执行完毕,ParallelActivity 复合活动才能完成。这意味着以下XAML片断执行时,第一行总是输出“Zero”而最后一行总是输出“Five”,而中间的顺序则是不确定的。
<SequenceActivity
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow"
xmlns:my="http://schemas.example.org/MyStuff"
>
<my:WriteLine Text="Zero"/>
<ParallelActivity>
<SequenceActivity>
<my:WriteLine Text="One"/>
<my:WriteLine Text="Two"/>
</SequenceActivity>
<SequenceActivity>
<my:WriteLine Text="Three"/>
<my:WriteLine Text="Four"/>
</SequenceActivity>
</ParallelActivity>
<my:WriteLine Text="Five"/>
</SequenceActivity>
3、 Activity活动执行的内部
WWF的关键设计原则之一是描述程序语义是活动的职责而不是运行时的职责,开发人员或是领域专家应该完整地定义符合给定应用领域的活动。在执行期间,运行时会使用由活动的基类中的虚拟函数定义的接口与活动通信,以完成领域的特定语义。WWF预定义的活动和自定义的活动对于运行时来说并没有任何不同。
为了更好地理解活动和运行时的交互,我们来看看活动的状态机。这个状态机是通过ActivityExecutionStatus枚举表现出来的,此外,在活动的基类实现中,当发生状态改变时也会点火一个CLR事件。
图表 1 活动Activity的状态机
每个活动在其生命周期的任何时点总是处于下面6个状态中的一个:初始化initialized, 运行executing, 放弃canceling, 失败faulting, 冲正compensating, 和关闭closed。通常,活动的状态变化是由运行时直接改变,或是由直接的父活动向运行时发起请求以改变活动的状态,在这两种情况时,运行时会强制改变活动的状态。
运行时与活动交互的接口定义为活动基类的保护虚拟函数,如下所示:
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 方法(当运行时在Host内首次运行时同步调用此方法),其它方法在返回时都需要返回将控制交回给运行时的时候活动应处的状态。对于可以迅速地同步完成的操作,活动应该返回下一个状态的枚举值(通常是closed):
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的唯一途径。更重要的是,这个运行时引用只在活动的调用方法执行期是合法的,一旦活动的方法将控制交回给运行时,这个运行时引用就不再合法了,如果你调用这个引用试图保存下来以便后继使用的话,运行时将会强制抛出一个异常。
不管运行时与活动的接口是否支持异步特征,运行时决不会同时调用同一个活动。所以,在一个给定的运行时中,任何活动至多只会有一个方法在执行,这大简化了在类似C#或VB这样的语言中开发活动时的难度。
4、 活动的正常执行
正常情况下,一个活动只会发生两次状态变化:一是从初始到执行,二是从执行到关闭。第一次状态变化(从初始到执行)发生在运行时为了活动的执行分配了所需资源并调用了活动的执行方法时。第二次状态变化(从执行到状态)发生在活动的执行方法同步返回ActivityExecutionStatus.Closed,或者是活动的执行方法返回ActivityExecutionStatus.Executing通知运行时活动还需异步执行,并且活动在随后的事件处理器方法中调用了ActivityExecutionContext.CloseActivity 通知运行时状态发生变化。
复合活动的执行方法由于需要请求运行时执行每个子活动的执行方法,通常应该订阅子活动的异步状态变化,只要任一个子活动还处理执行状态,复合活动的状态也应该保持在执行状态。
namespace System.Workflow.Activities
{
public class SequenceActivity : CompositeActivity
{
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext aec)
{
// if we have no children, then transition to closed
if (this.EnabledActivities.Count == 0)
return ActivityExecutionStatus.Closed;
// otherwise, schedule the first child after
// registering for its Closed event
Activity child = this.EnabledActivities[0];
child.Closed += OnChildClosed;
aec.ExecuteActivity(child);
return ActivityExecutionStatus.Executing;
}
...
}
}
void OnChildClosed(object sender, ActivityStatusChangeEventArgs e)
{
ActivityExecutionContext aec = (ActivityExecutionContext)sender;
Activity child = e.ForActivity;
// unregister event handler on child
child.Closed -= OnChildClosed;
// find index of child
int index = EnabledActivities.IndexOf(e.ForActivity);
// if this was the last child, transition to Closed
if (index == EnabledActivities.Count—1)
{
aec.CloseActivity();
}
else
{
// otherwise schedule the next one
child = EnabledActivities[index + 1];
child.Closed += OnChildClosed;
aec.ExecuteActivity(child);
}
}
5、 上下文和状态管理
运行时是通过ActivityExecutionContext (AEC)与活动交互的,概念上,可以把AEC看作一个序列化后的活动的执行环境,在这个执行环境中活动的对象状态是由环境自动管理的。工作流运行时允许活动在正常执行期间创建一个新的持久化的执行环境,这样,需要子活动多次执行的活动就需要为子活动的每次执行创建一个新的AEC,这样作的例子可以参见WhileActivity活动,它类假于C语言中的while循环。
下图是为WhileActivity活动的每次迭代都创建一个AEC的例子。假设这个WhileActivity需要迭代3次,WhileActivity中的3个SequenceActivity 实例可以分别在保留有自己状态的AEC中动态执行。通过这样的安排,当活动提供了冲正逻辑时,运行时可以对每一次迭代进行冲正。
工作流任务可能执行很长时间,一个工作流实例中有可能所有活动都没有可以执行的任务,称之为空闲。一个工作流实例在空闲时,可以将其状态保存到宿主程序提供的存储中,并从内存中移除,这个过程称之为“钝化”(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来标识的,在保存工作流实例状态时,也应相应保存这个GUID以备后来恢复。
运行时在空闲时保存工作流实例的状态也可能是基于可靠性的要求。通常工作流实例是驻留在内存中的,宿主程序可以将运行时的UnloadOnIdle 属性置为真,或是调用工作流实例的WorkflowInstance.Unload方法将工作流实例强制移出内存。宿主程序可以向运行时订阅事件以在工作流实例空闲时得到通知,象这样:
WorkflowRuntime runtime = new WorkflowRuntime();
runtime.StartRuntime();
runtime.WorkflowIdled += delegate(object s, WorkflowEventArgs e)
{
...
};
要将宿主程序保存的工作流实例状态恢复到内存中继续执行,可以以工作流实例的标识GUID为参数,调用WorkflowRuntime.GetWorkflow方法。