WF 4 活动工具箱中的示例包括:Sequence、Parallel、If、ForEach、Pick、Flowchart 和 Switch 等等。

WF 控制流是基于层次结构的,因此 WF 程序就是一个活动树。

您将了解如何采用循序渐进的方法编写自己的控制流活动:我们从一个非常简单的控制流活动开始,逐渐丰富其内容,最终打造一个有用的新控制流活动。我们所有示例的源代码都可供下载。

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

活动

我们称之为 活动。

msdn.microsoft.com/library/dd560893。

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

图 1 活动类型层次结构

控制流活动通常用于安排其他活动(例如 Sequence、Parallel 或 Flowchart),但也可能包含以下活动:使用 CancellationScope 或 Pick 实施自定义取消;使用 Receive 创建书签;使用 Persist 实现持久性。

变量表示数据的临时存储。

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

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

将变量和参数结合使用,可以为活动之间的通信提供可预测的通信模式。

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

一个简单的控制流活动

对于这种情况,我们需要一个活动,该活动能够根据布尔条件的值执行另一个活动。

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

  • 这个参数是必需参数。
  • 活动用户可以提供主体,即在条件为 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);
  }
}
        

 

因此,它必须从 NativeActivity 派生,因为它需要与 WF 运行时交互以安排子活动。

WF 运行时将在准备要执行的活动时强制进行此项验证:

  1.  
  2.           [RequiredArgument]
  3. public InArgument<bool> Condition { get; set; }
  4.  
  5. public Activity Body { get; set; }
  6.         

WF 运行时不会立即执行活动,而是将这些活动添加到一个工作项列表中以安排执行:

  1.  
  2.           protected override void Execute(NativeActivityContext context)
  3. {
  4.     if (context.GetValue(this.Condition) && this.Body != null)
  5.         context.ScheduleActivity(this.Body);
  6. }
  7.         

这意味着该类型便于进行 XAML 序列化。

在本例中,如果当前日期为星期六,则将向控制台输出字符串“Rest!”:

  1.  
  2.           var act = new ExecuteIfTrue
  3. {
  4.   Condition = new InArgument<bool>(c => DateTime.Now.DayOfWeek == DayOfWeek.Tuesday),
  5.   Body = new WriteLine { Text = "Rest!" }
  6. };
  7.  
  8. WorkflowInvoker.Invoke(act);
  9.         

但是不要被该代码的简单性所迷惑,它实际上是一个功能完备的控制流活动!

安排多个子活动

此活动在功能上与产品附带的 Sequence 几乎完全相同。

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

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

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

图 2 SimpleSequence 活动

  1.  
  2.           public class SimpleSequence : NativeActivity
  3. {
  4.   // Child activities collection
  5.   Collection<Activity> activities;
  6.   Collection<Variable> variables;
  7.  
  8.   // Pointer to the current item in the collection being executed
  9.   Variable<int> current = new Variable<int>() { Default = 0 };
  10.      
  11.   public SimpleSequence() { }
  12.  
  13.   // Collection of children to be executed sequentially by SimpleSequence
  14.   public Collection<Activity> Activities
  15.   {
  16.     get
  17.     {
  18.       if (this.activities == null)
  19.         this.activities = new Collection<Activity>();
  20.  
  21.       return this.activities;
  22.     }
  23.   }
  24.  
  25.   public Collection<Variable> Variables 
  26.   { 
  27.     get 
  28.     {
  29.       if (this.variables == null)
  30.         this.variables = new Collection<Variable>();
  31.  
  32.       return this.variables; 
  33.     } 
  34.   }
  35.  
  36.   protected override void CacheMetadata(NativeActivityMetadata metadata)
  37.   {
  38.     metadata.SetChildrenCollection(this.activities);
  39.     metadata.SetVariablesCollection(this.variables);
  40.     metadata.AddImplementationVariable(this.current);
  41.   }
  42.  
  43.   protected override void Execute(NativeActivityContext context)
  44.   {
  45.     // Schedule the first activity
  46.     if (this.Activities.Count > 0)
  47.       context.ScheduleActivity(this.Activities[0], this.OnChildCompleted);
  48.   }
  49.  
  50.   void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
  51.   {
  52.     // Calculate the index of the next activity to scheduled
  53.     int currentExecutingActivity = this.current.Get(context);
  54.     int next = currentExecutingActivity + 1;
  55.  
  56.     // If index within boundaries...
  57.           if (next < this.Activities.Count)
  58.     {
  59.       // Schedule the next activity
  60.       context.ScheduleActivity(this.Activities[next], this.OnChildCompleted);
  61.  
  62.       // Store the index in the collection of the activity executing
  63.       this.current.Set(context, next);
  64.     }
  65.   }
  66. }
  67.         

