Windows Foundation 4

在 WF 4 中编写自定义控制流活动

Leon Welicki

控制流是指组织和执行程序中各个指令的方法。在 Windows Workflow Foundation 4 (WF 4) 中,控制流活动掌控了一个或多个子活动的执行语义。WF 4 活动工具箱中的示例包括:Sequence、Parallel、If、ForEach、Pick、Flowchart 和 Switch 等等。

WF 运行时对 Sequence 或 Parallel 等控制流并不很了解。从它的角度看来,一切都只是活动而已。运行时只强制实施一些简单的规则(例如,“只要有任何子活动仍在运行,活动就不能完成”)。WF 控制流是基于层次结构的,因此 WF 程序就是一个活动树。

WF 4 中的控制流选项并不仅限于框架中提供的活动。您可以编写自己的活动,然后将其与框架中提供的活动结合使用,这就是本文要讨论的话题。您将了解如何采用循序渐进的方法编写自己的控制流活动:我们从一个非常简单的控制流活动开始,逐渐丰富其内容,最终打造一个有用的新控制流活动。我们所有示例的源代码都可供下载。

但首先,让我们介绍一些有关活动的基本概念,让大家掌握一些基础知识。

活动

活动是 WF 程序中的基本执行单元;而工作流程序则是由 WF 运行时执行的活动树。WF 4 是一套全面的活动集,包含超过 35 个活动,能够用于为流程建模或创建新的活动。其中一些活动控制如何执行其他活动的语义(例如 Sequence、Flowchart、Parallel 和 ForEach),称为复合 活动。其他活动则用于执行单个原子任务(WriteLine、InvokeMethod 等等)。我们称之为 活动。

WF 活动以 CLR 类型的形式实现,正因如此,它们派生自其他现有的类型。您可以利用 WF 设计器以可视化和声明的方式创建活动,也可以通过编写 CLR 代码强制创建活动。图 1 中的活动类型层次结构中定义了一些可用于创建自定义活动的基本类型。MSDN 库中提供了有关此类型层次结构的详细解释,网址为 msdn.microsoft.com/library/dd560893

图 1 活动类型层次结构

本文重点介绍从 NativeActivity 派生的活动。(NativeActivity 是一个基类,通过这个基类可以访问整个 WF 运行时。)因为控制流活动需要与 WF 运行时交互,所以它们都是从 NativeActivity 类型派生的复合活动。控制流活动通常用于安排其他活动(例如 Sequence、Parallel 或 Flowchart),但也可能包含以下活动:使用 CancellationScope 或 Pick 实施自定义取消;使用 Receive 创建书签;使用 Persist 实现持久性。

活动数据模型定义了一个清晰的模型,可用于在创建和使用活动时对数据进行推理。数据是通过参数和变量定义的。参数是活动的绑定终端,根据哪些数据能传递给活动(输入参数)以及当活动完成执行时要返回哪些数据(输出参数)来定义自己的公共签名。变量表示数据的临时存储。

活动的创建者使用参数来定义数据在活动中流入和流出的方式,并且按照以下几种方式来使用变量:

  • 在活动定义上公开一个用户可编辑的变量集,以便在多个活动中共享变量(例如 Sequence 和 Flowchart 中的 Variables 集合)。
  • 为活动的内部状态建模。

工作流创建者通过编写表达式,用参数将活动与环境绑定,并在工作流的不同范围内声明变量,从而在活动之间共享数据。将变量和参数结合使用,可以为活动之间的通信提供可预测的通信模式。

现在,我已经介绍了活动的一些核心基础知识,接下来让我们开始第一个控制流活动。

一个简单的控制流活动

首先,我将创建一个非常简单的控制流活动,名为 ExecuteIfTrue。此活动没有太多的内容:如果某个条件为 True,就执行一个包含的活动。WF 4 提供了一个 If 活动,其中包含 Then 和 Else 子活动;我们通常只需要提供 Then,Else 只是开销。对于这种情况,我们需要一个活动,该活动能够根据布尔条件的值执行另一个活动。

此活动的工作原理应该是这样的:

  • 活动用户必须提供一个布尔条件。这个参数是必需参数。
  • 活动用户可以提供主体,即在条件为 True 时执行的活动。
  • 在执行时:如果条件为 True 且主体不为 Null,则执行主体。

下面是一个 ExecuteIfTrue 活动的实现,其行为方式与上文所述完全相同:

public class ExecuteIfTrue : NativeActivity
{
  [RequiredArgument]
  public InArgument<bool> Condition { get; set; }

