适用于:
Windows Workflow Foundation Beta 2 版
Microsoft Visual C# 2.0 版
Visual Studio 2005
摘要:本文构思了一个自定义复合活动,并表明如何在工作流中使用该活动。我们要讨论的这个活动是产品附带的“并行”活动的扩展,并对验证、编写自定义执行程序组件以及使用设计器类和主题类对用户界面进行样式设计进行了概述。(请注意示例程序文件中的程序员注释使用的是英文,本文中将其译为中文是为了便于参考。)
注意:本文中的内容是使用 Beta 2 版编写的,将来可能需要进行一些更改才能适用于 Windows Workflow Foundation 的更高版本。
下载代码示例: Windows Workflow 示例 - ParallelIf.msi。
本页内容
简介
可视类
行为
自定义活动
现用活动
详细信息
关于作者
简介
在本文中,我将深入探讨针对 Windows Workflow Foundation (WF) 开发自定义活动的详细信息,并提供包括组成自定义活动的所有部分的实现示例。尽管如此,首先还是提供有关活动的一些背景以及用于组成完整活动的类。
为了创建活动,您需要掌握许多类 - 在此示例中,将创建一个称为 ParallelIf 的活动,该活动的运行方式类似于常规的并行活动;但每个分支都有一个条件,即如果为 true,则确保执行该分支。同样,如果条件的评估结果为 false,则将执行子活动。
下面图 1 中所示图像表明了将在本文中使用的类。
我特意将这些类分为左右两侧,左侧的类主要处理对活动进行设计时的情况,右侧的类则处理活动在工作流中使用时的行为。活动是唯一必需的对象 - 如果未明确定义,所有其他对象都将使用默认值。在下面的介绍中,术语“可视”和“行为”是我自己选择使用的。
可视类
有三个主要类用于构成活动的可视方面:ToolboxItem、Designer 和 Theme。
ToolboxItem 类
我将介绍的第一个类是 ToolboxItem 类。您可能会立刻想到该类用于定义对工具箱中图像进行的可视着色,它确实是这样,但它是将定义实际图像的任务委派给我们的老朋友 - ToolboxBitmap 属性(正如您可能知道的,自从 .NET Framework 1.0 以后,就有了该属性)。可以向 ToolboxItem 类添加相当多的内容,但我在这只重点说明要将您的活动构建到工具箱中所需的最低限度内容。
该类的主要作用是定义将您的活动从工具箱拖到工作流设计器中时会发生什么,因此您需要替换返回已初始化组件列表(即,您的活动以及您要添加的任何子活动)的 CreateComponentsCore 方法。下面缩减的代码表明了工具箱项示例以及如何将其挂钩到您的活动。
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Runtime.Serialization; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Design; namespace MNS.Activities { public class ParallelIfToolboxItem :ActivityToolboxItem { protected override IComponent[] CreateComponentsCore(IDesignerHost host) { CompositeActivity activity = new ParallelIfActivity(); activity.Activities.Add(new ParallelIfBranch()); activity.Activities.Add(new ParallelIfBranch()); activity.Activities.Add(new ParallelIfBranch()); return new IComponent[] { activity }; } // 为清晰表述而忽略了其它方法 } [ToolboxItem(typeof(ParallelIfToolboxItem))] [ToolboxBitmap(typeof(ParallelIfActivity), "Resources.ParallelIfActivity.png")] public partial class ParallelIfActivity :CompositeActivity, IActivityEventListener < ActivityExecutionStatusChangedEventArgs > { ... } }
我已在此处定义了 ParallelIfToolboxItem 类并添加了 CreateComponentsCore 方法的实现。该方法用于构造新的父活动 (ParallelIfActivity),然后添加三个 ParallelIfBranch 活动作为新活动的子活动(在本文后面将定义这些活动)。之后活动返回给调用者。因此,当您将该活动放到设计器中时,它将创建活动并在该活动中构造子分支。显然,您还可以在此处为活动属性设置适当的默认值。
然后显示了 ParallelIfActivity 类的第一个部分,定义了将工具箱项挂钩到活动以及图像的资源的属性。ToolboxBitmap 属性有许多替换值,由于我通常沿用 Microsoft 在内部使用的同一方法,因此我使用了这个替换值。在我的项目中,使用了一个 Resources 目录,如图 2 中所示。
我在此处创建了一个图像,并将其定义为嵌入的资源,以便在编译期间它能包含在程序集中。用于工具箱项的图形应该为 16 X 16 像素,颜色深度最高为每像素 256 位。
Designer 类
为了在窗体上绘制活动时更改活动的可视方面,需要添加自定义设计器类,该类通常衍生自 ActivityDesigner 或 CompositeActivityDesigner(如果为复合活动)。设计器通过 [Designer] 属性链接到活动,方式与本文中介绍的其他关联类的所示方式相同。
在设计器中,您可以添加代码以提供完整的自定义绘制功能(如果需要),但在此示例中,将使用自定义主题,该主题允许我更改活动的许多可视方面,而完成此操作毫不费力。
下面的代码表明了设计器类的实现,以及如何通过 [Designer] 属性将其与 ParallelIfActivity 类关联。
using System; using System.Collections.ObjectModel; using System.Workflow.ComponentModel.Design; namespace MNS.Activities { [Designer(typeof(ParallelIfDesigner))] public partial class ParallelIfActivity :CompositeActivity, ... { ... } public class ParallelIfDesigner :ParallelActivityDesigner { public override bool CanInsertActivities(HitTestInfo insertLocation, ReadOnlyCollection<Activity> activitiesToInsert) { return false; } public override bool CanMoveActivities(HitTestInfo moveLocation, ReadOnlyCollection<Activity> activitiesToMove) { return true ; } public override bool CanRemoveActivities(ReadOnlyCollection<Activity> activitiesToRemove) { return true ; } protected override CompositeActivity OnCreateNewBranch() { return new ParallelIfBranch(); } } }
设计器类用于确定在工作流设计器中会发生什么,因此我在此替换了 CanInsertActivities 以返回 false。此方法会在某活动从工具箱拖到 ParallelIfActivity 中时调用,并且我始终在此返回 false,因为只有 ParallelIfBranch 可以作为 ParallelIfActivity 的直接子活动(由于是使用 OnCreateNewBranch() 方法添加 ParallelIfBranch 的,因此它不在工具箱中)。您可以扩展此处理过程,以检查放置何种类型的活动,并可允许某些活动而拒绝其他活动。这完全取决于您。
我从 CanRemoveActivities 和 CanMoveActivities 返回 true,因为在我的设计器中支持这两者。对 ParallelActivityDesigner 的 CanRemoveActivities 方法的默认处理是检查定义了多少子分支,以及允许删除某分支(仅当这可确保至少仍有两个定义的分支时)。我替换了此行为,将其改为如果可用分支少于两个,则验证程序发出错误。
复合活动(例如此活动)需要一些创建新分支的方法,在此分支是 ParallelIfActivity 的直接子活动。当您右键单击活动,然后选择 Add Branch(添加分支)时,即在设计器中使用 OnCreateNewBranch 方法,如图 3 中所示。
此外,您可以在从 OnCreateNewBranch 方法返回之前,再次设置分支的相应属性(如果需要)。上面的图像中也显示了下面定义的主题类的输出。
从验证程序(下文予以介绍)发出的每个错误都可以包含一些自定义数据,此数据包含在哈希表中,并且应该可序列化,因为验证程序和设计器在独立的应用程序域中运行。我使用了此数据来添加字符串值,用于指示定义的分支少于两个。之后在我的设计器中,我替换 OnExecuteDesignerAction 并检查是否存在此额外数据,如果发现存在,则向活动添加分支,直至达到两个。此代码如下所示。
private void OnAddBranch(object sender, EventArgs e) { CompositeActivity activity1 = this.OnCreateNewBranch(); CompositeActivity activity2 = base.Activity as CompositeActivity; if ((activity2 != null) && (activity1 != null)) { int num1 = this.ContainedDesigners.Count; Activity[] activityArray1 = new Activity[] { activity1 }; CompositeActivityDesigner.InsertActivities(this, new ConnectorHitTestInfo(this, HitTestLocations.Designer, activity2.Activities.Count), new List<Activity>(activityArray1).AsReadOnly(), string.Format("正在添加分支 {0}", activity1.GetType().Name)); if ((this.ContainedDesigners.Count > num1) && (this.ContainedDesigners.Count > 0)) { this.ContainedDesigners[this.ContainedDesigners.Count - 1].EnsureVisible(); } } } protected override void OnExecuteDesignerAction(DesignerAction designerAction) { // 检查我的用户数据是否存在... if (designerAction.UserData.Contains("LESS_THAN_TWO")) { CompositeActivity parent = this.Activity as CompositeActivity; if (null != parent) { while ( parent.Activities.Count < 2 ) OnAddBranch(this, EventArgs.Empty); } } else base.OnExecuteDesignerAction(designerAction); }
因此,如果用户从 ParallelIfActivity 删除所有活动,则在用户界面上将显示警告,提示如果单击,将向设计器中添加适当数量的子分支。
Theme 类
Theme 类用于定义活动的可视表示,在此您可以定义属性,例如,在活动中使用的线条的颜色、线端(如箭头等)、用于绘制图像轮廓的笔以及绘制图像时使用的背景刷。
Theme 类有许多其他属性,因此您可以了解一下还可以定义其他什么属性。定义主题很简单,并会使您的活动格外出众 - 尽管我建议不要采用您的用户不喜欢的过于夸张的颜色。
using System; using System.Drawing.Drawing2D; using System.Workflow.ComponentModel.Design; namespace MNS.Activities { [ActivityDesignerTheme( typeof (ParallelIfTheme))] public class ParallelIfDesigner :ParallelActivityDesigner { ... } public class ParallelIfTheme :CompositeDesignerTheme { public ParallelIfTheme(WorkflowTheme theme) : base(theme) { this.ShowDropShadow = true; this.ConnectorStartCap = LineAnchor.None; this.ConnectorEndCap = LineAnchor.None; this.BorderStyle = DashStyle.Dash; } } }
主题的类型主要有两种 - 用于常规活动的主题和用于复合活动的主题。用于着色复合活动的主题是可用于常规活动的主题的超集。ActivityDesignerTheme 基类用于常规活动,而在此使用 CompositeDesignerTheme 是因为我的 ParallelIfActivity 包含子活动。我只定义了主题来设置边框样式、投影以及线端。
主题通过使用 [ActivityDesignerTheme] 属性附加到设计器,如上面的代码中所示。而设计器通过使用 [Designer] 属性附加到活动,如前面的代码段中所示。
行为
既然已完成了活动的可视表示,接下来就是对行为进行处理。在此只有一个类 Validator。
Validator 类
该类主要用于在设计时向用户显示提示,说明活动处于不一致的状态,且通常用于在尚未定义必需属性的情况下显示警告。
在验证程序中,您可能想检查某个特定属性是否为非空,或者验证是否已向活动添加了适当数量的子活动。下面是我在我的代码中使用的示例,在此示例中我检查 ParallelIfActivity 是否至少包含两个分支:
using System; using System.Drawing.Drawing2D; using System.Workflow.ComponentModel.Design; namespace MNS.Activities { [ToolboxItem(typeof(ParallelIfToolboxItem))] [ToolboxBitmap(typeof(ParallelIf),"Resources.ParallelIf.png")] [ActivityValidator(typeof(ParallelIfValidator))] public class ParallelIfActivity :CompositeActivity, ... { } public class ParallelIfValidator :CompositeActivityValidator { public override ValidationErrorCollection Validate(ValidationManager manager, object obj) { if (null == manager) throw new ArgumentNullException("manager"); if (null == obj) throw new ArgumentNullException("obj"); ParallelIfActivity pif = obj as ParallelIfActivity; if (null == pif) throw new ArgumentException("此验证程序只能由 ParallelIfActivity 使用", "obj"); ValidationErrorCollection errors = base.Validate(manager, obj); if ( null != pif.Parent ) { // 现在实际验证该活动... if ( pif.Activities.Count < 2 ) { ValidationError err = new ValidationError ( "ParallelIf 至少需要两个分支", 100, false ); err.UserData.Add("LESS_THAN_TWO", "分支数少于两个"); errors.Add(err); } } return errors; } } }
在我的验证程序代码中,我先检查输入参数,然后调用返回现有验证错误(如果有)集合的基类 Validate 方法。然后检查 ParallelIfActivity 的子节点的数量,如果少于两个,则构造一个 ValidationError 实例并将其添加到返回的错误集合。如果从此方法返回错误,则工作流设计器将在活动旁边显示一个图标,并使用来自 ValidationError 对象的错误字符串,作为向用户显示的内容。
在此示例中,将向验证错误 UserData 集合添加“LESS_THAN_TWO”标记,该标记由设计器拾取并用于为活动创建适当数量的子分支。
创建验证程序时,我总是会建议调用基类 Validate() 方法,因为除了您自己确定的错误以外,基类中可能还有其他应该报告给用户的错误。
请注意,有两个主要的 Validator 类:ActivityValidator 和 CompositeActivityValidator。前者用于所有简单活动,后者用于所有包含子活动的活动。主要区别是用于 CompositeActivityValidator 的基类 Validate() 方法也将验证所有子活动,因此您可能会收到有关父活动的一长串错误列表,这些错误与子活动的所有错误相关。
在执行验证程序的情况下进行编译
当您将验证程序与活动关联后,特别是当您已从 Windows Workflow Beta 1 版升级后,您可能会发现,由于目前在编译期间会执行验证程序,因此无法实际编译代码。这是 Beta 2 版中的新情况,正因如此,我想要介绍如何避免此问题。
例如以下代码,这是来自我最初为 WriteLineActivity(下载代码中附带)编写的验证程序中的代码。
public override ValidationErrorCollection Validate(ValidationManager manager, object obj) { if (null == manager) throw new ArgumentNullException("manager"); if (null == obj) throw new ArgumentNullException("obj"); // 获取错误的基本集 ValidationErrorCollection err = base.Validate(manager, obj); WriteLine wl = obj as WriteLine; if (null == wl) throw new ArgumentException("对象不是 WriteLine 活动", "obj"); if ( null == wl.Message ) err.Add(ValidationError.GetNotSetValidationError("Message")); return err; }
这段代码检查是否已为 WriteLine 类的 Message 属性输入了内容,并在尚未输入任何文本时显示验证错误,如图 4 中所示。
至少可以说这个错误让人感到沮丧 - 我要设法做的就是编译代码而已(甚至未使用 WriteLineActivity),因此很烦恼。这是 Beta 2 版中的新情况,我认为这是在 Windows Workflow 的最终发布版本之前需要更改的地方。要避免此问题,可以采取两种方法。
第一种方法是检查验证程序中是否有要验证的活动的父活动。如果有父活动,则某个工作流中在使用您的自定义活动,因此应该进行验证。但是,如果没有父活动,则您的活动在仅进行编译时不需要进行验证。因此,为了避免这个编译时的问题,只需向验证程序添加一行代码,用于检查是否存在父活动,如果有父活动,才进行验证。
if (null != wl.Parent) { if (string.IsNullOrEmpty(wl.Message)) errors.Add(ValidationError.GetNotSetValidationError("Message")); }
这项检查与一般情况相悖,无疑会使许多开发人员感到意外,这正是我在本文中介绍它的原因。
另一种避免这个编译错误的方法是在常规“类库”项目中创建工作流活动,而不是在“工作流活动库”项目中创建。在这种情况下,编译代码时根本不会运行验证程序,因此您可以不必使用以上代码对父活动进行检查。就我个人而言,喜欢用此方法来避免在编译活动时运行验证程序。
您可能想知道,在代码中为什么将检查定义为 if (null != wl.Parent) 而不是 if ( wl.Parent != null )。它只是一个顽固的老习惯而已 - 我过去曾是 C++ 程序员,这种顺序的表达式较为安全,因为在使用 C++ 时,可能无意中遗漏了“!”字符,这会将空值指定给 wl.Parent。因此,我对赋值采用的所有方式可能看起来偏离正常方式,但它们均源自我多年前养成的好习惯。
自定义活动
ParallelIfActivity
既然已定义了设计器、验证程序和主题,那么实现 ParallelIfActivity 的代码就相当简单了。
[ToolboxBitmap(typeof(ParallelIfActivity), "Resources.ParallelIfActivity.png")] [ToolboxItem(typeof(ParallelIfToolboxItem))] [Designer(typeof(ParallelIfDesigner))] [ActivityValidator(typeof(ParallelIfValidator))] public partial class ParallelIfActivity :CompositeActivity, IActivityEventListener < ActivityExecutionStatusChangedEventArgs > { private static readonly DependencyProperty IsExecutingProperty = DependencyProperty.Register("IsExecuting", typeof(bool), typeof(ParallelIf)); public ParallelIf() { InitializeComponent(); } /// <summary> /// 用于指示活动是否在执行的标志 /// </summary> public bool IsExecuting { get { return (bool)base.GetValue(ParallelIf.IsExecutingProperty); } set { base.SetValue(ParallelIf.IsExecutingProperty, value); } } }
这段代码定义了指向所有已定义的 UI 类和 Behavior 类的链接,并定义了在执行程序中使用的并保存表明当前是否在执行活动的值的布尔标志。
执行活动
运行工作流时,会对活动调用 Execute 方法,其目的是调用每个子活动并执行每个子活动。执行的顺序完全取决于您,但最可能的是您将遍历所有启用的子活动(如果这是一个复合活动),然后按顺序执行每个子活动。
在 ParallelIfActivity 示例中,必须根据与活动的分支关联的条件确定是否执行该分支。如果该条件的评估结果为 true,则执行该分支的活动。
下面的代码显示了如何对每个分支评估条件以验证是否应该执行该分支。
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { if (null == executionContext) throw new ArgumentNullException("executionContext"); // 设置标志以指示我正在执行 this.IsExecuting = true; bool hasStarted = false; for (int child = 0; child < this.EnabledActivities.Count; child++) { ParallelIfBranch branch = this.EnabledActivities[child] as ParallelIfBranch; if (null != branch) { // 评估条件并仅执行返回 true 的分支 if (branch.Condition.Evaluate(branch, executionContext)) { // 现在注册一个回调以在子项关闭时获得通知 branch.RegisterForStatusChange(Activity.ClosedEvent, this); // 然后执行子活动 executionContext.ExecuteActivity(branch); // 用于确定执行方法的结果 hasStarted = true; } } } return hasStarted ?ActivityExecutionStatus.Executing :ActivityExecutionStatus.Closed ; }
在此示例中对输入参数进行验证,然后遍历所有可执行的活动 - 这些活动均定义为尚未在设计器中注释掉的活动。之后对这些活动中的每个活动来评估条件,如果为 true,则将活动加入队列,以使用 executionContext.ExecuteActivity() 方法调用来执行。另外,还设置了一个标志,表明至少有一个活动在执行,该标志用在方法结束后以返回适当的状态代码。如果有子活动在执行,则返回 ActivityExecutionStatus.Executing,否则返回 ActivityExecutionStatus.Closed。
因此工作流运行时需要通过某种方式知道并行活动已完成 - 这通过添加子活动完成时出现的事件处理程序来实现,即添加对 RegisterForStatusChange 的调用,如代码中所示。然后,可以验证所有子活动是否都已完成,如果是,则调用 ActivityExecutionContext.CloseActivity(),如下面的代码中所示。
void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent( object sender, ActivityExecutionStatusChangedEventArgs e) { if (null == sender) throw new ArgumentNullException("sender"); if (null == e) throw new ArgumentException("e"); ActivityExecutionContext context = sender as ActivityExecutionContext; if (null == context) throw new ArgumentException("发送方必须为 ActivityExecutionContext", "sender"); // 检查进行调用时的状态... if (e.ExecutionStatus == ActivityExecutionStatus.Closed) { // 我们已为此活动中的已关闭事件完成侦听 e.Activity.UnregisterForStatusChange(Activity.ClosedEvent, this); // 现在检查是否所有子活动均已关闭... ParallelIfActivity pif = context.Activity as ParallelIfActivity; bool finished = true; for (int branch = 0; branch < pif.EnabledActivities.Count; branch++) { Activity child = pif.EnabledActivities[branch]; if ((child.ExecutionStatus != ActivityExecutionStatus.Initialized) && (child.ExecutionStatus != ActivityExecutionStatus.Closed)) finished = false; } // 所有子项均已完成 - 就此结束 if (finished) context.CloseActivity(); } }
这段代码只是遍历了每个子活动,并检查是否存在既非“已初始化”又非“已关闭”的活动 - 在这种情况下,仍有子活动在执行,因此无法关闭父活动。
上面介绍的状态更改代码可能因使用了泛型而看起来有些奇怪,但是从概念上讲,它非常简单。每个活动均有一组在其生存期间出现的事件,如 ExecutingEvent、CancelingEvent、ClosedEvent 等。这些事件都定义为 Activity 类的相关属性。
在您调用 activity.RegisterForStatusChange () 方法时,将委派传递给在活动状态更改时调用的特定函数。您的委派可能需要处理多个状态更改,如果这样,可以通过检查传递的事件参数为“e”的 ExecutionStatus 属性来查找活动的当前状态。
在内部,对于每个可能出现的事件,都保持着一个委派列表,且在状态更改时会遍历此列表并调用每个委派。
除了上面介绍的 Execute 方法和 OnEvent 方法以外,我还提供了 Cancel 方法和 OnActivityChangeAdd 方法的实现。在 Cancel 方法中,需要取消所有执行的分支,其中涉及遍历所有执行的子活动,并对这些子活动调用 CancelActivity 方法。
例如,如果您的代码向 ParallelIfActivity 动态添加节点,则可能会在运行时调用 OnActivityChangeAdd 方法。在这种情况下,我需要为新添加的活动注册状态更改处理程序,然后执行该活动。这些方法的代码可通过下载获得。
ParallelIfBranch 活动
这是 ParallelfActivity 的子活动,包含由执行程序评估以确定是否应该执行分支的 Condition 属性。此活动的完整代码如下所示。
[ToolboxItem(false)] [Designer(typeof(ParallelIfBranchDesigner))] [ActivityValidator(typeof(ParallelIfBranchValidator))] public class ParallelIfBranch :SequenceActivity { /// <summary> /// 将条件属性定义为 DependencyProperty /// </summary> public static readonly DependencyProperty ConditionProperty = DependencyProperty.Register("Condition", typeof(Condition), typeof(ParallelIfBranch), new PropertyMetadata(DependencyPropertyOptions.Metadata)); /// <summary> /// 获取/设置条件 /// </summary> /// <remarks> /// 运行时评估条件,如果为 true 则将执行子活动 /// </remarks> public Condition Condition { get { return base.GetValue(ConditionProperty) as Condition; } set { base.SetValue(ConditionProperty, value); } } }
除了此活动以外,还有一个验证程序和一个设计器:验证程序用来确保已为 Condition 属性定义了值;设计器只是替换 CanBeParentedTo 方法来确保可以将 ParallelIfBranch 仅作为 ParallelIfActivity 的直接子活动来添加。
现用活动
由于本文的其中一点是向可用活动库提供新活动,因此本部分将介绍如何能够在工作流中使用 ParallelIfActivity。在下载代码中,已包含了可以用于将此活动添加到工具箱的完整实现。这样,您可以将 ParallelIfActivity 拖到设计器中,并将得到与下面的图 5 中所示类似的结果。
默认情况下,会定义 ToolboxItem 类以向 ParallelIfActivity 添加三个分支。由于尚未定义活动的 Condition 属性,因此这些会导致显示错误。
按顺序选择每个分支并定义条件 - 在此您可以使用代码条件或规则条件。为每个分支定义了条件后,应该能够执行工作流。我已在下载代码中包含了可以用于将数据输出到控制台的 WriteLineActivity,这在检查已实际执行了 ParallelIf 的哪些分支时很有用。
另一种将数据输出到控制台的方法是,在活动执行时使用 Windows Workflow 的内置跟踪服务来记录额外数据。为了执行此操作,可以调用 Activity 类的 TrackData () 方法来在跟踪存储中记录任何想要的自定义数据。
详细信息
有许多可以用来获取有关 Windows Workflow Foundation 的详细信息的站点。有关详细信息,请访问 http://msdn.microsoft.com/workflow 和 http://www.windowsworkflow.net。
另外,还有一本有关 Windows Workflow 的很有帮助的介绍性书籍,该书由 Microsoft 小组的几个主要成员编写而成,可在线获取。有关详细信息,请参阅 Presenting Windows Workflow Foundation(英文)。