WF(Windows Workflow Foundation,Windows工作流基础)为.NET提供了一种基于模型的、声明方式的过程执行引擎,它改变了传统的通过一行行编写代码来开发服务功能的方式。
WF包含三个核心组件:活动框架(activity framework)、运行时环境(runtime environment)、工作流设计器(workflow designer)。
WF不是什么
工作流这个词在软件开发领域和相关社区里已经被“滥用”了。所以弄清楚WF在这些流行的工作流概念中到底指的是哪一种就非常重要。
1. WF不是服务器,虽然可以将工作流功能集中起来然后通过服务器暴露给其他应用程序使用。
2. WF不是一种BPM(Business Process Management,业务流程管理)工具,虽然可以将WF作为工作流执行引擎来创建BPM工具。
3. WCF并不直接面向业务分析人员,虽然可以使用可重新承载的设计器向业务分析人员提供创建自己的工作流的工功能。WF非常灵活,它允许将此功能集成到分析人员熟悉的环境中。如果现有的设计器不能满足要求,还可以创建定制的工作流设计器。
4. WF不是企业应用集成工具,虽然可以将第三方的系统功能封装在活动中,然后将它们加到工作流里。
5. WF不是一个玩具。这可以在具有大量服务器集群的企业级规模环境里高性能地运作。它可以胜任企业级应用,SharePoint Server就是一个很好的应用实例。而WF本身并不是一个企业级应用,它只是一组开发套件。
WF不仅可以用在基于服务器的应用程序里,它还可以用在WinForm应用中执行从服务协调到用户界面定制的任何应用程序逻辑。也可以用在Web应用程序中以管理流程状态。总之,任何可以编写.NET代码的地方都可使用它作为提供逻辑的代码。
活动
活动(activity)是WF的基本构建模块。从复杂的执行逻辑到执行一个对SQL数据库的更新,这些操作都被封装到一个个独立的工作单元, 也就是活动中。活动是所有直接或间接继承自System.Workflow.ComponentModel.Activity的类。活动具有两个方面的行 为:运行时行为(runtime behavior)和设计时行为(design-time behavior)。
运行时行为是活动在工作流中使用时执行的代码。它可能是访问一个Web服务或执行一块代码,也可能是协调子活动的执行。一个常被问及的问题 是:WF的性能怎样?在活动级别,答案很简单:活动可以和在.NET程序集里相同代码的执行速度一样快。因为活动只是一个已经编译好的.NET程序集,它 包含从activity继承的类。管理活动的生命周期需要额外的开支。
当包含活动的工作流被创建时,该活动就会被初始化并停留在Initialized(初始)状态。也就是说,当CreateInstance方法 被调用且数据流实例被创建时,所有的活动都将被初始化。服务在被调度执行时,其状态会变成Executing(执行)。如果一切正常,活动会进入到 Closed(关闭)状态,也就是它的任务已完成,可以休息了。
活动可能会遇到错误,从而进入Faulting状态而被关闭。活动也可能被其他的活动取消掉,从而进入Canceling状态,然后再进入 Closed状态。最后,如果活动为其执行定义了回滚或补偿操作,活动就会从Closed状态激活并进入到Compensating状态。由于某些原因, 一个错误或一个取消处理都需要调用补偿逻辑。
另外,活动的设计体验对创建工作流是非常重要的。活动可能需要在设计窗口里有些个性化的表示,目的是为了告诉开发者可以一目了然地知道活动正在做什么。
开箱即用活动
随WF一起发布的活动通常被称为开箱即用活动(out of the box activity)。它们是一组基本的活动,可用来创建简单的工作流和组建新的活动。下表给出了每个开箱即用活动的简单介绍:
创建定制服务
工作流开发者可能经常需要创建定制活动。从封装经常使用的方法到创建能为一种新的执行模式建模的复合活动,开发者在开始一个工作流项目时需要首先思考他需要什么样的活动。随着时间的推移,这些活动可被复用或被用来组建更高层次的活动,最后就像处理现在的普通对象一样。
基础
创建定制活动的最基本方法就是从System.Workflow.ComponentModel.Activity继承一个类。这将为活动创建所有的基本机制,除了活动实现的实际逻辑。可通过重载Execute方法来实现实际的逻辑。如下所示:
{
public BasicActivity()
{
this.Name ="BasicActivity";
}
protectedoverride ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
Console.WriteLine("Basic Activity");
return ActivityExecutionStatus.Closed;
}
}
Execute方法执行活动的操作并需同时通知运行时它的状态。在这里,它返回Closed状态,表示该活动已完成它的工作。一个更加复杂的模 式是:返回Executing状态,同时等待一个需要运行较长时间的工作完成。当这个较长时间的工作完成时,活动将通知运行时它已经完成了。在等待时,工 作流实例可能停顿下来较长时间。
我们经常需要控制一些与活动相关的参数,它们会影响活动所提供的有用功能。最简单的方法就是为活动类添加属性,如下所示:
{
get { return textToPrint; }
set { textToPring ="value"; }
}
protectedoverride ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
Console.WriteLine("Text to print: {0}", TextToPrint);
return ActivityExecutionStatus.Closed;
}
当使用XAML声明式地创建工作流时,这些属性会被作为活动的特性(attribute),如下所示:
x:Name="Workflow1"
xmlns:ns0="clr-namespace:SampleWFApplication"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
<ns0:BasicActivity TextToPrint="Hello World"
x:Name="basicActivity1"/>
</SequentialWorkflowActivity>
这样就允许在设计器里设定属性的值从而定制活动的行为。但这还是静态的,属性被限制为设计时设定的值。当创建的工作流需要评估传入的定制对象时 呢?这可以通过使用依赖属性(dependency property)来实现。依赖属性和之前介绍的.NET标准属性类似,但在声明和用户法上有所不同。VS中有内建的代码来创建它们,如下所示:
DependencyProperty.Register("OrderAmount", typeof(int), typeof(BasicActivity));
[Description("This property holds the amount of the order")]
[Category("Order Details")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
publicint OrderAmount
{
get
{
return ((int)(base.GetValue(BasicActivity.OrderAmountProperty)));
}
set
{
base.SetValue(BasicActivity.OrderAmountProperty, value);
}
}
这个稍长的声明里不仅有属性的声明,而且还包含了一个静态DependencyProperty变量的声明。 DependencyProperty是一种添加到DependencyObject(Activity就继承自DependencyObject)的特 殊属性类型。DependencyProperty不同于传统属性,它可以支持三种特殊的使用方式:
1. 活动绑定。
2. 元数据,只在设计时赋值,在运行时不能更改。
3. 附加属性(attached property),动态地为活动添加属性。
使用依赖属性的最普通场景是支持活动绑定。使用依赖属性的好处是可以添加新的设计时行为。将活动拖到设计窗口后,观察它的属性窗口,会发现在刚长声明的属性旁边新增一个new图标。单击该图标会弹出一个新的Bind(绑定)对话框。
Bind对话框允许将属性的值与工作流中的另外一个值动态绑定。这个值可以是在工作流创建时传入该工作流的一个属性,也可以是另外一个活动的属 性。在设计时,活动被告知到哪儿去查找依赖属性的值。通过选择工作流中同一类型(可以是一个定制类型)的另外一个值,一个绑定表达式会出现在属性窗口里作 为这个依赖属性的值。表达式与下面的式子类似的:
Activity=Workflow1, Path=WorkflowOrderAmount
式子的第一部分表示源(这里是父工作流),然后是活动可以使用的属性。式子里可以使用点操作符,如果希望被绑定的值是属性的下面几层,它也可以被访问。依赖属性的值还可以直接写在代码里。
Bind对话框里有一个Bind to a New Member(绑定到一个新成员)标签页。这个窗口可以使一个活动的依赖属性“提升”(promote)为窗口活动(即包含活动的活动)的一个成员。
组合
第二种创建活动的方法是复合(composition),这也是VS的默认方法。以下代码在VS中新建一个活动类的定义:
它继承自SequenceActivity。SequenceAcitivity是创建顺序执行工作流的基类,它的Execute方法负责将包 含的活动依次调度执行。通过继承,可保留我们希望的这种模式。这将使活动的开发者通过拖拽其他活动到新的活动里来创建一个新的活动,也就是从现有的活动创 建新的活动。这是一个创建新工作单元的强大工具。这意味着,如果有一组已经实现且足够强大的基本活动(如对现有API的封装)的集合,就可使用它们快速创 建更高层次的新功能单元。
考虑下面的活动:
1. SendEmail
2. LookupManager
3. WaitForResponseToEmail
使用这些活动及一些开箱即用活动提供的结构化活动,可以创建任意复杂的标准活动,然后新建的活动就可以作为批准活动被任意的工作流使用。
定制复合活动
我们还可以创建另外一类活动,即定制复合活动(custom composite activity)。复合活动包含子活动,它的Execute方法负责调度执行这些子活动。在开箱即用活动中,Sequence、While和Parallel都是复合活动的例子。
复合活动执行时,会执行它的子活动并订阅这些子活动的完成事件。活动会返回Executing状态,向运行时指明它要继续执行下一个被调度的活 动。通过接收子活动的完成事件,复合活动可以继续调度执行其他的活动或判断是否可以结束。当所有的活动都结束了或复合活动决定结束时,它会返回 Closed状态,表明它已经结束了。工作流会确保在允许一个活动关闭前,它的所有子活动都关闭了。
活动通信
工作流并不是在完全孤立的环境里执行的,它们经常需要和宿主程序交互以发送消息给宿主或从宿主接收消息。两个开箱即用活动被设计成支持这样的交 互:Handle-ExtenalEvent和CallExternalMethod。这些活动通过宿主和工作流间共享的契约通信。该契约的实现作为本地 服务提供给运行时ExternalDataExchangeService。
让我们来看一个基于员工调查应用场景的例子。首先创建一个在宿主和工作流间共享的契约,并为其添加ExtenalDataChange特性:
using System.Workflow.ComponentModel;
using System.Workflow.Activities;
namespace ExternalEventSample
{
[ExternalDataExchange()]
publicinterface ISurveyResponseService
{
void SurveyEmployee(string employee, string surveyQuestion);
event EventHandler<SurveyEventArgs> SurveyCompleted;
}
[Serializable]
publicclass SurveyEventArgs : ExternalDataEventArgs
{
privatestring employee;
publicstring Employee
{
get { return employee; }
set { employee = value; }
}
privatestring surveyResponse;
publicstring SurveyResponse
{
get { return surveyResponse; }
set { surveyResponse = value; }
}
public SurveyEventArgs(Guid instanceId, string employee, string surveyResponse) : base(instanceId)
{
this.employee = employee;
this.surveyResponse = surveyResponse;
}
}
}
该接口定义了一个事件、一个定制的事件参数类和一个公共方法。我们再提供一个接口的实现:
namespace ExternalEventSample
{
class SurveyResponseService : ISurveyResponseService
{
publicvoid SurveyEmployee(string employee, string surveyQuestion)
{
//here we would notify and display the survey
Console.WriteLine("Hey {0}, what do you think of {1}?", employee, surveyQuestion);
}
publicevent EventHandler<SurveyEventArgs> SurveyCompleted;
publicvoid CompleteSurvey(Guid instanceId, string employee, string surveyResponse)
{
// the host will call this method when it wants to raisethe event into the workflow.
// Note that the workflow instance id needs to be passed in.
EventHandler<SurveyEventArgs> surveyCompleted =this.SurveyCompleted;
if(surveyCompleted !=null)
{
surveyCompleted(null, new SurveyEventArgs(instanceId, employee, surveyResponse));
}
}
}
}
现在,将ExternalDataExchange服务添加到运行时,并且将接口实现的一个实例添加为本地服务。使用OnWorkflowIdled事件发送响应给宿主。
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Workflow.Activities;
namespace ExternalEventSample
{
class Program
{
static SurveyResponseService surveyService;
staticvoid Main(string[] args)
{
using(WorkflowRuntime workflowRuntime =new WorkflowRuntime())
{
// add the local service to the external data exchange service
surveyService =new SurveyResponseService();
ExternalDataExchangeService dataService =new ExternalDataExchangeService();
workflowRuntime.AddService(dataService);
dataService.AddService(surveyService);
AutoResetEvent waitHandle =new AutoResetEvent(false);
workflowRuntime.WorkflowCompleted +=delegate(object sender, WorkflowCompletedEventArgs e)
{ waitHandle.Set(); };
workflowRuntime.WorkflowTerminated +=delegate(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine(e.Exception.Message);
waitHandle.Set();
};
workflowRuntime.WorkflowIdled += OnWorkflowIdled;
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof
WorkflowConsoleApplication13.Workflow1));
instance.Start();
waitHandle.WaitOne();
}
}
staticvoid OnWorkflowIdled(object sender, WorkflowEventArgs e)
{
surveyService.CompleteSurvey(e.WorkflowInstance.InstanceId, "Matt", "Very Statisfied");
}
}
}
回到工作流,将CallExternalMethod活动拖到设计窗口。注意智能标签验证表明接口类型和方法名称都没有定义。点击 InterfaceType属性会弹出一个标准的类型浏览窗口以选择合适的接口。方法可以从MethodName属性的下拉列表中选择,方法选择好后,对 应于输入参数和接口里定义的输出的其他属性就会添加到属性窗口里。
当CallExternalMethod活动执行时,它通过ActivityExecutionContext.GetService获得对契约实现的访问,并调用其方法。
为使用HandleExternalEvent活动,要做的第一件事就是为宿主程序提供一种为工作流运行时触发事件的方法,以接收和路由消息。 在上例中通过调用服务实现的方法CompleteSurvey方法来完成。该方法将触发一个事件,运行时会根据传入的workflowId参数将其路由给 相应的工作流实例。在内部实现时,HandleExternalEvent会创建一个队列并订阅放在队列里的消息。当接收到一个事件时,运行时会将消息放 在正在等待该类型消息的队列上。可以让多个队列同时监听同一种类型消息--如等待多个员工完成调查。在这种情况下,事件将被路由到哪个队列的粒度可通过使 用correlation来指定。
为使用HandleExternalEvent,首先将它拖到设计窗口里,其过程与CallExternalMethod类似。当工作流到达 HandleExternalEvent活动时,它会创建一个队列来监听指定类型的事件类型,然后,或者停下来,或者继续执行其他被调度执行的活动。