65.9K
CodeProject 正在变化。 阅读更多。
Home

你好,WF!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.52/5 (42投票s)

2006年11月12日

11分钟阅读

viewsIcon

177044

downloadIcon

741

介绍了Windows Workflow Foundation(WF)的非常基础的知识,使用了“最愚蠢的WF应用程序”。

引言

本文包含了世界上最微不足道、最无用的Windows Workflow Foundation (WF) 应用程序,以及 WF 本身的一些背景介绍。这个软件怪胎的目的是让 WF 新手了解如何入门这个令人兴奋的 .NET 3.0 新特性。它不涉及任何复杂场景,当然也不包含任何 WF 最佳实践。它只是展示了如何构建一个最小化的 WF 应用。如果您有更实际的需求,可以将此代码作为真实开发的起点。

我现在(2006年11月,纽约市)就承认,我绝不是 WF 专家。我只是对 WF 感到非常兴奋,并一直在业余时间阅读和尝试使用它。本文纯粹是出于对这项技术的兴奋而撰写,希望这种兴奋能传递给您,让您也感到兴奋。

本文中展示的应用程序是 Dharma Shukla 和 Bob Schmidt 合著的优秀书籍《Essential Windows Workflow Foundation》开篇演示应用程序的一个变体。如果您想认真学习 WF,我强烈推荐这本书。您还可以查看 MSDN 上的这篇由 Don Box 和 Dharma Shukla 撰写的文章

WF 是什么鬼?

在开始构建 WF 应用程序之前,让我们花点时间来理解 WF 背后的基本思想。WF 代表 Windows Workflow Foundation。它是 .NET Framework 3.0 的一个子系统,提供了一个用于创建和执行基于工作流的应用程序的运行时。这听起来像漂亮的营销辞令,但具体是什么意思呢?好问题……

从非常高层次的角度来看,WF 允许您创建可以在不活动时持久化到后端存储,并在需要时恢复的程序。您可以使用一种称为 eXtensible Application Markup Language (XAML) 的基于 XML 的语言声明整体程序流,并在运行时加载/执行您的 XAML 工作流。使用标记语言表达应用程序的通用逻辑流程极大地简化了开发过程,特别是因为它为图形设计工具在软件创建中的使用开辟了新的可能性。

为什么要使用 WF?

许多应用程序本质上是反应式的。它们在不确定的时间内等待某个事件的发生(例如,某个目录中创建了一个文件)。当外部刺激到来时,应用程序就会忙于处理传入的数据。这个常见场景的问题在于,由于应用程序在等待外部输入时会占用它运行的线程的处理时间,因此性能和可伸缩性会受到很大影响。

WF 为此问题提供了解决方案。由于 WF 中的工作流被表示为对象树,并且这些对象支持序列化/反序列化,因此 WF 工作流内置了从数据库保存和加载的功能。我说的不是只保存应用程序处理的数据;我指的是“程序”本身被保存。

应用程序的状态有效地被“冻结”并放入一个低温冰箱,可以这么说。当需要恢复工作流处理时(即外部刺激到来),“冷冻”的工作流会被“解冻”并继续正常执行。请记住,工作流可以在不同的计算机上恢复,可能是在它被冷冻的地点一千里之外,十五个月之后。这在性能和可伸缩性方面带来了巨大的提升,因为工作流不限于特定的线程、进程甚至计算机。

以上就是我将要提供的关于 WF 的所有介绍性材料。我并没有解释 WF 的全部内容。如果您对 WF 的更全面、更详尽的解释感兴趣,我建议您阅读前面提到的书籍(《Essential Windows Workflow Foundation》)。现在,让我们来玩一些 WF 代码!

最愚蠢的 WF 应用程序

我将在这里向您展示的应用程序会询问用户的姓名,等待用户输入,然后将“Hello, UserName!”打印到控制台。显然,这个应用程序之所以有趣,仅仅是因为它使用了 Windows Workflow Foundation 来完成它的魔力。我试图尽可能地简化和最小化 WF 的使用,以便您能轻松地看到如何设置一个使用 WF 的应用程序。

此应用程序涉及两个程序集

  1. FirstWFLibrary.DLL – 此程序集包含自定义的 WF 活动,用于执行向控制台打印输出和获取用户姓名的任务。
  2. FirstWFApp.EXE – 此程序集是一个控制台应用程序,它加载 WF WorkflowRuntime 并利用 FirstWFLibrary 中的活动。

每个程序集都包含了谜题的两个部分

FirstWFLibrary.DLL

  • 自定义活动 – WF 工作流中的基本工作单元是“活动”。所有自定义活动都派生自 Activity 基类,并重写其受保护的 Execute 方法来提供自己的执行逻辑。
  • 命名空间映射 – 为了能够在 XAML 中使用我们的自定义活动,我们需要一种方法来告诉 XAML 解析器这些类存在于哪个 CLR 命名空间中。这通过将一个属性应用于程序集来实现,我们稍后会看到。

FirstWFApp.EXE

  • 工作流声明 – 在此应用程序中,读取用户姓名并将其回显给用户的所需工作流通过 XAML 进行表达。此程序集中的 XAML 文件使用 FirstWFLibrary 程序集中声明的自定义活动。请注意,不要求工作流在 XAML 中声明。也可以用其他格式声明,例如 C# 代码。我选择在此应用程序中使用 XAML 来表达工作流,因为那样更有趣。:)
  • 工作流运行时宿主 – 谜题的最后一部分是一个类,它加载 WF 运行时,对其进行配置,然后指示它开始运行。此应用程序中的 EntryPoint 类负责这项工作。

本文的其余部分将仔细检查上面列出的应用程序的每个部分。

自定义活动

如前所述,这个演示应用程序需要执行三个任务。首先,它必须在控制台窗口中显示一条消息,询问用户的姓名。然后,它必须等待用户输入姓名并按 Enter 键。最后,它向用户显示另一条消息,其中包含他们的姓名。

这些任务中的每一个都表示为一个单独的 Activity 派生类。这些类的实例将执行使程序运行所需的实际工作。首先,让我们看看如何将初始提示显示到控制台。

/// <summary>
/// Asks the user for their name.
/// </summary>
public class PromptForUserName : Activity
{
 protected override ActivityExecutionStatus Execute(
                                  ActivityExecutionContext executionContext )
 {
  Console.Write( "Please enter your name and press Enter: " );
  return ActivityExecutionStatus.Closed;
 }
}

这个类完美地演示了如何创建可以包含在 WF 工作流中的活动。它继承自 System.Workflow.ComponentModel.Activity 类,并重写 Execute 方法以提供自定义活动执行逻辑。由于此活动在将消息写入控制台后在逻辑上是完整的,因此它返回 ActivityExecutionStatus.Closed 以通知 WF 运行时它已完成。

您可能会想,为什么活动在尚未完成执行时会从 Execute 方法返回?还记得我之前提到 WF 工作流可以“钝化”并存储在数据库中,直到需要继续执行吗?好吧,活动的 Execute 方法将在无法完成直到外部输入最终到达时返回 ActivityExecutionStatus.Executing

事实上,最愚蠢的 WF 应用程序的下一步需要无限长的时间才能继续处理。用户可能需要三秒钟或三天才能输入姓名。在此期间,工作流将没有要处理的内容。如果这是一个不那么愚蠢的 WF 应用程序,我们可能会决定钝化工作流直到用户姓名最终到达,届时我们将恢复工作流并让它继续。我们在这里不这样做,但下一个活动展示了如何设置一个“书签”,以便 WF 运行时可以在输入到达时通知活动。

/// <summary>
/// An activity which represents reading a line of text from the console.
/// </summary>
public class ReadConsoleLine : Activity
{
 #region InputText Property

 private string inputText;
 public string InputText
 {
  get { return this.inputText; }
 }

 #endregion // InputText Property

 #region Execute [override]

 protected override ActivityExecutionStatus Execute( 
    ActivityExecutionContext executionContext )
 {
  // Create a WorkflowQueue, which allows this activity to "bookmark" 
  // where it should continue executing once the external input arrives 
  // (in this case, a string is read from the console).
  WorkflowQueue workflowQueue = this.GetWorkflowQueue( executionContext );

  // Attach a handler which processes the external input.
  workflowQueue.QueueItemAvailable += ProcessQueueItemAvailable;

  // Attach a handler which cleans up after the input has been processed.
  workflowQueue.QueueItemAvailable += CloseActivity;

  // Indicate to the Workflow runtime that this activity is logically still 
  // executing, even though it will not do anything until input arrives.
  return ActivityExecutionStatus.Executing;
 }

 #endregion // Execute [override]

 #region Event Handlers

 void ProcessQueueItemAvailable( object sender, QueueEventArgs e )
 {
  // The external input has arrived, so wake up and process it.
  WorkflowQueue workflowQueue = this.GetWorkflowQueue( 
    sender as ActivityExecutionContext );

  if( workflowQueue.Count > 0 )
   this.inputText = workflowQueue.Dequeue() as string;
 }

 void CloseActivity( object sender, QueueEventArgs e )
 {
  // The external input has arrived and been processed, so throw away the 
  // WorkflowQueue we used, and tell the WF runtime the activity is finished.
  ActivityExecutionContext executionContext = 
    sender as ActivityExecutionContext;

  WorkflowQueuingService queuingService = 
    executionContext.GetService<WorkflowQueuingService>();

  queuingService.DeleteWorkflowQueue( this.Name );

  executionContext.CloseActivity();
 }

 #endregion // Event Handlers

 #region Private Helpers

 // Helper method which returns a WorkflowQueue.
 WorkflowQueue GetWorkflowQueue( ActivityExecutionContext executionContext )
 {
  WorkflowQueue queue;
  WorkflowQueuingService queuingService = 
    executionContext.GetService<WorkflowQueuingService>();

  if( queuingService.Exists( this.Name ) )
   queue = queuingService.GetWorkflowQueue( this.Name );
  else
   queue = queuingService.CreateWorkflowQueue( this.Name, true );

  return queue;
 }

 #endregion // Private Helpers
}

ReadConsoleLineExecute 方法中,会建立一个“书签”,并且方法会立即将控制权返回给 WF 运行时。但是,它返回‘Executing’,让 WF 运行时知道此时不应继续处理任何其他活动。当用户输入最终到达时,将调用 ReadConsoleLineProcessQueueItemAvailableCloseActivity 事件处理方法。第一个方法将用户姓名存储在一个私有变量中。另一个方法清理并关闭活动,以便 WF 运行时可以继续处理其他活动。

最后一个活动负责向用户打印包含他们姓名的问候语。此活动有一个名为 UserName 的依赖属性。稍后我们将看到,此属性已绑定到 ReadConsoleLine 活动接收到的输入值。数据绑定在这些对象的 XAML 声明中建立。我们很快就会讲到,现在让我们看看 GreetUser 活动。

/// <summary>
/// Prints a greeting to the user.
/// </summary>
public class GreetUser : Activity
{
 public static readonly DependencyProperty UserNameProperty;

 static GreetUser()
 {
  UserNameProperty = DependencyProperty.Register( 
   "UserName", 
   typeof( string ), 
   typeof( GreetUser ) );
 }

 // UserName is a dependency property so that it can be bound to the
 // InputText property of the ReadConsoleLine activity.
 public string UserName
 {
  get { return (string)GetValue( UserNameProperty ); }
  set { SetValue( UserNameProperty, value ); }
 }

 protected override ActivityExecutionStatus Execute(
                                 ActivityExecutionContext executionContext )
 {
  string greeting = String.Format( "Hello, {0}!", this.UserName );
  Console.WriteLine( greeting );

  return ActivityExecutionStatus.Closed;
 }
}

命名空间映射

为了在 XAML 中使用我们的自定义活动,我们需要一种方法让 XAML 解析器知道这些类存在于哪个 CLR 命名空间中。由于 XAML 是一种 XML 语言,我们需要一种将 CLR 命名空间与任意 XML 命名空间(本质上是一个 URI)关联起来的方法。这可以在项目中的任何代码文件中完成,但我出于传统原因创建了一个 AssemblyInfo.cs。这是该文件的内容。

using System.Workflow.ComponentModel.Serialization;

// This attribute makes it possible to use our custom activities in XAML.
[assembly: XmlnsDefinition( "http://FirstWFLibrary", "FirstWFLibrary" )]

工作流声明

现在我们有了执行程序逻辑所需的自定义活动,并且它们的命名空间已映射,我们就可以创建这些类型的实例了。在此演示中,我们将它们在 XAML 中创建。将 XAML 视为一种通用的对象实例化标记语言。它允许您非常轻松地配置对象并表达它们之间的层次关系。

这是最愚蠢的工作流的 XAML 声明

<wf:SequenceActivity 
  xmlns="http://FirstWFLibrary" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow"
  >
  <PromptForUserName />
  <ReadConsoleLine x:Name="getUserName" />
  <GreetUser UserName="{wf:ActivityBind Name=getUserName, Path=InputText}" />
</wf:SequenceActivity>

工作流中的根活动是 SequenceActivity 对象,这是 WF 框架提供的一个类。它是一个 CompositeActivity 派生类,它按照声明顺序执行其子活动,仅在前一个子活动完成后才执行下一个子活动。

根活动包含 XML 命名空间映射。默认 XML 命名空间被映射到文章上一节的 XmlnsDefinition 属性中指定的 URI。这允许我们在没有命名空间前缀的情况下引用我们的自定义活动类型。

另一个有趣的点是 ReadConsoleLineGreetUser 活动之间的关系。后者的 UserName 属性绑定到前者的 InputText 属性。此绑定允许从一个活动将输入到控制台的文本传输到另一个活动。为了使一个属性成为绑定的目标,它必须是一个“依赖属性”。正如我们在“自定义活动”部分所见,创建依赖属性比创建普通属性涉及更多一点代码,但依赖属性可以以普通属性无法做到的方式使用。如果您感兴趣,可以阅读 SDK 中关于这些差异的内容。

工作流运行时宿主

您可以通过多种方式宿主 WF 运行时,但此演示仅使用了一个普通的控制台应用程序。您必须遵循几个步骤才能在您的 AppDomain 中启动和运行 WF 运行时。以下方法是“最愚蠢的 WF 应用程序”宿主 WF 运行时的方法(当然,此方法在一个类中)。

public static void Main()
{
 // Create an instance of WorkflowRuntime, which will execute and coordinate 
 // all of our workflow activities.
 using( WorkflowRuntime workflowRuntime = new WorkflowRuntime() )
 {
  // Tell the Workflow runtime where to find our custom activity types.
  TypeProvider typeProvider = new TypeProvider( workflowRuntime );
  typeProvider.AddAssemblyReference( "FirstWFLibrary.dll" );
  workflowRuntime.AddService( typeProvider );

  // Activate the Workflow runtime.
  workflowRuntime.StartRuntime();

  // Load the XAML file which contains the declaration of our simple workflow
  // and create an instance of it.  Once it is loaded, the workflow is started
  // so that the activities in it will execute.
  WorkflowInstance workflowInstance;
  using( XmlTextReader xmlReader = 
    new XmlTextReader( @"..\..\HelloUserWorkflow.xaml" ) )
  {
   workflowInstance = workflowRuntime.CreateWorkflow( xmlReader );
   workflowInstance.Start();
  }    

  // The ReadConsoleLine activity uses a "bookmark" to indicate that it must
  // wait for external input before it can complete.  In this case, the 
  // external input is the user's name typed into the console window.
  string userName = Console.ReadLine();
  workflowInstance.EnqueueItem( "getUserName", userName, null, null );
  
  // Pause here so that the workflow can display the greeting.
  Console.ReadLine();
      
  // Tear down all of the Workflow services and runtime.
  // (This is probably redundant since the 'runtime' object is in
  // a using block).
  workflowRuntime.StopRuntime();
 }
}

我将逐行解释该方法,因为它已经注释得很好了。我将提到的唯一一个值得关注的点是,一旦 WorkflowInstance 启动,该方法就会调用 Console.ReadLine 来获取用户的姓名。一旦获取姓名,它将被放入“getUserName”工作流队列,该队列由 ReadConsoleLine 活动创建并为其服务。这是将外部输入提供给工作流的一个示例,导致其恢复处理。一旦用户的姓名被放入工作流队列,ReadConsoleLine 活动的 callback 方法将被调用,以便它能够完成执行。

结论

本文展示了如何创建一个使用 Windows Workflow Foundation 的应用程序。希望它让您觉得“最愚蠢的 WF 应用程序”名副其实,但同时也让您理解了 WF 的基本概念以及如何在应用程序中使用它。

正如我之前提到的,这个应用程序根本不需要使用 WF 的强大功能,但它确实传达了相关的基本概念。它展示了 WF 工作流如何是活动树,这些活动如何使用“书签”概念来指示在外部刺激发生后应在哪里继续处理,工作流如何可以通过 XAML 声明,以及如何宿主 WF 工作流运行时。在此过程中,我提到 WF 工作流可以被钝化和恢复,这提供了改进应用程序可伸缩性和性能的强大手段。

© . All rights reserved.