代码很简单,但引入了一些有趣的概念。

因此,它从 NativeActivity 派生,因为它需要与运行时交互以安排子活动。

因此,这些属性符合“创建-设置-使用”模式。

图 3 延迟实例化方法

  1.  
  2.           public Collection<Activity> Activities
  3. {
  4.   get
  5.   {
  6.     if (this.activities == null)
  7.       this.activities = new Collection<Activity>();
  8.  
  9.     return this.activities;
  10.   }
  11. }
  12.  
  13. public Collection<Variable> Variables 
  14.   get 
  15.   {
  16.     if (this.variables == null)
  17.       this.variables = new Collection<Variable>();
  18.  
  19.     return this.variables; 
  20.   } 
  21. }
  22.         

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

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

此目的通过使用 ImplementationVariable 来实现。

为了清楚地说明这一点,并继续 Sequence 示例:如果保存了 SimpleSequence 实例,当它复原时,将“记住”执行过的最后一个活动的索引。

此项活动在 CacheMetadata 方法的执行过程中进行。

在 CacheMetadata 中,此活动会说:“大家好,我是 If 活动,我有一个输入变量叫 Condition,还有两个子活动,分别是 Then 和 Else。”当使用 SimpleSequence 活动时,此活动会说:“大家好,我是 SimpleSequence,我有一个子活动集合、一个变量集合和一个实现变量。”CacheMetadata 代码中包含的内容也无非就是 SimpleSequence 代码中的那些内容:

  1.  
  2.           protected override void CacheMetadata(NativeActivityMetadata metadata)
  3. {
  4.   metadata.SetChildrenCollection(this.activities);
  5.   metadata.SetVariablesCollection(this.variables);
  6.   metadata.AddImplementationVariable(this.current);
  7. }
  8.         

而 SimpleSequence 则相反,因为默认实现“猜不出”我要使用实现变量,所以必须实现此活动。

安排活动时不会调用 CompletionCallback,安排的活动执行完成时才会调用 CompletionCallback:

  1.  
  2.           protected override void Execute(NativeActivityContext context)
  3. {
  4.   // Schedule the first activity
  5.   if (this.Activities.Count > 0)
  6.     context.ScheduleActivity(this.Activities[0], this.OnChildCompleted);
  7. }
  8.         

了解如何为多个执行波编程是成为一个熟练的控制流活动创建者的最大挑战之一:

  1.  
  2.           void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
  3. {
  4.   // Calculate the index of the next activity to scheduled
  5.   int currentExecutingActivity = this.current.Get(context);
  6.   int next = currentExecutingActivity + 1;
  7.  
  8.   // If index within boundaries...
  9.           if (next < this.Activities.Count)
  10.   {
  11.     // Schedule the next activity
  12.     context.ScheduleActivity(this.Activities[next], this.OnChildCompleted);
  13.  
  14.     // Store the index in the collection of the activity executing
  15.     this.current.Set(context, next);
  16.   }
  17. }
  18.         

在本示例中,我要将三个字符串写入控制台(“Hello”、“Workflow”和“!”):

  1.  
  2.           var act = new SimpleSequence()
  3. {
  4.   Activities = 
  5.   {
  6.     new WriteLine { Text = "Hello" },
  7.     new WriteLine { Text = "Workflow" },
  8.     new WriteLine { Text = "!" }
  9.   }
  10. };
  11.  
  12. WorkflowInvoker.Invoke(act);
  13.         