  public Activity Body { get; set; }

  public ExecuteIfTrue() { }  

  protected override void Execute(NativeActivityContext context)
  {            
    if (context.GetValue(this.Condition) && this.Body != null)
      context.ScheduleActivity(this.Body);
  }
}

此代码非常简单,但是并不像看起来那么简单。 ExecuteIfTrue 在条件为 True 时执行子活动,因此需要安排另一个活动。 因此,它必须从 NativeActivity 派生,因为它需要与 WF 运行时交互以安排子活动。

一旦您决定了活动的基类,就需要定义其公共签名。 在 ExecuteIfTrue 中,这包括以下各项:类型为 InArgument<bool>、名为 Condition 的布尔输入参数,包含要评估的条件;类型为 Activity、名为 Body 的属性,包含要在条件为 True 时执行的活动。 Condition 参数使用 RequiredArgument 特性加以修饰,该特性指示 WF 运行时必须要为其设置一个表达式。 WF 运行时将在准备要执行的活动时强制进行此项验证:

[RequiredArgument]
public InArgument<bool> Condition { get; set; }

public Activity Body { get; set; }

此活动中最有意思的一段代码就是 Execute 方法,该方法用于执行“操作”。 所有的 NativeActivity 都必须 重写此方法。 Execute 方法将收到一个 NativeActivityContext 参数,该参数是活动创建者与 WF 运行时之间的交互点。 在 ExecuteIfTrue 中,此上下文用于检索 Condition 参数的值 (context.GetValue(this.Condition)),并使用 ScheduleActivity 方法安排 Body。 请注意,我说的是安排 而不是执行。 WF 运行时不会立即执行活动,而是将这些活动添加到一个工作项列表中以安排执行:

protected override void Execute(NativeActivityContext context)
{
    if (context.GetValue(this.Condition) && this.Body != null)
        context.ScheduleActivity(this.Body);
}

同时请注意,该类型被设计为遵循“创建-设置-使用”模式。 XAML 语法基于这种类型设计模式,因此该类型具备一个公共的默认构造函数和一些公共的读/写属性。 这意味着该类型便于进行 XAML 序列化。

以下代码段显示了如何使用此活动。 在本例中,如果当前日期为星期六,则将向控制台输出字符串“Rest!”:

var act = new ExecuteIfTrue
{
  Condition = new InArgument<bool>(c => DateTime.Now.DayOfWeek == DayOfWeek.Tuesday),
  Body = new WriteLine { Text = "Rest!" }
};

WorkflowInvoker.Invoke(act);

第一个控制流活动已通过这 15 行代码创建。 但是不要被该代码的简单性所迷惑,它实际上是一个功能完备的控制流活动!

安排多个子活动

下一个挑战就是编写一个简化的 Sequence 活动。 此练习的目的是为了让您了解如何编写控制流活动,以安排多个子活动并在多个部分中执行这些子活动。 此活动在功能上与产品附带的 Sequence 几乎完全相同。

此活动的工作原理应该是这样的:

  • 活动的用户必须通过 Activities 属性提供要按顺序执行的子活动集合。
  • 在执行时:
    • 活动包含一个内部变量,其值是已经执行的集合中最后一项的索引。
    • 如果子活动集合中包含内容,则安排第一个子活动。
    • 当子活动完成时:
      • 递增最后执行的项的索引。
      • 如果索引仍在子活动集合的范围内,则安排下一个子活动。
      • 重复执行。

图 2 中的代码实现了一个 SimpleSequence 活动,其行为方式与上文所述完全相同。

图 2 SimpleSequence 活动

public class SimpleSequence : NativeActivity
{
  // Child activities collection
  Collection<Activity> activities;
  Collection<Variable> variables;

  // Pointer to the current item in the collection being executed
  Variable<int> current = new Variable<int>() { Default = 0 };
     
  public SimpleSequence() { }

  // Collection of children to be executed sequentially by SimpleSequence
  public Collection<Activity> Activities
  {
    get
    {
      if (this.activities == null)
        this.activities = new Collection<Activity>();

      return this.activities;
    }
  }

  public Collection<Variable> Variables 
  { 
    get 
    {
      if (this.variables == null)
        this.variables = new Collection<Variable>();

      return this.variables; 
    } 
  }

  protected override void CacheMetadata(NativeActivityMetadata metadata)
  {
    metadata.SetChildrenCollection(this.activities);
    metadata.SetVariablesCollection(this.variables);
    metadata.AddImplementationVariable(this.current);
  }

