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

WF 4 的动态重新托管工作流设计器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (26投票s)

2012 年 5 月 1 日

CPOL

10分钟阅读

viewsIcon

115172

downloadIcon

11877

本文介绍了一个框架,允许您更轻松地将工作流设计器集成到自己的应用程序中。

(更新于 2012 年 7 月 29 日)

引言

当您使用 Windows Workflow Foundation (WF) 编写应用程序时,您通常希望在您的应用程序中集成自己的工作流设计器。Microsoft 提供了一个名为 WorkflowDesigner 的类来解决这个问题,但仍然有一些任务需要您自己完成

  • 初始化 WorkflowDesigner
  • 加载工具箱(尤其是加载图标很棘手)
  • 加载/保存工作流 XAML(数据库、文件系统...)
  • 处理错误(无效的 xaml 等)

我意识到,这些任务每次的实现方式都基本相同,所以我开始考虑一个通用的基础设施。首先,有一些我希望基础设施满足的指南

  • 通用布局 - 我通过将设计器的每个部分定义为 prism 模块来实现这一点,因此您可以将这些模块放入自己的应用程序中,更改它们的位置...
  • 通用逻辑 - 几乎所有关于逻辑的内容都由一个接口描述,并使用 UnityContainer(依赖注入)进行解析

为了向您展示一些示例,这里有两个使用该框架的实现。

1. 我的标准工作流设计器应用程序

My standard workflow designer application

2. 使用功能区和 Outlook 栏(来自 Odyssey 库)的另一个应用程序

Another test using a ribbon bar and an outlook bar (from Odyssey library)

核心组件

如前所述,我们正在使用 Prism 模块。对于不了解 Prism 的人:我们有一个根视图,其中包含一些内容控件(区域)。在运行时,视图(通常是 UserControls)作为内容附加到它们。因为我提供了这些视图,所以您可以通过使用不同的根视图轻松地更改布局。我们基本上需要

  • 设计器模型的视图
  • 属性网格的视图
  • 工具箱的视图
  • 一个 ViewModel 来托管 WorkflowDesigner 类,我们还必须在这里考虑初始化设计器和错误处理

视图

由于 WorkflowDesigner 已经提供了 UIElements 作为属性,我们可以轻松地将我们的视图绑定到它们,同时使用 ViewModel 作为 DataContext。我决定为每个视图实现一个 UserControl,而不是仅仅将 UIElements 从 WorkflowDesigner 传递给 Prism,因为在没有 WorkflowDesigner 的情况下,我们仍然希望显示某些内容,例如在发生加载错误时。

<UserControl ...
     <ContentControl Content="{Binding CurrentSurface.Designer.View}"/>
</UserControl>  

ViewModel

现在,让我们谈谈 ViewModel。您可能已经注意到绑定路径中的 CurrentSurface.Designer,那么 ViewModel 的内部结构是什么?

DesignerViewModel 由以下接口定义

public interface IDesignerViewModel
{
    void ReloadDesigner(object root);
    
    object CurrentSurface { get; }
    event Action SurfaceChanged;
} 

正如您所见,ViewModel 的实际状态由一个“表面”表示。这可能只是正常的工作流设计器视图,也可能是一个错误显示。由于 WPF 绑定在不起作用时不会抛出异常,因此当发生错误时,我们上面的内容控件将不可见。了解这一点后,我们可以在通常被设计器视图覆盖的内容控件后面放置一个错误 TextBox。

ReloadDesigner 方法通过创建 StandardDesignerSurface(它托管 WorkflowDesigner)来告诉 ViewModel 使用给定的设计器根(ActivityActivityBuilder)重新加载设计器。

对于无效 XAML 定义的情况,我创建了另一个我的 DesignerViewModel 实现的接口

public interface ILoadErrorDesignerViewModel
{
    void ReloadError(string xaml);
} 

调用 ReloadError 方法会告诉 ViewModel 使用 LoadErrorDesignerSurface 作为 CurrentSurface,您可以从中获取无效的 XAML 并显示它或做任何您想做的事情。

存储模块

