ID #33617

《WF编程》系列之46 第七章 事件驱动工作流

  在创建一个新的工作流时,需要做出一项重要的抉择:我们要创建的工作流究竟是一个顺序工作流,还是一个状态机工作流?WF提供了两种“即开即用的”(out of the box)工作流执行类型。为了回答这个问题,我们不得不决定谁在受控。

   顺序工作流是一种预知的工作流。执行路径可能是分支、循环、或等待一个外部事件的发生,但是最终顺序工作流将会使用活动、条件和我们在前面章节所提供的必不可少的匹配规则。工作流在进程中受控。

   状态机工作流是一种事件驱动的工作流。就是说,状态机工作流依赖于额外的事件来驱动工作流的完成。我们定义了工作流的合法状态,以及在这些状态之间的合法迁移。工作流总是在其中的一个状态中,并且不得不在迁移到一个新状态之前等待事件的到达。一般来说,重要的选择发生在工作流之外。状态机定义了要遵循的结构,但控制是属于外部世界的。

   当我们能对工作流内部中的大多数决策进行编码时,我们会使用顺序工作流。当决策发生在工作流外部时,我们要使用状态机工作流。在本章中,我们将详细讨论状态机工作流是如何工作的。

  什么是状态机?

   状态机在计算机科学中已经应用了很长一段时间。你将会看到它们在反应系统(reactive system)中尤其流行,就像用于视频游戏和机器人这样的软件。设计者使用状态机为使用状态、事件和迁移的系统建模。

   State,代表一种情形或环境。在下面的截图中,有一个状态机并具备两个状态:Power On状态和Power Off状态。状态机总是这两个状态中的一个。

《WF编程》系列之46 第七章 事件驱动工作流

   事件(event),是一些外部的刺激。在上面的截图中,我们只有一种类型的事件——按钮的点击事件。状态机将会对Power On或Power Off状态上的这个事件做出响应。并不是所有的事件必须对相同的事件做出响应。

   迁移(transition)将状态机转移到下一个状态。迁移只能发生在对事件的响应上。迁移不必将状态机转移到一个新的状态——迁移可以环回(loop back)到相同的状态。当状态机在Power Off状态中接收到按钮的点击事件时,它会迁移到Power On状态。反之,如果状态机在Power On状态中接收到按钮的点击事件,那么它会转移到Power Off状态。

   状态迁移的概念暗示了在迁移之前或之后将会发生一些操作。就是说,状态机并不只是存储状态,它还会在事件到达的时候执行代码。在上面的截图中,状态机在到达一个新的状态时,将会通过打开或关闭电路的方式来控制电流的流向。

  Windows工作流中的状态机

   在上面截图中的状态机是相当简单的,而大多数系统将需要更高级的模型。然而,在截图中介绍的概念(状态、事件和迁移),和我们在Windows工作流中用来创建状态机的概念是相同的。

   在WF中,State活动表示在状态机工作流中的一个状态。随着事件的到达,工作流将会在State活动间迁移。状态机工作流必须指定一个初始状态,这将是该工作流的开始状态。状态机工作流还可以指定一个完成状态(可选的)。工作流将会在其迁移到完成状态后终结。

   EventDriven活动表示状态机中的一个事件。我们把这些活动放在State活动中来表示该状态的合法事件。在EventDriven活动中,我们可以放置一系列将要在事件到达时执行的活动。序列中最后一个活动通常是SetState活动。SetState活动指定了下一个迁移状态。

  我们的第一个状态机

   我们在第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的严重程度。如果我们不能把我们所需要的所有智能放入到工作流中,那么我们将依赖于外部的事件来告诉工作流我们做出了什么决策。

  创建项目

   就像创建大多数项目那样,我们在Visual Studio的对话框中选择File | New Project。正如在下面的截图中所示,我们将使用State Machine Workflow Console模式的应用程序。项目模板将会创建一个项目,其中带有我们在WF编程中需要引用的所有程序集。

《WF编程》系列之46 第七章 事件驱动工作流

   新的项目将会在名为Workflow1.cs的文件中包括一个默认的工作流。我们可以删除这个文件并添加我们自己的State Machine Workflow(with code separation),命名为BugWorkflow.xoml(参见下面的截图)。

《WF编程》系列之46 第七章 事件驱动工作流

   这些事件的事件参数需要进程中的服务传递工作流可以使用的信息。例如,一条可用的信息就是一个携带了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 EventHandler<BugStateChangedEventArgs> BugOpened;

  public void OpenBug(Guid id, Bug bug)
  {
    if (BugOpened != null)
    {
      BugOpened(null,
        new BugStateChangedEventArgs(id, bug));
    }
  }

  //and so on …
}

   既然我们知道了关于我们的工作流将要使用的服务契约,我们就能够继续创建我们的状态机。

  State活动

   State活动代表状态机工作流中的一种状态。不要惊讶,State活动是事件驱动工作流的支柱。通常,我们可以通过拖动工具箱中的State活动到设计器来开始一个工作流设计。如果我们为软件bug的每一个可能的状态拖动一个State活动,我们就具有下面这样的设计器视图:

《WF编程》系列之46 第七章 事件驱动工作流

   注意到上面截图中的两种图形,它们在左上角使用了特殊的图标。BugFlowInitialState图形在左上角有一个绿色的图标,因为它是工作流的初始状态。每个状态机工作流必须具有一个初始状态,这将是工作流进入或开始的状态。我们可以通过右击另一个图形并在上下文菜单中选择Set As Initial State来改变初始状态。

   BugClosedState在左上角有一个红色的图标,因为它是完成状态。当一个工作流进入完成状态时,它也就完成了,但是完成状态是可选的。在很多bug跟踪系统中,一个bug可以从关闭(closed)状态重新打开(re-open),但是在我们的工作流中,我们将设置关闭状态为完成状态。我们可以通过右击一个图形并在上下文菜单中选择Set As Completed State来设置完成状态。

   我们的下一步是定义状态机在每一个状态中将要处理的事件。我们将使用EventDriven活动来定义这些事件。

  EventDriven活动

   EventDriven活动是少数几个我们可以从工具箱中拖出并拖入到State活动中的活动之一。在下面的截图中,我们拖动EventDriven活动到BugFlowInitialState的内部。我们还使用了属性(Properties)窗口来将EventDriven活动的名称修改为OnBugOpened。

《WF编程》系列之46 第七章 事件驱动工作流

   OnBugOpened代表了状态机将如何在它的初始状态与BugOpened事件进行交互。我们不能在这一级别的细节上更多地利用该活动。我们需要通过双击OnBugOpened来深入到活动内部。这将带我们进入详细的活动视图中,如下图所示:

《WF编程》系列之46 第七章 事件驱动工作流

   这个详细视图沿着设计器的上方显示了一个“面包屑”(breadcrumb)导航控件。面包屑的意图使我们了解到我们正在编辑位于BugFlow工作流中的BugFlowInitialState活动。在这个视图的中间,是我们拖动到状态中的OnBugOpened这个EventDriven活动的详细视图。

   在详细视图中,我们可以看到EventDriven活动就像一个顺序活动,并且它可以保存额外的子活动。然而,这里有一些约束。EventDriven活动中的第一个活动必须实现IEventActivity接口。基础活动库中有三个活动符合这个条件——Delay活动,HandleExternalEvent活动,以及WebServiceInput活动。我们所有的这些事件来自一个本地的通信服务,因此我们将使用HandleExternalEvent活动。

   下面的截图显示了在OnBugOpened活动中的一个HandleExternalEvent活动。我们将活动的名称修改为handleBugOpenedEvent,并将InterfaceType设置为对我们先前定义的IBugService接口的引用。最后,我们选择BugOpened为要处理的事件名称。我们已经完成了所有的初始化工作,我们需要在我们的初始化工作流状态中处理事件。

《WF编程》系列之46 第七章 事件驱动工作流

   到目前为止,我们可以在事件处理程序之后继续添加活动。例如,我们可以添加一个活动来向小组成员发送关于这个新bug的通知。当我们完成了添加这些处理活动时,那么最后一个我们想要执行的活动将会是SetState活动,这也是我们接下来将会提及的。

  SetState活动

   接下来的事件强迫状态机迁移到新的状态。我们可以使用SetState活动为迁移建模,该活动只能出现在状态机工作流的内部。SetState活动是相对简单的。该活动包括了指向目标状态的TargetStateName属性。

   在下面的截图中,我们已经添加了SetState活动到OnBugOpened,并将TargetStateName属性设置为BugOpenState。TargetStateName的属性编辑器在可供选择的下拉列表中只包括有效的状态名称。

《WF编程》系列之46 第七章 事件驱动工作流

   我们现在可以点击面包屑中的BugFlow链接,并回过头来查看我们的状态机工作流。设计器将识别出我们刚刚配置的SetState活动,并绘制出一条从BugFlowInitialState图形到BugOpenState的线(参见下面的截图)。工作流设计器为我们展现了一个bug工作流的全景:它开始于BugFlowInitialState,并在接下来的BugOpened事件通知一个新的bug的正式产生时,转移到BugOpenState。

《WF编程》系列之46 第七章 事件驱动工作流

   到目前为止,我们可以继续添加EventDriven活动到我们的工作流中。我们需要覆盖在bug生命期中的所有的事件和迁移。状态机的一个优点是,我们控制了哪个事件在哪个具体的状态下是合法的。例如,除了初始状态,我们不想要任何状态来处理BugOpened事件。我们还可以设计我们的状态机,从而在延迟状态中的bug将只会处理一个BugAssigned事件。下面的截图显示了我们的状态机,并在适当的位置具有所有的事件和迁移。

《WF编程》系列之46 第七章 事件驱动工作流

   注意到,在上面的截图中,BugClosedState不需要处理任何事件。这个状态是完成状态,并且工作流将不会处理任何额外的事件。

  StateInitialization和StateFinalization活动

   我们可以拖动到State活动中的两个额外的活动是StateInitialization活动和StateFinalization活动。State活动可以具有一个StateInitialization活动和一个StateFinalization活动。

   这两种活动都将顺次执行一组子活动。当状态机迁移到包括初始化活动的状态时,StateInitialization活动会运行。相反,只要状态机迁移到包括终结活动的状态之外,StateFinalization活动就会执行。使用这两个活动,我们可以在我们的状态机的状态中执行预处理和后事处理。

  驱动状态机

   开始一个状态机工作流与开始其它工作流没有什么不同。我们首先创建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事件。工作流运行时将保证我们的应用程序遵循状态机所描述的进程。这就提供了一个优势——它保证了编码不正确的应用程序将不会引起状态迁移,这样的工作流被认为不具有可用性,因此将总是遵循工作流编码的企业级处理。然而,需要着重注意的是,任何出发不可用事件的代码将不会引起编译期错误——我们只有到运行期才会看到错误。


2009-03-23 00:00
阅读:
I'm VC , Just U know Y
本站部分文章来源于互联网,版权归原作者所有。

延伸阅读:

C#操作技巧的数据类型之间的转换

C#代码操作IIS之虚拟目录

C#中父窗口和子窗口之间实现控件互操作

C#箴言之用属性来访问类的私有成员

C#多线程-不同线程之间通过事件委托封送调用方法