现在,让我们迎接下一个挑战。

实现新的控制流模式

本节将介绍如何构建您自己的控制流活动,以支持 WF 4 自带的现成控制流模式以外的模式。

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

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

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

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

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

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

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

图 4 Series 活动

  1.  
  2.           public class Series : NativeActivity
  3. {
  4.   internal static readonly string GotoPropertyName = 
  5.     "Microsoft.Samples.CustomControlFlow.Series.Goto";
  6.  
  7.   // Child activities and variables collections
  8.   Collection<Activity> activities;
  9.   Collection<Variable> variables;
  10.  
  11.   // Activity instance that is currently being executed
  12.   Variable<ActivityInstance> current = new Variable<ActivityInstance>();
  13.  
  14.   // For externally initiated goto's; optional
  15.   public InArgument<string> BookmarkName { get; set; }
  16.  
  17.   public Series() { }
  18.  
  19.   public Collection<Activity> Activities 
  20.   { 
  21.     get {
  22.       if (this.activities == null)
  23.         this.activities = new Collection<Activity>();
  24.     
  25.       return this.activities; 
  26.     } 
  27.   }
  28.  
  29.   public Collection<Variable> Variables 
  30.   { 
  31.     get {
  32.       if (this.variables == null)
  33.         this.variables = new Collection<Variable>();
  34.  
  35.       return this.variables; 
  36.     } 
  37.   }
  38.     
  39.   protected override void CacheMetadata(NativeActivityMetadata metadata)
  40.   {                        
  41.     metadata.SetVariablesCollection(this.Variables);
  42.     metadata.SetChildrenCollection(this.Activities);
  43.     metadata.AddImplementationVariable(this.current);
  44.     metadata.AddArgument(new RuntimeArgument("BookmarkName", typeof(string), 
  45.                                               ArgumentDirection.In));
  46.   }
  47.  
  48.   protected override bool CanInduceIdle { get { return true; } }
  49.  
  50.   protected override void Execute(NativeActivityContext context)
  51.   {
  52.     // If there activities in the collection...
  53.           if (this.Activities.Count > 0)
  54.     {
  55.       // Create a bookmark for signaling the GoTo
  56.       Bookmark internalBookmark = context.CreateBookmark(this.Goto,
  57.                 BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);
  58.  
  59.       // Save the name of the bookmark as an execution property
  60.       context.Properties.Add(GotoPropertyName, internalBookmark);
  61.  
  62.       // Schedule the first item in the list and save the resulting 
  63.       // ActivityInstance in the "current" implementation variable
  64.       this.current.Set(context, context.ScheduleActivity(this.Activities[0], 
  65.                                 this.OnChildCompleted));
  66.  
  67.       // Create a bookmark for external (host) resumption
  68.       if (this.BookmarkName.Get(context) != null)
  69.         context.CreateBookmark(this.BookmarkName.Get(context), this.Goto,
  70.             BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);
  71.     }
  72.   }
  73.  
  74.   void Goto(NativeActivityContext context, Bookmark b, object obj)
  75.   {
  76.     // Get the name of the activity to go to
  77.     string targetActivityName = obj as string;
  78.  
  79.     // Find the activity to go to in the children list
  80.     Activity targetActivity = this.Activities
  81.                                   .Where<Activity>(a =>  
  82.                                          a.DisplayName.Equals(targetActivityName))
  83.                                   .Single();
  84.  
  85.     // Schedule the activity 
  86.     ActivityInstance instance = context.ScheduleActivity(targetActivity, 
  87.                                                          this.OnChildCompleted);
  88.  
  89.     // Cancel the activity that is currently executing
  90.     context.CancelChild(this.current.Get(context));
  91.  
  92.     // Set the activity that is executing now as the current
  93.     this.current.Set(context, instance);
  94.   }
  95.  
  96.   void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
  97.   {
  98.     // This callback also executes when cancelled child activities complete 
  99.     if (completed.State == ActivityInstanceState.Closed)
  100.     {
  101.       // Find the next activity and execute it
  102.       int completedActivityIndex = this.Activities.IndexOf(completed.Activity);
  103.       int next = completedActivityIndex + 1;
  104.  
  105.       if (next < this.Activities.Count)
  106.           this.current.Set(context, 
  107.                            context.ScheduleActivity(this.Activities[next],
  108.                            this.OnChildCompleted));
  109.     }
  110.   }
  111. }
  112.         