在讨论了框架的基础知识之后,我们可以继续讨论一些更高级的内容。在存储方面,我们将不得不谈论为每个工作流存储的附加数据:也许您想要一个描述,或者您想输入数据库的密钥或其他内容。将此附加数据包含在我们的设计中,就引出了工作流类型的定义。这意味着存在不同类型的工作流,它们在存储方式、附加的附加数据甚至编辑此类工作流时需要加载的工具箱项方面有所不同。

public interface IWorkflowType
{
    IToolboxCreatorService ToolboxCreatorService { get; }
    IStorageAdapter StorageAdapter { get; }
    string DisplayName { get; }
    string Description { get; }
} 

ToolboxCreatorService 负责加载正确的工具箱项,而 StorageAdapter 负责处理我们之前谈过的所有其他事情。

public interface IStorageAdapter
{
    IEnumerable<IWorkflowDescription> GetAllWorkflowDescriptions();

    bool CanCreateNewDesignerModel { get; }

    IDesignerModel CreateNewDesignerModel();
    IDesignerModel GetDesignerModel(object key);

    bool SaveDesignerModel(IDesignerModel designerModel);
    bool DeleteDesignerModel(object key);
}

出于性能原因,我们不希望在用户只想加载一个已存储的工作流时将整个数据库(或所有数据文件,具体取决于我们使用的存储机制)加载到内存中。用户识别他们想要加载的工作流所需的一切是一个名称,也许还有一个描述,因此我们通过定义一个接口(IWorkflowDescription)将这些内容与其余内容(XAML 定义等)分开。另一个接口(IDesignerModel)包含所有内容,它还负责通过名为 PropertyObject 的属性来保存附加数据。这两个接口通过一个应该用作存储工作流的键的对象相关联。如果您问自己如何通过属性网格编辑附加数据,那当然是:存储模块用一个由 WorkflowDesigner 的标准属性网格和一个绑定到 IDesignerModelPropertyObject 的自定义 PropertyGrid 组成的视图替换了标准属性视图(我在这里获得了自定义属性网格)。

显示一个存储对话框是有意义的,例如可能看起来像这样

您可以在左侧选择工作流类型,在中间选择活动。请注意,在一个应用程序中拥有多种工作流类型的可能性。

XAML 视图

在某些情况下,直接编辑 XAML 比在设计器中编辑工作流更有效。尤其是在 XAML 定义无效的情况下,您别无选择,只能编辑 XAML,因为工作流无法显示。

XAML 视图再次作为 Prism 模块实现。我创建了一个 ViewModel,它响应 IDesignerViewModel 实现的 SurfaceChanged 事件。在事件处理程序中,我们首先检查新表面是 IDesignerSurface 还是 ILoadErrorDesignerSurface

  • 如果它是 ILoadErrorDesignerSurface,我们就获取无效的 XAML 并显示它
  • 如果它是 IDesignerSurface,我们向工作流设计器的 ModelChanged 事件添加一个事件处理程序,从而在模型更改时更新我们的文本

每次用户编辑 XAML 时,ViewModel 都会尝试更新设计器视图并显示可能出现的错误。为了获得更好的性能,我内置了一个计时器来延迟模型更新。由于每次按下按键时都会重新启动此计时器,因此模型仅在用户停止书写时才更改,而不是每次按下按键时更改。

我正在使用 AvalonEdit 文本编辑器来获得一些漂亮的颜色

如何将它们组合在一起

基本想法是将模块集成到另一个应用程序中,但如果您只需要一个工作流设计器,您可以使用我从中截取屏幕截图的示例实现。基本上,所有应用程序所做的就是运行一个非常直接的 PrismBootstrapper,它显示 MainWindow(其中定义了 Prism 区域)。您可以通过在 UnityContainer 中注册自己的 IWorkflowType(使用 App.config)来扩展应用程序。

<types>
        <!--...-->
        <!--this is the example workflow type-->
    <type name="Selen.ActivityWorkflowType"
        type="Selen.WorkflowDesigner.Contracts.IWorkflowType, Selen.WorkflowDesigner.Contracts"
        mapTo="Selen.ActivityWorkflowType.VoidActivityType, Selen.ActivityWorkflowType">
        <lifetime type="singleton"/>
    </type>
        <!--Put your own workflow type here-->
</types>

ActivityBuilder 和 DynamicActivity