  protected override void Execute(NativeActivityContext context)
  {
    // Schedule the first activity
    if (this.Activities.Count > 0)
      context.ScheduleActivity(this.Activities[0], this.OnChildCompleted);
  }

  void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
  {
    // Calculate the index of the next activity to scheduled
    int currentExecutingActivity = this.current.Get(context);
    int next = currentExecutingActivity + 1;

    // If index within boundaries...
if (next < this.Activities.Count)
    {
      // Schedule the next activity
      context.ScheduleActivity(this.Activities[next], this.OnChildCompleted);

      // Store the index in the collection of the activity executing
      this.current.Set(context, next);
    }
  }
}

同样,只用几行代码(在本例中,大约为 50 行)就完成了一个功能完备的控制流活动。 代码很简单,但引入了一些有趣的概念。

SimpleSequence 按顺序执行子活动集合,因此需要安排其他活动。 因此,它从 NativeActivity 派生,因为它需要与运行时交互以安排子活动。

下一步是为 SimpleSequence 定义公共签名。 在本例中,它由通过 Activities 属性公开的活动集合(类型为 Collection<Activity>)和通过 Variables 属性公开的变量集(类型为 Collection<Variable>)构成。 通过变量,就可以在所有子活动之间共享数据。 请注意,这些属性中只有“getters”通过“延迟实例化”方法公开集合(参见图 3),因此访问这些属性永远不会导致 Null 引用。 因此,这些属性符合“创建-设置-使用”模式。

图 3 延迟实例化方法

public Collection<Activity> Activities
{
  get
  {
    if (this.activities == null)
      this.activities = new Collection<Activity>();

    return this.activities;
  }
}

public Collection<Variable> Variables 
{ 
  get 
  {
    if (this.variables == null)
      this.variables = new Collection<Variable>();

    return this.variables; 
  } 
}

类中有一个私有成员,不属于签名:名为“current”的 Variable<int> 用于保存正在执行的活动的索引:

// Pointer to the current item in the collection being executed
Variable<int> current = new Variable<int>() { Default = 0 };

因为此信息属于 SimpleSequence 的内部执行状态的一部分,所以您需要将其设为私有,不向 SimpleSequence 的用户公开。 您还希望在保留活动时将其保存和还原。 此目的通过使用 ImplementationVariable 来实现。

Implementation 变量是活动的内部变量。 它们供活动创建者,而不是活动使用者使用。 Implementation 变量在保留活动时保存,在重新加载活动时还原,不需要我们进行任何操作。 为了清楚地说明这一点,并继续 Sequence 示例:如果保存了 SimpleSequence 实例,当它复原时,将“记住”执行过的最后一个活动的索引。

WF 运行时无法自动了解实现变量。 如果您想在活动中使用 ImplementationVariable,需要显式通知 WF 运行时。 此项活动在 CacheMetadata 方法的执行过程中进行。

尽管名字有些令人生畏,但 CacheMetadata 其实没那么难。 从概念上说,它实际上很简单:其实就是活动用于向运行时进行“自我介绍”的方法。 想一下 If 活动。 在 CacheMetadata 中,此活动会说:*“大家好,我是 If 活动,我有一个输入变量叫 Condition,还有两个子活动,分别是 Then 和 Else。”当使用 SimpleSequence 活动时,此活动会说:“大家好,我是 SimpleSequence,我有一个子活动集合、一个变量集合和一个实现变量。”*CacheMetadata 代码中包含的内容也无非就是 SimpleSequence 代码中的那些内容:

protected override void CacheMetadata(NativeActivityMetadata metadata)
{
  metadata.SetChildrenCollection(this.activities);
  metadata.SetVariablesCollection(this.variables);
  metadata.AddImplementationVariable(this.current);
}

CacheMetadata 的默认实现使用反射,从活动获取此数据。 在 ExecuteIfTrue 示例中,我并未实现 CacheMetadata,而是依赖默认的实现来反射公共成员。 而 SimpleSequence 则相反,因为默认实现“猜不出”我要使用实现变量,所以必须实现此活动。

此活动中下一段有意思的代码是 Execute 方法。 在本例中,如果集合中有活动,就告知 WF 运行时:*“请执行活动集合中的第一个活动,完成后再调用 OnChildCompleted 方法。”*您通过 NativeActivityContext.ScheduleActivity 以 WF 语言来传达这个意思。 请注意,当您安排一个活动时,需要提供第二个参数,该参数是一个 CompletionCallback。 简单来说,它是一个在活动执行完成时调用的方法。 同样,一定要注意计划和执行之间的差别。 安排活动时不会调用 CompletionCallback,安排的活动执行完成时才会调用 CompletionCallback:

protected override void Execute(NativeActivityContext context)
{
  // Schedule the first activity
  if (this.Activities.Count > 0)
    context.ScheduleActivity(this.Activities[0], this.OnChildCompleted);
}

从学习的角度来说,OnChildCompleted 方法是此活动中最有意思的部分,实际上这也是我在本文中谈及 SimpleSequence 的主要原因。 此方法用于获取集合中的下一个活动并对其进行安排。 安排了下一个子活动后,就会提供一个 CompletionCallback,在本例中,它指向同一个方法。 因此,当某个子活动完成后,此方法会再次执行,以寻找并执行下一个子活动。 显然,执行过程是一波一波或一段一段推进的。 因为工作流可以被保留并从内存中卸载,所以在两波执行之间可以相隔很长时间。 此外,这些执行波能够在不同的线程、进程甚至计算机上执行(因为保留的工作流实例可以在不同的进程或计算机上重新加载)。 了解如何为多个执行波编程是成为一个熟练的控制流活动创建者的最大挑战之一:

void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
{
  // Calculate the index of the next activity to scheduled
  int currentExecutingActivity = this.current.Get(context);
  int next = currentExecutingActivity + 1;

  // If index within boundaries...
if (next < this.Activities.Count)
  {
    // Schedule the next activity
    context.ScheduleActivity(this.Activities[next], this.OnChildCompleted);

    // Store the index in the collection of the activity executing
    this.current.Set(context, next);
  }
}

以下代码段显示了如何使用此活动。 在本示例中,我要将三个字符串写入控制台(“Hello”、“Workflow”和“!”):

var act = new SimpleSequence()
{
  Activities = 
  {
    new WriteLine { Text = "Hello" },
    new WriteLine { Text = "Workflow" },
    new WriteLine { Text = "!" }
  }
};

WorkflowInvoker.Invoke(act);

我创建了自己的 SimpleSequence! 现在,让我们迎接下一个挑战。

实现新的控制流模式

接下来,我将创建一个复杂的控制流活动。 正如我之前提到的,您的选择并不仅限于 WF 4 附带的控制流活动。 本节将介绍如何构建您自己的控制流活动,以支持 WF 4 自带的现成控制流模式以外的模式。

我们将把新的控制流活动命名为 Series。 目标很简单:提供支持 GoTo 的 Sequence,通过工作流内部(通过 GoTo 活动)或通过主机(通过恢复众所周知的书签)显式操作下一个要执行的活动。

为了实现这个新的控制流,我需要创建两个活动:Series,一个复合活动,包含活动集合并按顺序执行其中的活动(但允许跳转至序列中的任一项);GoTo,一个叶活动,我将在 Series 内部使用此活动显式建立跳转模型。

总的来说,我将一一列举自定义控制活动的目标和要求:

  1. 它是一个活动 Sequence。
  2. 它可以包含 GoTo 活动(在任何深度),用于将执行点更改至 Series 的任一直接子活动。
  3. 也可以从外部(例如,从一个用户)接收 GoTo 消息,将执行点更改至 Series 的任一直接子活动。

首先实现 Series 活动。 让我们用简单的语言来描述执行语义:

  • 活动的用户必须通过 Activities 属性提供要按顺序执行的子活动集合。
  • 在执行方法中:
    • 用子活动可以使用的方法为 GoTo 创建一个书签。
    • 活动包含一个内部变量,其值是正在执行的活动实例。
    • 如果子活动集合中包含内容,则安排第一个子活动。
    • 当子活动完成时:
      • 在 Activities 集合中查找已完成的活动。
      • 递增最后执行的项的索引。
      • 如果索引仍在子活动集合的范围内,则安排下一个子活动。
      • 重复执行。
  • 如果已恢复 GoTo 书签:
    • 获取我们要转到的活动的名称。
    • 在活动集合中找到该活动。
    • 将目标活动安排在执行集中,然后注册一个完成回调,以安排下一个活动。
    • 取消当前正在执行的活动。
    • 将当前正在执行的活动存储到“current”变量中。

图 4 中的代码示例显示了 Series 活动的实现,其行为方式与上文所述完全相同。

图 4 Series 活动

public class Series : NativeActivity
{
  internal static readonly string GotoPropertyName = 
    "Microsoft.Samples.CustomControlFlow.Series.Goto";

  // Child activities and variables collections
  Collection<Activity> activities;
  Collection<Variable> variables;

  // Activity instance that is currently being executed
  Variable<ActivityInstance> current = new Variable<ActivityInstance>();
 
  // For externally initiated goto's; optional
  public InArgument<string> BookmarkName { get; set; }

  public Series() { }

  public Collection<Activity> Activities 
  { 
    get {
      if (this.activities == null)
        this.activities = new Collection<Activity>();
    
      return this.activities; 
    } 
  }

  public Collection<Variable> Variables 
  { 
    get {
      if (this.variables == null)
        this.variables = new Collection<Variable>();

      return this.variables; 
    } 
  }
    
  protected override void CacheMetadata(NativeActivityMetadata metadata)
  {                        
    metadata.SetVariablesCollection(this.Variables);
    metadata.SetChildrenCollection(this.Activities);
    metadata.AddImplementationVariable(this.current);
    metadata.AddArgument(new RuntimeArgument("BookmarkName", typeof(string), 
                                              ArgumentDirection.In));
  }

  protected override bool CanInduceIdle { get { return true; } }

  protected override void Execute(NativeActivityContext context)
  {
    // If there activities in the collection...
if (this.Activities.Count > 0)
    {
      // Create a bookmark for signaling the GoTo
      Bookmark internalBookmark = context.CreateBookmark(this.Goto,
                BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);

      // Save the name of the bookmark as an execution property
      context.Properties.Add(GotoPropertyName, internalBookmark);

      // Schedule the first item in the list and save the resulting 
      // ActivityInstance in the "current" implementation variable
      this.current.Set(context, context.ScheduleActivity(this.Activities[0], 
                                this.OnChildCompleted));

      // Create a bookmark for external (host) resumption
      if (this.BookmarkName.Get(context) != null)
        context.CreateBookmark(this.BookmarkName.Get(context), this.Goto,
            BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);
    }
  }

  void Goto(NativeActivityContext context, Bookmark b, object obj)
  {
    // Get the name of the activity to go to
    string targetActivityName = obj as string;

    // Find the activity to go to in the children list
    Activity targetActivity = this.Activities
                                  .Where<Activity>(a =>  
                                         a.DisplayName.Equals(targetActivityName))
                                  .Single();

    // Schedule the activity 
    ActivityInstance instance = context.ScheduleActivity(targetActivity, 
                                                         this.OnChildCompleted);

    // Cancel the activity that is currently executing
    context.CancelChild(this.current.Get(context));

    // Set the activity that is executing now as the current
    this.current.Set(context, instance);
  }

  void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
  {
    // This callback also executes when cancelled child activities complete 
    if (completed.State == ActivityInstanceState.Closed)
    {
      // Find the next activity and execute it
      int completedActivityIndex = this.Activities.IndexOf(completed.Activity);
      int next = completedActivityIndex + 1;

      if (next < this.Activities.Count)
          this.current.Set(context, 
                           context.ScheduleActivity(this.Activities[next],
                           this.OnChildCompleted));
    }
  }
}

此代码中的某些部分与前面示例中的代码类似。 我将讨论此活动的实现。

Series 从 NativeActivity 派生,因为它需要与 WF 运行时交互以安排子活动、创建书签、取消子活动以及使用执行属性。

与先前一样,下一步是为 Series 定义公共签名。 与 SimpleSequence 一样,签名中包含 Activities 和 Variables 集合属性。 还有一个名为 BookmarkName 的字符串输入参数(类型为 InArgument<string>),其值为用于恢复主机而创建的书签的名称。 同样,我将按照“创建-设置-使用”模式设计活动类型。

Series 有一个名为“current”的私有成员,其中包含正在执行的 ActivityInstance,而不是像 SimpleSequence 一样,只是包含指向集合中的项的指针。 为什么 current 是 Variable<ActivityInstance> 而不是 Variable<int>? 因为我需要稍后通过 GoTo 方法控制此活动中当前正在执行的子活动。 我稍后会解释具体细节,现在最重要的是要了解,会有一个用于保存正在执行的活动实例的实现变量:

Variable<ActivityInstance> current = new Variable<ActivityInstance>();

在 CacheMetadata 中,您需要提供有关活动的运行时信息:子活动和变量集合、包含当前活动实例的实现变量以及书签名称参数。 与前一个示例的唯一区别是,我会手动在 WF 运行时中注册 BookmarkName 输入参数,将新的 RuntimeArgument 实例添加到活动元数据中:

protected override void CacheMetadata(NativeActivityMetadata metadata)
{                        
  metadata.SetVariablesCollection(this.Variables);
  metadata.SetChildrenCollection(this.Activities);
  metadata.AddImplementationVariable(this.current);
  metadata.AddArgument(new RuntimeArgument("BookmarkName",  
                                           typeof(string), ArgumentDirection.In));
}

下一个新内容是 CanInduceIdle 属性重载。 这只是活动为 WF 运行时提供的更多元数据。 如果此属性返回 True,我会通知运行时,此活动可以将工作流转入空闲状态。 我需要重写此属性,为创建书签的活动返回 True,因为这些活动会将工作流转入空闲状态以等待其恢复。 此属性的默认值为 False。 如果此属性返回 False,并且我们创建了一个书签,我会在执行活动时收到 InvalidOperationException 异常:

protected override bool CanInduceIdle { get { return true; } }

我通过 Execute 方法创建书签 (internalBookmark) 并将其存储在执行属性中,现在,这个方法变得更有趣了。 但是,在进行下一步之前,让我先介绍一下书签和执行属性。

书签 是一种机制,通过这种机制,活动可以被动等待恢复。 当活动希望“阻止”挂起某个事件时,会注册一个书签,然后返回一个表示继续的执行状态。 这提示运行时:尽管活动的执行尚未完成,当前工作项也没有任何工作要做了。 在您使用书签时,可以利用某种响应执行的形式创建自己的活动:创建书签就会生成活动,恢复书签就会调用一段代码(书签恢复回调),以响应书签的恢复。

与直接面向 CLR 的程序不同,工作流程序是在线程不可知的环境中执行的、以分层形式确定范围的执行树。 这意味着标准的线程本地存储 (TLS) 机制无法直接用于确定给定工作项范围所在的上下文。 工作流执行上下文在活动的环境中引入了执行属性,以便活动能够声明哪些属性在其子树范围之内,并在其子活动之间共享这些属性。 因此,活动能够通过这些属性将数据与其后代共享。

现在,您已经了解了书签和执行属性,让我们回到代码。 我在 Execute 方法开始时创建了书签(使用 context.CreateBookmark),并将其保存到一个执行属性中(使用 context.Properties.Add)。 此书签是一个多次恢复书签,表示它可以进行多次恢复。此书签在其父活动处于执行状态时可用。 它还是 NonBlocking,因此一旦完成了自己的工作,就不会阻止活动完成。 书签恢复后会调用 GoTo 方法,因为我为 CreateBookmark(第一个参数)提供了一个 BookmarkCompletionCallback。 将其保存在执行属性中的原因是让所有子活动都能使用它。 (您稍后会看到 GoTo 活动如何使用此书签。)请注意,执行属性有自己的名称。 因为该名称是一个字符串,我使用它为活动中的属性定义了一个常量 (GotoPropertyName)。 该名称遵循完全限定名称方法。 以下是最佳做法:

internal static readonly string GotoPropertyName = 
                                "Microsoft.Samples.CustomControlFlow.Series.Goto";

...
...
// Create a bookmark for signaling the GoTo
Bookmark internalBookmark = context.CreateBookmark(this.Goto,                                         
                       BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);

// Save the name of the bookmark as an execution property
context.Properties.Add(GotoPropertyName, internalBookmark);

当我声明了书签后,就准备安排我的第一个活动。 我已经在前面的活动中进行过此操作,因此对具体过程很熟悉。 我将安排集合中的第一个活动,并告知运行时,在活动结束时调用 OnChildCompleted 方法(正如我在 SimpleSequence 中所做的)。 Context.ScheduleActivity 将返回一个代表正在执行的活动实例的 ActivityInstance,此实例已分配给当前的实现变量。 让我对此稍加解释。 活动是定义,就像一个类;而 ActivityInstance 则是实际的实例,就像一个对象。 同一个活动可以有多个 ActivityInstance:

// Schedule the first item in the list and save the resulting 
// ActivityInstance in the "current" implementation variable
this.current.Set(context, context.ScheduleActivity(this.Activities[0],  
                                                   this.OnChildCompleted));

最后,我们创建一个书签,主机可以使用此书签跳转到 Series 中的任一活动。 其中的原理很简单:因为主机知道书签的名称,所以它可以通过跳转到 Series 中的任一活动来恢复该书签:

// Create a bookmark for external (host) resumption
 if (this.BookmarkName.Get(context) != null)
     context.CreateBookmark(this.BookmarkName.Get(context), this.Goto,
                           BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);

OnChildCompleted 方法现在应当很简单,因为它与 SimpleSequence 中的方法类似:我在活动集合中寻找下一个元素,并对其进行安排。 主要的区别是,只有在当前活动成功完成执行(即到达关闭状态,未被取消或出错)时,我才会安排下一个活动。

GoTo 方法无疑是最有趣的。 此方法是作为正在恢复的 GoTo 书签的结果执行的。 此方法将收到一些输入数据,这些数据在恢复书签的时候传递。 在本例中,该数据是我们要转到的活动的名称:

void Goto(NativeActivityContext context, Bookmark b, object data)
{
  // Get the name of the activity to go to
  string targetActivityName = data as string;
       
  ...
}

目标活动的名称是活动的 DisplayName 属性。 我在“活动”集合中寻找请求的活动定义。 找到请求的活动后,就对其进行安排,指明活动完成后就应当执行 OnChildCompleted 方法:

// Find the activity to go to in the children list
Activity targetActivity = this.Activities
                              .Where<Activity>(a =>  
                                       a.DisplayName.Equals(targetActivityName))
                              .Single();
// Schedule the activity 
ActivityInstance instance = context.ScheduleActivity(targetActivity, 
                                                     this.OnChildCompleted);

接下来,我将取消当前正在执行的活动实例,并将当前正在执行的活动设置为上一步安排的 ActivityInstance。 这两个任务都使用“current”变量。 首先,将此变量作为 NativeActivityContext 的 CancelChild 方法的参数传递,然后使用前面的代码块中安排的 ActivityInstance 来更新变量的值:

// Cancel the activity that is currently executing
context.CancelChild(this.current.Get(context));

// Set the activity that is executing now as the current
this.current.Set(context, instance);

GoTo 活动

GoTo 活动只能在 Series 活动内使用,用于跳转到其 Activities 集合中的某个活动。 它与命令式程序中的 GoTo 语句类似。 其工作原理非常简单:它恢复由其所在的 Series 活动创建的 GoTo 书签,指明我们要转到的活动的名称。 当书签恢复后,Series 就会跳转到所指的活动。

让我们用简单的语言来描述执行语义:

  • 活动用户必须提供一个字符串 TargetActivityName。 这个参数是必需参数。
  • 在执行时:
    • GoTo 活动会找到 Series 活动创建的“GoTo”书签。
    • 如果找到了书签,就通过传递 TargetActivityName,恢复该书签。
    • 它将创建一个同步书签,因此活动不会完成。
      • 它将由 Series 取消。

图 5 中的代码显示了 GoTo 活动的实现,其行为方式与上文所述完全相同。

图 5 GoTo 活动

public class GoTo : NativeActivity
{
  public GoTo() 
  { }
       
  [RequiredArgument]
  public InArgument<string> TargetActivityName { get; set; }

  protected override bool CanInduceIdle { get { return true; } }
    
  protected override void Execute(NativeActivityContext context)
  {
    // Get the bookmark created by the parent Series
    Bookmark bookmark = context.Properties.Find(Series.GotoPropertyName) as Bookmark;

    // Resume the bookmark passing the target activity name
    context.ResumeBookmark(bookmark, this.TargetActivityName.Get(context));

    // Create a bookmark to leave this activity idle waiting when it does
    // not have any further work to do.
Series will cancel this activity 
    // in its GoTo method
    context.CreateBookmark("SyncBookmark");
  }

}

GoTo 从 NativeActivity 派生,因为它需要与 WF 运行时交互以创建并恢复书签,还要使用执行属性。 它的公共签名由 TargetActivityName 字符串输入参数组成,其中包含我们要跳转到的活动的名称。 我用 RequiredArgument 特性修饰此参数,表示 WF 验证服务将会强制其使用一个表达式。

我依赖默认的 CacheMetadata 实现来反射活动的公共接口,以查找并注册运行时元数据。

最重要的部分在 Evaluate 方法中。 我首先查找由父 Series 活动创建的书签。 因为该书签被存储为一个执行属性,所以我在 context.Properties 中进行查找。 找到书签后,我会通过将 TargetActivityName 作为输入数据传递,恢复该书签。 此书签恢复操作会导致调用 Series.Goto 方法(因为该方法是在创建书签时提供的书签回调)。 该方法将在集合中查找下一个活动,安排该活动并取消当前正在执行的活动:

// Get the bookmark created by the parent Series
Bookmark bookmark = context.Properties.Find(Series.GotoPropertyName) as Bookmark; 

// Resume the bookmark passing the target activity name
context.ResumeBookmark(bookmark, this.TargetActivityName.Get(context));

最后一行代码是最难的:创建一个同步书签,使 GoTo 活动保持运行状态。 因此,当 GoTo.Execute 方法完成时,此活动仍然会处于执行状态,等待激发因素来恢复书签。 在我讨论 Series.Goto 的代码时,我曾经提到过该代码取消了正在执行的活动。 在本例中,Series.Goto 实际上会取消正在等待该书签恢复的 Goto 活动实例。

让我进行更详细的解释:GoTo 活动的实例由 Series 活动安排。 当此活动完成时,Series 中的完成回调 (OnChildCompleted) 将在 Series.Activities 集合中寻找下一个活动并对其进行安排。 在本例中,我不想安排下一个活动,而是想安排 TargetActivityName 引用的活动。 此书签可以实现此目的,因为它会在安排目标活动时将 GoTo 活动保持在执行状态。 如果取消了 GoTo,Series.OnChildCompleted 回调将不执行任何操作,因为只有当完成状态为 Closed(在本例中为 Cancelled)时,它才会安排下一个活动:

// Create a bookmark to leave this activity idle waiting when it does
// not have any further work to do.
Series will cancel this activity 
// in its GoTo method
context.CreateBookmark("SyncBookmark");

图 6 显示了使用此活动的示例。 在本例中,我将根据变量的值返回到上一个状态。 下面是一个简单的示例,用于说明 Series 的基本使用方法,但是此活动还可用于实现复杂的实际业务方案,以帮助您在连续的过程中跳过、重做或跳转至某些步骤。

图 6 在 Series 中使用 GoTo

var counter = new Variable<int>();

var act = new Series
{
  Variables = { counter},
  Activities =
  {
    new WriteLine 
    {
      DisplayName = "Start",
      Text = "Step 1"
    },
    new WriteLine
    {
      DisplayName = "First Step",
      Text = "Step 2"
    },
    new Assign<int>
    {
      To = counter,
      Value = new InArgument<int>(c => counter.Get(c) + 1)
    },
    new If 
    {
      Condition = new InArgument<bool>(c => counter.Get(c) == 3),
      Then = new WriteLine
      {
        Text = "Step 3"
      },
      Else = new GoTo { TargetActivityName = "First Step" }
    },
    new WriteLine 
    {
      Text = "The end!"
    }
  }
};

WorkflowInvoker.Invoke(act);

参考

Windows Workflow Foundation 4 开发人员中心
msdn.microsoft.com/netframework/aa663328

Endpoint.tv:活动创建最佳实践
channel9.msdn.com/shows/Endpoint/endpointtv-Workflow-and-Custom-Activities-Best-Practices-Part-1/

设计和实现自定义活动
msdn.microsoft.com/library/dd489425

ActivityInstance 类
msdn.microsoft.com/library/system.activities.activityinstance

RuntimeArgument 类
msdn.microsoft.com/library/dd454495

遵循流程

在本文中,我介绍了编写自定义控制流活动的一些常规性内容。 在 WF 4 中,控制流范围并不固定,编写自定义活动的过程已经大幅简化。 如果提供的现成活动无法满足您的需求,您可以轻松创建自已的活动。 在本文中,我首先例举了一个简单的控制流活动,然后用我的方式实现了一个自定义控制流活动,将新的执行语义添加到 WF 4 中。 如果您想了解更多内容,CodePlex 提供了有关状态机的社区技术预览,并提供了完整的源代码。 您还可以找到一系列第 9 频道视频,帮助您了解创建活动的最佳实践。 编写自己的自定义活动时,您可以在 WF 中表现出任何控制流模式,并调整 WF 以适应您的问题的特殊之处。

Leon Welicki 是 Microsoft Windows Workflow Foundation (WF) 团队的一名项目经理,从事 WF 运行时方面的工作。在加入 Microsoft 之前,他曾担任西班牙一家大型电信公司的首席架构师兼开发经理,并且是西班牙马德里萨拉曼卡宗座大学计算机科学研究生学院的外聘副教授。

衷心感谢以下技术专家对本文的审阅:Joe ClancyDan GlickRajesh SampathBob SchmidtIsaac Yuen