事件驱动工作流
当我们能对工作流内部中的大多数决策进行编码时我们会使用顺序工作流。当决策发生在工作流外部时我们要使用状态机工作流。在本章中我们将详细讨论状态机工作流是如何工作的。7.1 什么是状态机状态机在计算机科学中已经应用了很长一段时间。你将会看到它们在反应系统reactive system中尤其流行就像用于视频游戏和机器人这样的软件。设计者使用状态机为使用状态、事件和迁移的系统建模。State代表一种情形或环境。在下面的截图中有一个状态机并具备两个状态Power On状态和Power Off状态。状态机总是这两个状态中的一个。事件event是一些外部的刺激。在上面的截图中我们只有一种类型的事件——按钮的点击事件。状态机将会对Power On或Power Off状态上的这个事件做出响应。并不是所有的事件必须对相同的事件做出响应。迁移transition将状态机转移到下一个状态。迁移只能发生在对事件的响应上。迁移不必将状态机转移到一个新的状态——迁移可以环回loop back到相同的状态。当状态机在Power Off状态中接收到按钮的点击事件时它会迁移到Power On状态。反之如果状态机在Power On状态中接收到按钮的点击事件那么它会转移到Power Off状态。状态迁移的概念暗示了在迁移之前或之后将会发生一些操作。就是说状态机并不只是存储状态它还会在事件到达的时候执行代码。在上面的截图中状态机在到达一个新的状态时将会通过打开或关闭电路的方式来控制电流的流向。7.2 Windows工作流中的状态机在上面截图中的状态机是相当简单的而大多数系统将需要更高级的模型。然而在截图中介绍的概念状态、事件和迁移和我们在Windows工作流中用来创建状态机的概念是相同的。在WF中State活动表示在状态机工作流中的一个状态。随着事件的到达工作流将会在State活动间迁移。状态机工作流必须指定一个初始状态这将是该工作流的开始状态。状态机工作流还可以指定一个完成状态可选的。工作流将会在其迁移到完成状态后终结。EventDriven活动表示状态机中的一个事件。我们把这些活动放在State活动中来表示该状态的合法事件。在EventDriven活动中我们可以放置一系列将要在事件到达时执行的活动。序列中最后一个活动通常是SetState活动。SetState活动指定了下一个迁移状态。7.3 我们的第一个状态机我们在第2章详细介绍过我们可以只使用代码来创建工作流或只使用XAML或使用代码和XAML代码分离。状态机工作流在这一点上并没有区别。我们将在本章使用代码分离的方法创建工作流虽然其中任何一种创建模式都可以工作。我们的工作流将支持Bug跟踪的应用程序。详细而言随着bug从Open状态迁移到Closed状态我们将会跟踪软件Bug的生命周期。在生命期内bug也可以是Assigned、Resolved或Deferred状态。为什么要使用状态机来为修复Bug的工作流建模呢因为对选择bug进行建模是不可能的而bug是需要到达一个完成状态的。思考一下在bug生命期中的每一步骤所需要的决策。一个新公开的bug需要一些评估。这个bug是重复的么这个bug真的是一个bug么即使这个bug真的是一个defect并不是所有的defect会直接转移到某个人的工作对列中。我们必须针对用来修复这个bug所需要的有效资源和项目计划来评估这个bug的严重程度。如果我们不能把我们所需要的所有智能放入到工作流中那么我们将依赖于外部的事件来告诉工作流我们做出了什么决策。7.3.1 创建项目就像创建大多数项目那样我们在Visual Studio的对话框中选择File | New Project。正如在下面的截图中所示我们将使用State Machine Workflow Console模式的应用程序。项目模板将会创建一个项目其中带有我们在WF编程中需要引用的所有程序集。新的项目将会在名为Workflow1.cs的文件中包括一个默认的工作流。我们可以删除这个文件并添加我们自己的State Machine Workflowwith code separation命名为BugWorkflow.xoml参见下面的截图。工作流设计器将和我们新的状态机工作流一起出现参见下面的截图。现在工具箱窗体是可以使用的其中填充着基础活动库中的活动。然而最初我们只能使用活动类型的子集——这些活动类型列出于下面截图的BugFlowInitalState图形中。在开始设计我们的状态机之前我们将需要一些代码支持。特别地我们需要一个能提供事件的服务来驱动工作流。7.3.2 Bug的生命期状态机将花费大部分时间在等待来自本地通信服务的事件到达上。我们在第3章讨论过本地通信服务我们需要一个接口来定义服务契约。接口将定义一些事件服务可以触发这些事件以提供数据到工作流接口还定义了一些方法工作流可以在服务上调用这些方法。对于下面这个例子我们的通讯是单向的——我们仅定义了几个事件。[ExternalDataExchange] public interface IBugService { event EventHandlerBugStateChangedEventArgs BugOpened; event EventHandlerBugStateChangedEventArgs BugResolved; event EventHandlerBugStateChangedEventArgs BugClosed; event EventHandlerBugStateChangedEventArgs BugDeferred; event EventHandlerBugStateChangedEventArgs BugAssigned; }这些事件的事件参数需要进程中的服务传递工作流可以使用的信息。例如一条可用的信息就是一个携带了bug所有特性title、description、assignment的Bug对象。[Serializable] public class BugStateChangedEventArgs : ExternalDataEventArgs { public BugStateChangedEventArgs(Guid instanceID, Bug bug):base(instanceID) { _bug bug; WaitForIdle true; } private Bug _bug; public Bug Bug { get { return _bug; } set { _bug value; } } }实现了IBugService接口的服务将会在bug的状态发生改变时触发事件。例如服务可能触发来自Smart Client应用程序的事件以响应用户在UI中操作的bug。有选择地服务可能在一个web服务的调用中接收到更新过的bug信息并触发来自ASP.NET的web服务的事件。核心问题是工作流不关心为什么触发也不关心导致事件的结果。工作流只关心有事件发生了。我们将使用bug服务接口的本地实现并提供触发事件的简单方法。在本章的后面我们将在控制台模式的程序中使用这个服务以触发事件到工作流。public class BugService : IBugService { public event EventHandlerBugStateChangedEventArgs BugOpened; public void OpenBug(Guid id, Bug bug) { if (BugOpened ! null) { BugOpened(null, new BugStateChangedEventArgs(id, bug)); } } //and so on … }既然我们知道了关于我们的工作流将要使用的服务契约我们就能够继续创建我们的状态机。7.3.3 State活动State活动代表状态机工作流中的一种状态。不要惊讶State活动是事件驱动工作流的支柱。通常我们可以通过拖动工具箱中的State活动到设计器来开始一个工作流设计。如果我们为软件bug的每一个可能的状态拖动一个State活动我们就具有下面这样的设计器视图注意到上面截图中的两种图形它们在左上角使用了特殊的图标。BugFlowInitialState图形在左上角有一个绿色的图标因为它是工作流的初始状态。每个状态机工作流必须具有一个初始状态这将是工作流进入或开始的状态。我们可以通过右击另一个图形并在上下文菜单中选择Set As Initial State来改变初始状态。BugClosedState在左上角有一个红色的图标因为它是完成状态。当一个工作流进入完成状态时它也就完成了但是完成状态是可选的。在很多bug跟踪系统中一个bug可以从关闭closed状态重新打开re-open但是在我们的工作流中我们将设置关闭状态为完成状态。我们可以通过右击一个图形并在上下文菜单中选择Set As Completed State来设置完成状态。我们的下一步是定义状态机在每一个状态中将要处理的事件。我们将使用EventDriven活动来定义这些事件。7.3.3.1 EventDriven活动EventDriven活动是少数几个我们可以从工具箱中拖出并拖入到State活动中的活动之一。在下面的截图中我们拖动EventDriven活动到BugFlowInitialState的内部。我们还使用了属性Properties窗口来将EventDriven活动的名称修改为OnBugOpened。OnBugOpened代表了状态机将如何在它的初始状态与BugOpened事件进行交互。我们不能在这一级别的细节上更多地利用该活动。我们需要通过双击OnBugOpened来深入到活动内部。这将带我们进入详细的活动视图中如下图所示这个详细视图沿着设计器的上方显示了一个“面包屑”breadcrumb导航控件。面包屑的意图使我们了解到我们正在编辑位于BugFlow工作流中的BugFlowInitialState活动。在这个视图的中间是我们拖动到状态中的OnBugOpened这个EventDriven活动的详细视图。在详细视图中我们可以看到EventDriven活动就像一个顺序活动并且它可以保存额外的子活动。然而这里有一些约束。EventDriven活动中的第一个活动必须实现IEventActivity接口。基础活动库中有三个活动符合这个条件——Delay活动HandleExternalEvent活动以及WebServiceInput活动。我们所有的这些事件来自一个本地的通信服务因此我们将使用HandleExternalEvent活动。下面的截图显示了在OnBugOpened活动中的一个HandleExternalEvent活动。我们将活动的名称修改为handleBugOpenedEvent并将InterfaceType设置为对我们先前定义的IBugService接口的引用。最后我们选择BugOpened为要处理的事件名称。我们已经完成了所有的初始化工作我们需要在我们的初始化工作流状态中处理事件。到目前为止我们可以在事件处理程序之后继续添加活动。例如我们可以添加一个活动来向小组成员发送关于这个新bug的通知。当我们完成了添加这些处理活动时那么最后一个我们想要执行的活动将会是SetState活动这也是我们接下来将会提及的。7.3.3.2 SetState活动接下来的事件强迫状态机迁移到新的状态。我们可以使用SetState活动为迁移建模该活动只能出现在状态机工作流的内部。SetState活动是相对简单的。该活动包括了指向目标状态的TargetStateName属性。在下面的截图中我们已经添加了SetState活动到OnBugOpened并将TargetStateName属性设置为BugOpenState。TargetStateName的属性编辑器在可供选择的下拉列表中只包括有效的状态名称。我们现在可以点击面包屑中的BugFlow链接并回过头来查看我们的状态机工作流。设计器将识别出我们刚刚配置的SetState活动并绘制出一条从BugFlowInitialState图形到BugOpenState的线参见下面的截图。工作流设计器为我们展现了一个bug工作流的全景它开始于BugFlowInitialState并在接下来的BugOpened事件通知一个新的bug的正式产生时转移到BugOpenState。到目前为止我们可以继续添加EventDriven活动到我们的工作流中。我们需要覆盖在bug生命期中的所有的事件和迁移。状态机的一个优点是我们控制了哪个事件在哪个具体的状态下是合法的。例如除了初始状态我们不想要任何状态来处理BugOpened事件。我们还可以设计我们的状态机从而在延迟状态中的bug将只会处理一个BugAssigned事件。下面的截图显示了我们的状态机并在适当的位置具有所有的事件和迁移。注意到在上面的截图中BugClosedState不需要处理任何事件。这个状态是完成状态并且工作流将不会处理任何额外的事件。7.3.3.3 StateInitialization和StateFinalization活动我们可以拖动到State活动中的两个额外的活动是StateInitialization活动和StateFinalization活动。State活动可以具有一个StateInitialization活动和一个StateFinalization活动。这两种活动都将顺次执行一组子活动。当状态机迁移到包括初始化活动的状态时StateInitialization活动会运行。相反只要状态机迁移到包括终结活动的状态之外StateFinalization活动就会执行。使用这两个活动我们可以在我们的状态机的状态中执行预处理和后事处理。7.3.4 驱动状态机开始一个状态机工作流与开始其它工作流没有什么不同。我们首先创建WorkflowRuntime类的一个实例。我们将需要运行时寄宿host一个ExternalDataExchangeService这就会依次寄宿那些实现了IBugService接口的本地通信服务。第3章包括了本地通信服务以及ExternalDataExchangeService的更多细节。ExternalDataExchangeService dataExchange; dataExchange new ExternalDataExchangeService(); workflowRuntime.AddService(dataExchange); BugService bugService new BugService(); dataExchange.AddService(bugService); WorkflowInstance instance; instance workflowRuntime.CreateWorkflow( typeof(BugFlow)); instance.Start();在我们的程序中的下一部分代码将调用位于我们的bug服务上的方法。这些方法会触发工作流运行时将要捕获到的事件。我们已经把这些事件小心翼翼地安排到工作流的所有状态中并成功完成。Bug bug new Bug(); bug.Title Application crash while printing; bugService.OpenBug(instance.InstanceId, bug); bugService.DeferBug(instance.InstanceId, bug); bugService.AssignBug(instance.InstanceId, bug); bugService.ResolveBug(instance.InstanceId, bug); bugService.CloseBug(instance.InstanceId, bug); waitHandle.WaitOne();使用状态机的一个好处是如果我们的应用程序触发了一个当前工作流状态并不希望触发的事件那么工作流将触发一个异常。当状态机在它的初始状态时我们应该只触发BugOpened事件。当状态机在它的Assigned状态时我们应该只触发BugResolved事件。工作流运行时将保证我们的应用程序遵循状态机所描述的进程。这就提供了一个优势——它保证了编码不正确的应用程序将不会引起状态迁移这样的工作流被认为不具有可用性因此将总是遵循工作流编码的企业级处理。然而需要着重注意的是任何出发不可用事件的代码将不会引起编译期错误——我们只有到运行期才会看到错误。在真实的bug跟踪的应用程序中一个bug到达关闭状态可能需要数周的时间。幸运的是状态机工作流可以利用工作流服务就像跟踪和持久化全都在第6章描述过。持久化服务能保存我们的工作流状态并卸载内存中的实例然后在事件在数周后到达时重新加载实例。关于我们的实例有一些地方是不寻常的。我们的应用程序在工作流触发每个事件的时候知道工作流的状态。真实的应用程序可能并不知道工作流的这些隐秘信息。我们的应用程序可能并不记得存在了两个月的bug的状态在这种情形下它也不会知道要触发的合法事件。幸运的是WF使得这样的信息是可用的。7.4 检查状态机思考一下我们想要为bug跟踪服务提供的用户界面。我们不想给用户创建异常的机会。例如当bug位于一个并不会转移到关闭状态的状态时我们不想提供Close This Bug按钮。取而代之的是我们想要用户界面反映出这个bug的当前状态并只允许用户执行合法的操作。借助于StateMachineWorkflowInstance类我们是可以做到这样的。7.4.1 StateMachineWorkflowInstanceStateMachineWorkflowInstance类为我们提供了接口来管理和查询状态机工作流。正如在下面的类图中所显示的那样这个API包括了我们可以用来取出当前状态名称的属性以及找到这个状态的合法迁移。这个类还包括了一个设置状态机状态的方法。虽然我们通常想要bug遵循我们在状态机中设计的工作流我们可以使用SetState方法把bug设置回它的初始状态或者强迫这个bug迁移到关闭状态或者在这中间的任何状态。让我们修改一下原始的示例以调用下面的方法。我们将会在调用bug服务的AssignBug方法之后调用这个DumpWorkflow方法因此工作流应该处于Assigned状态。private static void DumpStateMachine(WorkflowRuntime runtime, Guid instanceID) { StateMachineWorkflowInstance instance new StateMachineWorkflowInstance(runtime, instanceID); Console.WriteLine(Workflow ID: {0}, instanceID); Console.WriteLine(Current State: {0}, instance.CurrentStateName); Console.WriteLine(Possible Transitions: {0}, instance.PossibleStateTransitions.Count); foreach (string name in instance.PossibleStateTransitions) { Console.WriteLine(\t{0}, name); } }这段代码首先使用工作流运行时和工作流ID来检索工作流实例对象。然后我们打印出工作流当前状态的名称、合法迁移的数量以及合法迁移的名称。输出如下所示我们可以使用上面的信息来自定义用户接口。如果用户在应用程序中打开了这个特别的bug而这个bug的状态是BugAssignedState我们最好提供按钮来把这个bug标注为已解决resolved或延迟defer。这些是当前状态的唯一合法迁移。StateMachineWorkflowInstance类的另一个有趣的属性是StateHistory属性。正如你可能猜到的那样这个属性能够给我们一组工作流所见到的所有状态。如果你还记得我们在第6章关于跟踪服务的讨论你可能还记得跟踪服务并不是一种彻底的工作来记录工作流的执行历史。如果你猜到StateHistory属性将使用内嵌在WF中的跟踪服务恭喜你答对了7.4.2 状态机跟踪第6章提供了我们需要配置、初始化、使用跟踪以及跟踪信息的所有细节因此我们在这里将不会包括相同的内容。为了使用StateHistory属性我们必须配置工作流运行时以使用跟踪服务。如果我们试图使用StateHistory属性却没有在适当的位置使用跟踪服务我们将只能创建一个InvalidOperationException异常。注意StateHistory和跟踪服务到写作本书时为止如果我们在app.config或web.config中以声明方式配置跟踪服务那么StateHistory属性将不能工作。取而代之我们必须以编程方式配置带有连接字符串的跟踪服务并把该服务传递到工作流运行时中。如果我们想要列举出bug经过的状态可以使用在第6章介绍过的类如SqlTrackingQuery。我们还可以使用StateMachineWorkflowInstance类和StateHistory属性来为我们完成所有的工作。让我们在关闭bug之前调用下面的方法private static void DumpHistory(WorkflowRuntime runtime, Guid instanceID) { StateMachineWorkflowInstance instance new StateMachineWorkflowInstance(runtime, instanceID); Console.WriteLine(State History:); foreach (string name in instance.StateHistory) { Console.WriteLine(\t{0}, name); } }这段代码给我们如下输出一组工作流可以看到的状态列表开始与最近访问过的状态。注意我们可以在工作流实例仍然运行的时候只使用StateMachineWorkflowInstance类。一旦工作流实例完成我们必须后退到跟踪服务并使用跟踪服务查询来读取状态机的历史。7.5 层叠式状态机我们的第一个状态机是相对简单的但它确实代表了便捷状态机的基本设计。然而有时候这种直接的方法可能难于管理。想象一下如果用于bug跟踪管理软件的工作流要求我们允许用户关闭或分配assign一个bug——而不管bug的当前状态是什么。我们必须为工作流中的assigned和closed事件添加事件驱动活动到每个状态上已完成的状态除外。如果我们只需要少数几个状态那么还好但是随着状态机的增长这可能会变得乏味并易于出错。幸运的是这里有一种比较容易的解决方案。层叠式的状态机允许我们在父状态中内嵌子状态。子状态本质上继承它们父亲的事件驱动活动。如果在我们bug跟踪的工作流中的每个状态需要处理具有相同行为的bug关闭事件那么我们只需要增加一个事件驱动活动到父状态并添加我们的bug状态作为这个父亲的后代。结果是状态机工作流本身是StateMachineWorkflowInstance类的一个实例它派生于StateActivity类参见下面的截图。