现在让我们完成最后一步,看看如何实现这样的工作流类型。由于多种原因,我以 ActivityBuilder 为例

  • Activity 是最动态的工作流类型,因为它什么都可以,甚至可以有参数进出,所以您可能可以在自己的应用程序中使用这种类型
  • 在重托管设计器(如 VisualStudio 设计器)中设计活动是否可行,以及如何设计,一直是一个讨论话题,因为这实际上并非易事。所以我想展示我的方法,这是一种技巧,但效果很好。

作为根和子活动的活动

对于那些可能不知道 ActivityBuilderDynamicActivity 之间区别的人:ActivityBuilder 类可以用作工作流设计器的根。当我们从它生成 XAML 时,我们会得到 "<Activity …" 的形式(就像 Visual Studio 一样)。但当我们想要执行活动或在工作流(或其他活动)中使用它时,我们必须将其转换为 DynamicActivity(有关更多详细信息,请参阅 MSDN)。这一切都很好,直到您尝试保存工作流,因为 DynamicActivity 不可能这样做。必须有一个解决方案,但我无法将 "<Activity …" XAML 编译为真实类型,所以我不得不处理 DynamicActivity。为了解决保存问题,我创建了另一个行为与 DynamicActivity 完全相同但可以保存的类。

PlaceholderActivity

我所谓的 PlaceholderActivity 公开了与 DynamicActivity 完全相同的属性,我只是为每个属性添加了该属性

[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

通过这样做,我告诉 WF Runtime 不要将这些属性序列化到 XAML,因为这会导致异常。将 DynamicActivity 保存到 XAML 的唯一方法是将其转换为 ActivityBuilder,而我正是这样做的。我实现了一个序列化为 XAML 的字符串属性,在该属性的 getter 中,我创建一个新的 ActivityBuilder,然后将 PlaceholderActivity 的所有属性复制到 ActivityBuilderActivityBuilder 具有与 DynamicActivity 相同的属性,因此与 PlaceholderActivity 相同)。之后,我可以序列化 ActivityBuilder 并返回生成的 XAML,其中包含有关我的 PlaceholderActivity 的所有信息。在反序列化工作流时,WF Runtime 会设置 XAML 属性,我可以从中加载 DynamicActivity 并再次复制属性。

public PlaceholderActivity(DynamicActivity dynamicActivity)
    : this()
{
    this.ApplyDynamicActivity(dynamicActivity);
}

public PlaceholderActivity()
    : base()
{
    this.typeDescriptor = new PlaceholderActivityTypeDescriptor(this);
}

private void ApplyDynamicActivity(DynamicActivity dynamicActivity)
{
    this.DisplayName = dynamicActivity.Name;

    foreach (var item in dynamicActivity.Attributes)
    {
        this.Attributes.Add(item);
    }

    foreach (var item in dynamicActivity.Constraints)
    {
        this.Constraints.Add(item);
    }

    this.Implementation = dynamicActivity.Implementation;
    this.Name = dynamicActivity.Name;

    foreach (var item in dynamicActivity.Properties)
    {
        this.Properties.Add(item);
    }
}

[Browsable(false)]
public string XAML
{
    get
    {
        var activityBuilder = new ActivityBuilder();

        foreach (var item in this.Attributes)
        {
            activityBuilder.Attributes.Add(item);
        }

        foreach (var item in this.Constraints)
        {
            activityBuilder.Constraints.Add(item);
        }

        activityBuilder.Implementation = this.Implementation != null ? this.Implementation() : null;
        activityBuilder.Name = this.Name;

        foreach (var item in this.Properties)
        {
            activityBuilder.Properties.Add(item);
        }

        var sb = new StringBuilder();
        var xamlWriter = ActivityXamlServices.CreateBuilderWriter(
                         new XamlXmlWriter(new StringWriter(sb), new XamlSchemaContext()));
        XamlServices.Save(xamlWriter, activityBuilder);

        return sb.ToString();
    }
    set
    {
        this.ApplyDynamicActivity(ActivityXamlServices.Load(new StringReader(value)) as DynamicActivity);
    }
}

数据合同的实现

两个数据合同(IDesignerModelIWorkflowDescription)的实现非常直接

public class WorkflowDescription : IWorkflowDescription
{
    public WorkflowDescription(object key)
    {
        if (key == null) throw new ArgumentNullException("key");
        this.Key = key;
    }

    public string WorkflowName { get; set; }

