WPF桌面应用集成Elsa工作流引擎:实现业务流程动态驱动与可视化设计
在实际企业级应用开发中,业务逻辑的流转往往比单一功能的实现更为复杂。当业务流程需要根据审批状态、数据条件或用户角色动态调整时,硬编码的if-else分支会迅速变得臃肿且难以维护。此时,引入一个可视化、可配置、可持久化的工作流引擎就成为架构演进的必然选择。Elsa Workflows 是一个基于 .NET 构建的现代化、开源工作流库,它允许开发者以代码或可视化的方式定义复杂的工作流,并轻松集成到各类应用中。本文将聚焦于如何在一个经典的 WPF 桌面应用程序中,集成 Elsa 工作流框架,实现业务流程的动态驱动与可视化设计。
我们将从一个最小化的 WPF 项目开始,逐步引入 Elsa 的核心包,配置工作流运行时,创建一个简单但完整的工作流,并在 WPF 界面中触发和执行它。整个过程会涉及 WPF 的 MVVM 模式、依赖注入、以及 Elsa 的活动定义、工作流定义和触发器等核心概念。通过本文,你将掌握在桌面端应用中使用工作流引擎解决业务编排问题的基本路径。
1. 理解 Elsa Workflows 的核心概念与集成价值
在开始编码之前,有必要厘清几个关键概念,这能帮助你理解我们即将构建的系统是如何工作的,以及为什么选择 Elsa。
1.1 工作流、活动与运行时的关系
工作流(Workflow)是由一系列活动(Activity)按照特定逻辑(顺序、分支、循环等)连接而成的有向图,它描述了一个完整的业务流程。活动是工作流中的基本执行单元,例如“发送邮件”、“审批节点”、“调用 HTTP API”、“执行 C# 脚本”等。Elsa 提供了大量内置活动,也支持自定义活动。
工作流运行时(Workflow Runtime)是 Elsa 的核心引擎,它负责加载工作流定义,解释其结构,调度活动的执行,并管理工作流实例的状态(如暂停、恢复、完成)。在 WPF 应用中集成 Elsa,本质上就是将这个运行时嵌入到我们的桌面程序中。
1.2 为何在 WPF 桌面应用中使用工作流引擎?
你可能会问,WPF 应用通常是单机或 C/S 架构,为何需要工作流?考虑以下场景:
- 工业控制流程:一个生产线监控软件,需要根据传感器数据(温度、压力)触发不同的控制指令序列。流程可能经常由工艺人员调整。
- 数据批处理向导:一个本地数据处理工具,包含“选择文件”、“验证数据”、“转换格式”、“导出结果”等多个步骤,步骤间的跳转逻辑复杂。
- 动态表单审批:一个内部办公系统,请假、报销等流程的审批节点和规则可能需要由管理员动态配置。
在这些场景下,使用 Elsa 可以将易变的业务流程从硬编码中解耦出来。业务专家甚至可以通过我们后续集成的设计器界面(Elsa Studio)来绘制和修改流程,而无需开发者重新编译和发布客户端。
1.3 Elsa 与 WPF 的集成模式
Elsa 本身是服务端导向的,但其核心库不依赖 ASP.NET Core,可以运行在任何 .NET 环境中,包括 WPF。我们的集成思路是:
- 将 Elsa 的工作流运行时(
IWorkflowRuntime)和活动注册表(IActivityRegistry)等核心服务,通过依赖注入容器(如 .NET 内置的IServiceCollection)进行配置和管理。 - 在 WPF 的
App.xaml.cs或程序启动入口处,构建这个服务容器,并从中获取所需的服务实例。 - WPF 的 ViewModel 或后台代码通过服务容器获取工作流运行时,从而触发或查询工作流。
2. 环境准备与项目初始化
我们将创建一个新的 WPF 项目,并添加必要的 NuGet 包。
2.1 创建 WPF 项目并配置依赖
首先,使用 Visual Studio 2022 或更高版本创建一个新的 WPF 应用项目,目标框架选择 .NET 6.0 或 .NET 8.0(Elsa 3.x 支持)。项目命名为WpfElsaWorkflowDemo。
然后,通过 NuGet 包管理器或dotnet add package命令,为项目添加以下核心包:
<!-- 项目文件 (.csproj) 中的 PackageReference 示例 --> <ItemGroup> <PackageReference Include="Elsa.Core" Version="3.2.0" /> <PackageReference Include="Elsa.Activities.Http" Version="3.2.0" /> <PackageReference Include="Elsa.Activities.ControlFlow" Version="3.2.0" /> <PackageReference Include="Elsa.Persistence.YesSql" Version="3.2.0" /> <PackageReference Include="YesSql.Provider.Sqlite" Version="4.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> </ItemGroup>包作用说明:
Elsa.Core: Elsa 工作流的核心运行时库。Elsa.Activities.Http: 提供 HTTP 相关活动(如发送请求),常用于与外部服务交互,即使在本例中也可能用到。Elsa.Activities.ControlFlow: 提供If,Switch,While,Fork等控制流活动,用于构建复杂逻辑。Elsa.Persistence.YesSql与YesSql.Provider.Sqlite: 用于将工作流定义和实例持久化到 SQLite 数据库。对于桌面应用,SQLite 是轻量级且方便的首选。Microsoft.Extensions.DependencyInjection与Microsoft.Extensions.Hosting: .NET 通用的依赖注入和托管扩展库,Elsa 重度依赖此模式。
2.2 配置服务容器与 Elsa
WPF 没有像 ASP.NET Core 那样的Startup类,我们需要在App.xaml.cs中初始化我们的服务提供者。
首先,修改App.xaml,移除StartupUri,以便我们在App类中手动控制启动逻辑:
<!-- App.xaml --> <Application x:Class="WpfElsaWorkflowDemo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Application.Resources> </Application.Resources> </Application>然后,在App.xaml.cs中,我们构建一个ServiceProvider:
using Elsa; using Elsa.Persistence.YesSql; using Elsa.Persistence.YesSql.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Windows; using YesSql.Provider.Sqlite; namespace WpfElsaWorkflowDemo { public partial class App : Application { private readonly IHost _host; public App() { _host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { // 1. 配置 Elsa services.AddElsa(elsa => elsa .AddYesSqlPersistence(config => config .UseSqLite(@"Data Source=elsa.db;Cache=Shared") // 使用 SQLite 数据库 ) .AddConsoleActivities() // 添加控制台活动(如WriteLine) .AddHttpActivities() // 添加HTTP活动 .AddControlFlowActivities() // 添加控制流活动 .AddWorkflow<HelloWorldWorkflow>() // 注册我们即将创建的工作流 ); // 2. 注册 WPF 相关的服务,如主窗口 services.AddSingleton<MainWindow>(); }) .Build(); } // 公开 ServiceProvider,以便在 ViewModel 或其他地方获取服务 public IServiceProvider Services => _host.Services; protected override async void OnStartup(StartupEventArgs e) { await _host.StartAsync(); // 启动 Host // 从容器中获取并显示主窗口 var mainWindow = Services.GetRequiredService<MainWindow>(); mainWindow.Show(); base.OnStartup(e); } protected override async void OnExit(ExitEventArgs e) { using (_host) { await _host.StopAsync(TimeSpan.FromSeconds(5)); } base.OnExit(e); } } }关键配置解释:
AddYesSqlPersistence: 配置 Elsa 使用 YesSql 作为持久化存储,并指定连接 SQLite 数据库文件elsa.db。所有工作流定义和运行实例都将存储于此。AddConsoleActivities,AddHttpActivities,AddControlFlowActivities: 注册不同类型的活动集,这样我们在设计工作流时才能使用对应的活动。AddWorkflow<HelloWorldWorkflow>: 注册一个具体的工作流定义类。这是我们用代码定义工作流的方式之一。- 使用
IHost来管理服务生命周期,这是一个在 .NET 中管理后台任务、配置和 DI 的推荐模式。
3. 定义第一个工作流:Hello World
现在,我们来创建一个简单的工作流。这个工作流由一个“开始”事件触发,然后执行一个“写入行”活动,在控制台输出 “Hello from Elsa Workflow!”。
在项目中创建一个新类HelloWorldWorkflow.cs:
using Elsa.Activities.Console; using Elsa.Activities.ControlFlow; using Elsa.Builders; namespace WpfElsaWorkflowDemo.Workflows { // 通过实现 IWorkflow 接口来定义工作流 public class HelloWorldWorkflow : IWorkflow { public void Build(IWorkflowBuilder builder) { builder .StartWith<WriteLine>(activity => activity.Set(x => x.Text, "Hello from Elsa Workflow!")) .Then<Finish>(); } } }代码详解:
IWorkflow接口要求实现一个Build方法,该方法接收一个IWorkflowBuilder。builder.StartWith<TActivity>指定工作流的第一个活动。这里使用WriteLine活动(来自Elsa.Activities.Console)。Set方法用于设置活动的属性。我们将WriteLine活动的Text属性设置为我们的问候语。.Then<Finish>()表示工作流在执行完WriteLine后,进入Finish活动,优雅地结束工作流实例。
这是一个完全用代码定义的“编程式”工作流。Elsa 也支持从数据库加载由设计器创建的“动态”工作流定义。
4. 在 WPF 界面中触发工作流
接下来,我们需要在 WPF 的主界面中添加一个按钮,点击时触发上面定义的工作流。
4.1 创建 ViewModel 并注入服务
我们采用简单的 MVVM 模式。首先创建一个MainViewModel.cs:
using Elsa.Services; using System; using System.Threading.Tasks; using System.Windows.Input; using Microsoft.Toolkit.Mvvm.Input; // 或 CommunityToolkit.Mvvm.Input using Microsoft.Extensions.DependencyInjection; namespace WpfElsaWorkflowDemo.ViewModels { public class MainViewModel { private readonly IServiceProvider _serviceProvider; public ICommand RunWorkflowCommand { get; } public MainViewModel(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; RunWorkflowCommand = new RelayCommand(async () => await RunWorkflowAsync()); } private async Task RunWorkflowAsync() { // 注意:在WPF中,通常需要将异步操作同步到UI线程,这里为简化示例,暂不处理。 // 实际项目中应考虑使用 ICommand 的异步版本或 Dispatcher。 // 从服务提供者获取工作流启动器 var workflowStarter = _serviceProvider.GetRequiredService<IStartsWorkflow>(); // 启动我们定义的 HelloWorldWorkflow // 需要提供工作流定义ID或类型。这里我们通过类型启动。 await workflowStarter.StartWorkflowAsync<HelloWorldWorkflow>(); } } }注意:这里使用了Microsoft.Toolkit.Mvvm的RelayCommand。你需要通过 NuGet 安装CommunityToolkit.Mvvm包。或者,你也可以使用 Prism、MVVMLight 等其他框架,或自己实现ICommand。
4.2 修改 MainWindow 以使用 ViewModel 和数据绑定
修改MainWindow.xaml,添加一个按钮并绑定命令:
<Window x:Class="WpfElsaWorkflowDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfElsaWorkflowDemo" mc:Ignorable="d" Title="WPF Elsa Workflow Demo" Height="350" Width="525"> <Grid> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <TextBlock Text="Elsa Workflow in WPF" FontSize="24" Margin="10"/> <Button Content="Run Hello World Workflow" Command="{Binding RunWorkflowCommand}" Padding="20,10" FontSize="14" Margin="10"/> <TextBlock x:Name="StatusText" Text="Ready." Margin="10" HorizontalAlignment="Center"/> </StackPanel> </Grid> </Window>修改MainWindow.xaml.cs,设置其 DataContext:
using System.Windows; using WpfElsaWorkflowDemo.ViewModels; namespace WpfElsaWorkflowDemo { public partial class MainWindow : Window { public MainWindow(MainViewModel viewModel) { InitializeComponent(); DataContext = viewModel; // 设置 ViewModel 为数据上下文 } } }最后,我们需要在App.xaml.cs的ConfigureServices中注册MainViewModel,以便依赖注入容器能自动解析它:
// 在 App.xaml.cs 的 ConfigureServices 方法内添加 services.AddTransient<MainViewModel>();5. 运行验证与结果分析
现在,所有部分都已就绪。按 F5 运行应用程序。
- 首次运行:程序启动后,会在项目输出目录(如
bin\Debug\net8.0)下创建一个elsa.db文件。这是 Elsa 用于存储工作流定义和实例的 SQLite 数据库。 - 界面操作:点击窗口中的 “Run Hello World Workflow” 按钮。
- 观察结果:你应该能在 Visual Studio 的“输出”窗口(选择“显示输出来源:调试”)中看到一行文本:
Hello from Elsa Workflow!。
验证成功的关键点:
- 按钮点击后没有抛出异常。
- “输出”窗口显示了预期的文本。
- 同时,你可以使用 SQLite 工具(如 DB Browser for SQLite)打开
elsa.db,查看WorkflowDefinition和WorkflowInstance表,里面应该已经存入了我们定义的工作流和本次执行的记录。这证明了持久化是生效的。
6. 常见问题排查
在实际集成过程中,你可能会遇到以下问题。这里提供排查思路。
6.1 按钮点击无反应,控制台无输出
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
点击按钮无任何反应,VS输出窗口也没有Hello from Elsa Workflow!。 | 1. 命令绑定失败。 2. 依赖注入未正确设置, IStartsWorkflow服务解析失败。3. 工作流定义未注册。 | 1. 检查按钮的Command属性绑定名称是否与 ViewModel 中的属性名一致。2. 在 RunWorkflowAsync方法开始处设置断点,看是否被调用。3. 在 App构造函数中Build()服务容器后,尝试手动Services.GetService<IStartsWorkflow>()看是否返回null。 | 1. 确认 ViewModel 已正确设置为 Window 的DataContext。2. 确认 App.xaml.cs中已调用AddElsa并注册了工作流AddWorkflow<HelloWorldWorkflow>()。3. 确认所有必要的 Elsa NuGet 包已安装,版本兼容。 |
6.2 出现数据库相关异常
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
程序启动时抛出SqliteException,如 “SQLite Error 1: ‘no such table: …’”。 | 1. SQLite 数据库文件路径不可写。 2. YesSql 初始化失败,表未创建。 | 1. 检查elsa.db文件是否在输出目录生成。2. 检查连接字符串 Data Source=elsa.db;Cache=Shared。3. 查看完整的异常堆栈信息。 | 1. 确保应用程序对输出目录有写入权限。 2. 尝试使用绝对路径,如 Data Source=C:\temp\elsa.db;。3. 删除已存在的 elsa.db文件,让 Elsa 在下次启动时重新创建。 |
6.3 工作流执行了,但输出不在预期位置
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 工作流似乎执行了(数据库中有记录),但没在 VS 输出窗口看到文字。 | WriteLine活动默认输出到System.Console,在 WPF 应用中可能被重定向或不可见。 | 1. 在WriteLine活动后添加一个自定义活动,将文本写入 WPF 的TextBox。2. 使用调试器查看 WriteLine活动是否真的执行。 | 1. 对于桌面应用,更常见的做法是将工作流执行结果(如输出变量)返回给调用者,再由 ViewModel 更新 UI。 2. 可以创建自定义的 WriteToUiActivity活动,通过事件或回调机制与 UI 线程通信。 |
7. 进阶实践与扩展方向
成功运行基础示例后,你可以从以下几个方向深化实践:
7.1 向工作流传递输入与获取输出
实际业务中,需要向工作流传递参数(如审批单ID),并获取执行结果。Elsa 通过Input和Output属性实现。
修改工作流定义:
public class GreetingWorkflow : IWorkflow { public void Build(IWorkflowBuilder builder) { builder .StartWith<SetVariable>(activity => activity .Set(x => x.VariableName, "Greeting") .Set(x => x.Value, context => $"Hello, {context.Input!}!") ) .Then<WriteLine>(activity => activity .Set(x => x.Text, context => context.GetVariable<string>("Greeting")) ); } }在 ViewModel 中触发并传递输入:
private async Task RunWorkflowWithInputAsync() { var workflowStarter = _serviceProvider.GetRequiredService<IStartsWorkflow>(); var input = "World"; // 从UI获取输入 await workflowStarter.StartWorkflowAsync<GreetingWorkflow>(input: input); }7.2 集成 Elsa Studio 进行可视化设计
对于桌面应用,可以嵌入 Elsa Studio(一个 Blazor 组件)来提供可视化工作流设计界面。这需要:
- 添加
Elsa.Designer.Components.Web和Elsa.Server.Api包。 - 在 WPF 应用中托管一个简单的 ASP.NET Core Kestrel 服务器来提供 Studio 的 Blazor 页面和 API。
- 使用
WebView2控件在 WPF 窗口中加载本地运行的 Studio URL。
此方案较为复杂,但对于需要最终用户自定义流程的场景价值巨大。
7.3 创建自定义活动
当内置活动不满足需求时,可以创建自定义活动。例如,创建一个更新 WPF UI 状态的活动:
[Activity(Category = "UI", Description = "Updates a status text in the WPF UI.")] public class UpdateStatusActivity : Activity { // 定义一个输入属性,用于接收状态文本 [ActivityInput] public string StatusText { get; set; } = "Done"; protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { // 这里需要一种方式将状态传递回UI线程。 // 一种方法是使用事件聚合器(如 Prism.EventAggregator) // 或通过依赖注入一个共享的 UI 状态服务。 var uiService = context.GetService<IUiStatusService>(); uiService?.UpdateStatus(StatusText); await CompleteAsync(); } }然后,你需要在 DI 容器中注册这个活动(services.AddActivity<UpdateStatusActivity>())并在工作流中使用它。
7.4 工作流的持久化与恢复
对于长时间运行的工作流(如审批流程),Elsa 可以自动将工作流实例挂起并持久化到数据库。当事件(如用户点击批准)触发时,可以从数据库恢复实例并继续执行。这需要结合IWorkflowRuntime的TriggerWorkflowAsync和书签(Bookmark)机制,是 Elsa 的高级特性。
在 WPF 桌面应用中集成 Elsa 工作流框架,核心在于理解其服务模型并将其适配到桌面应用的启动和生命周期管理中。从简单的代码定义工作流开始,逐步扩展到可视化设计、自定义活动、复杂输入输出和持久化恢复,可以构建出极其灵活和强大的业务流程驱动型桌面应用程序。关键在于将工作流引擎视为一个独立的业务逻辑执行内核,而 WPF 界面则作为其触发器和状态显示器。