共用方式為


本文章是由機器翻譯。

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 and 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