    public string Description { get; set; }

    public object Key { get; private set; }
} 

public class ActivityEntity
{
    [Category("General")]
    [Description("Activity description")]
    public string Description { get; set; }
}

public class DesignerModel : IDesignerModel
{
    private string originalKey;

    public DesignerModel(object rootActivity, ActivityEntity activityEntity)
    {
        if (activityEntity == null) throw new ArgumentNullException("activityEntity");

        this.RootActivity = rootActivity;
        this.ActivityEntity = activityEntity;
        this.ApplyKey();
    }

    public object Key { get { return this.RootActivity != null ? (this.RootActivity as ActivityBuilder).Name : null; } }

    public bool HasKeyChanged
    {
        get
        {
            return (string)this.Key != originalKey;
        }
    }

    public object RootActivity { get; set; }

    public object PropertyObject { get { return this.ActivityEntity; } }

    public ActivityEntity ActivityEntity { get; private set; }

    public void ApplyKey()
    {
        this.originalKey = (string)this.Key;
    }
} 

请注意,我们可以使用 ActivityBuilder 的名称作为工作流的键,但也可以在附加数据中定义键(在此示例中为 ActivityEntity 类)。我使用附加数据提供了一种编辑工作流描述的方法。我们还必须实现一个属性,该属性表示自上次保存工作流以来键是否已更改(我们知道这一点,因为每次保存工作流时都会调用 ApplyKey)。我的基础设施需要此信息,以防止用户意外覆盖具有相同键的工作流。

IActivityTemplateFactory 模板

因为我们稍后想在工具箱中显示我们的自定义活动,我们需要一个可以传递给 ToolboxItemWrapper 的类型。由于不可能定义参数,我们必须为每个活动创建一个类型。我决定通过 IActivityTemplateFactory 来做到这一点,因为我们没有真正的编译活动类型(只有 DynamicActivity)。所以我为每个活动编译一个程序集,其中只有一个派生自 IActivityTemplateFactory 的类型,并定义了以下私有字段

private readonly string xaml = @"{0}";
private readonly string description = @"{1}";
private readonly string activityName = @"{2}";

占位符代表工作流数据的硬编码值。私有字段通过公共属性暴露,以便 StorageAdapter 可以读取它们。由于我们可以实现 IActivityTemplateFactory 的 Create 方法,因此我们可以让一个 PlaceholderActivity 在用户将工具箱项拖到设计器表面时插入到工作流中。

public Activity Create(DependencyObject target)
{
    DynamicActivity dynamicActivity = ActivityXamlServices.Load(new StringReader(this.xaml)) as DynamicActivity;
    return new PlaceholderActivity(dynamicActivity);
}

StorageAdapter

StorageAdapter 需要做的就是编译包括 IActivityTemplateFactories 在内的程序集并加载它们。如果 StorageAdapter 遇到由无效 XAML 引起的加载错误,则必须抛出 WorkflowLoadException,其中包含无效 XAML 和附加数据

if (workflow == null)
{
    throw new LoadWorkflowException() { XAMLDefinition = instance.XAML, DesignerModel = new DesignerModel(null, new ActivityEntity() { Description = instance.Description }) };
}

在捕获此异常后,我的基础设施会调用 ILoadErrorDesignerViewModel 的 ReloadError 方法,从而在 XAML 视图中显示无效的 XAML 并显示错误文本。

测试主机

如果您想测试您使用我的重托管工作流设计器创建的自定义活动,您可以使用我的 Test Host 控制台应用程序(仅包含在源下载中),它会自动从 designer exe 加载活动 dll。要测试一个活动,请执行以下步骤

  1. 运行一次 TestHost.exe 以创建子文件夹
  2. 在设计器中创建您的活动并保存
  3. 进入 WorkflowDesigner.exe 附近的活动文件夹,并复制您想要测试的活动的 dll
  4. 将其放入 TestHost.exe 附近的活动文件夹
  5. 运行 TestHost.exe,也可以连续执行多个活动

这是 TestHost 执行后的样子

历史

  • 2012 年 7 月 29 日 - 更新了源代码和演示,向文章添加了 Test Host 描述
    UI 增强,一些 bug 修复,重构了源代码,工作流类型现在可以分组并在树视图中显示
© . All rights reserved.