我将讨论此活动的实现。

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

同样,我将按照“创建-设置-使用”模式设计活动类型。

我稍后会解释具体细节,现在最重要的是要了解,会有一个用于保存正在执行的活动实例的实现变量:

  1.  
  2.           Variable<ActivityInstance> current = new Variable<ActivityInstance>();
  3.         

与前一个示例的唯一区别是,我会手动在 WF 运行时中注册 BookmarkName 输入参数,将新的 RuntimeArgument 实例添加到活动元数据中:

  1.  
  2.           protected override void CacheMetadata(NativeActivityMetadata metadata)
  3. {                        
  4.   metadata.SetVariablesCollection(this.Variables);
  5.   metadata.SetChildrenCollection(this.Activities);
  6.   metadata.AddImplementationVariable(this.current);
  7.   metadata.AddArgument(new RuntimeArgument("BookmarkName",  
  8.                                            typeof(string), ArgumentDirection.In));
  9. }
  10.         

如果此属性返回 False,并且我们创建了一个书签,我会在执行活动时收到 InvalidOperationException 异常:

  1.  
  2.           protected override bool CanInduceIdle { get { return true; } }
  3.         

但是,在进行下一步之前,让我先介绍一下书签和执行属性。

在您使用书签时,可以利用某种响应执行的形式创建自己的活动:创建书签就会生成活动,恢复书签就会调用一段代码(书签恢复回调),以响应书签的恢复。

因此,活动能够通过这些属性将数据与其后代共享。

以下是最佳做法:

  1.  
  2.           internal static readonly string GotoPropertyName = 
  3.                                 "Microsoft.Samples.CustomControlFlow.Series.Goto";
  4.  
  5. ...
  6.           ...
  7.           // Create a bookmark for signaling the GoTo
  8. Bookmark internalBookmark = context.CreateBookmark(this.Goto,                                         
  9.                        BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);
  10.  
  11. // Save the name of the bookmark as an execution property
  12. context.Properties.Add(GotoPropertyName, internalBookmark);
  13.         

同一个活动可以有多个 ActivityInstance:

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

其中的原理很简单:因为主机知道书签的名称,所以它可以通过跳转到 Series 中的任一活动来恢复该书签:

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

主要的区别是,只有在当前活动成功完成执行(即到达关闭状态,未被取消或出错)时,我才会安排下一个活动。

在本例中,该数据是我们要转到的活动的名称:

  1.  
  2.           void Goto(NativeActivityContext context, Bookmark b, object data)
  3. {
  4.   // Get the name of the activity to go to
  5.   string targetActivityName = data as string;
  6.        
  7.   ...
  8.           }
  9.         

找到请求的活动后,就对其进行安排,指明活动完成后就应当执行 OnChildCompleted 方法:

  1.  
  2.           // Find the activity to go to in the children list
  3. Activity targetActivity = this.Activities
  4.                               .Where<Activity>(a =>  
  5.                                        a.DisplayName.Equals(targetActivityName))
  6.                               .Single();
  7. // Schedule the activity 
  8. ActivityInstance instance = context.ScheduleActivity(targetActivity, 
  9.                                                      this.OnChildCompleted);
  10.         

首先,将此变量作为 NativeActivityContext 的 CancelChild 方法的参数传递,然后使用前面的代码块中安排的 ActivityInstance 来更新变量的值:

  1.  
  2.           // Cancel the activity that is currently executing
  3. context.CancelChild(this.current.Get(context));
  4.  
  5. // Set the activity that is executing now as the current
  6. this.current.Set(context, instance);
  7.         

GoTo 活动

当书签恢复后,Series 就会跳转到所指的活动。

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

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

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

图 5 GoTo 活动

  1.  
  2.           public class GoTo : NativeActivity
  3. {
  4.   public GoTo() 
  5.   { }
  6.        
  7.   [RequiredArgument]
  8.   public InArgument<string> TargetActivityName { get; set; }
  9.  
  10.   protected override bool CanInduceIdle { get { return true; } }
  11.     
  12.   protected override void Execute(NativeActivityContext context)
  13.   {
  14.     // Get the bookmark created by the parent Series
  15.     Bookmark bookmark = context.Properties.Find(Series.GotoPropertyName) as Bookmark;
  16.  
  17.     // Resume the bookmark passing the target activity name
  18.     context.ResumeBookmark(bookmark, this.TargetActivityName.Get(context));
  19.  
  20.     // Create a bookmark to leave this activity idle waiting when it does
  21.     // not have any further work to do.
  22.           Series will cancel this activity 
  23.     // in its GoTo method
  24.     context.CreateBookmark("SyncBookmark");
  25.   }
  26.  
  27. }
  28.         

我用 RequiredArgument 特性修饰此参数,表示 WF 验证服务将会强制其使用一个表达式。

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

该方法将在集合中查找下一个活动,安排该活动并取消当前正在执行的活动:

  1.  
  2.           // Get the bookmark created by the parent Series
  3. Bookmark bookmark = context.Properties.Find(Series.GotoPropertyName) as Bookmark; 
  4.  
  5. // Resume the bookmark passing the target activity name
  6. context.ResumeBookmark(bookmark, this.TargetActivityName.Get(context));
  7.         

在本例中,Series.Goto 实际上会取消正在等待该书签恢复的 Goto 活动实例。

如果取消了 GoTo,Series.OnChildCompleted 回调将不执行任何操作,因为只有当完成状态为 Closed(在本例中为 Cancelled)时,它才会安排下一个活动:

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

下面是一个简单的示例,用于说明 Series 的基本使用方法,但是此活动还可用于实现复杂的实际业务方案,以帮助您在连续的过程中跳过、重做或跳转至某些步骤。

图 6 在 Series 中使用 GoTo

  1.  
  2.           var counter = new Variable<int>();
  3.  
  4. var act = new Series
  5. {
  6.   Variables = { counter},
  7.   Activities =
  8.   {
  9.     new WriteLine 
  10.     {
  11.       DisplayName = "Start",
  12.       Text = "Step 1"
  13.     },
  14.     new WriteLine
  15.     {
  16.       DisplayName = "First Step",
  17.       Text = "Step 2"
  18.     },
  19.     new Assign<int>
  20.     {
  21.       To = counter,
  22.       Value = new InArgument<int>(c => counter.Get(c) + 1)
  23.     },
  24.     new If 
  25.     {
  26.       Condition = new InArgument<bool>(c => counter.Get(c) == 3),
  27.       Then = new WriteLine
  28.       {
  29.         Text = "Step 3"
  30.       },
  31.       Else = new GoTo { TargetActivityName = "First Step" }
  32.     },
  33.     new WriteLine 
  34.     {
  35.       Text = "The end!"
  36.     }
  37.   }
  38. };
  39.  
  40. WorkflowInvoker.Invoke(act);
  41.         
 

参考

msdn.microsoft.com/netframework/aa663328

channel9.msdn.com/shows/Endpoint/endpointtv-Workflow-and-Custom-Activities-Best-Practices-Part-1/

msdn.microsoft.com/library/dd489425

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

msdn.microsoft.com/library/dd454495

 

遵循流程

编写自己的自定义活动时,您可以在 WF 中表现出任何控制流模式,并调整 WF 以适应您的问题的特殊之处。

 

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

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

相关文章: