WPF桌面应用集成Elsa工作流引擎:实现业务流程动态驱动与可视化设计

发布时间:2026/8/2 9:43:23

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.0Elsa 3.x 支持。项目命名为WpfElsaWorkflowDemo。然后通过 NuGet 包管理器或dotnet add package命令为项目添加以下核心包!-- 项目文件 (.csproj) 中的 PackageReference 示例 -- ItemGroup PackageReference IncludeElsa.Core Version3.2.0 / PackageReference IncludeElsa.Activities.Http Version3.2.0 / PackageReference IncludeElsa.Activities.ControlFlow Version3.2.0 / PackageReference IncludeElsa.Persistence.YesSql Version3.2.0 / PackageReference IncludeYesSql.Provider.Sqlite Version4.0.0 / PackageReference IncludeMicrosoft.Extensions.DependencyInjection Version8.0.0 / PackageReference IncludeMicrosoft.Extensions.Hosting Version8.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 配置服务容器与 ElsaWPF 没有像 ASP.NET Core 那样的Startup类我们需要在App.xaml.cs中初始化我们的服务提供者。首先修改App.xaml移除StartupUri以便我们在App类中手动控制启动逻辑!-- App.xaml -- Application x:ClassWpfElsaWorkflowDemo.App xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml Application.Resources /Application.Resources /Application然后在App.xaml.cs中我们构建一个ServiceProviderusing 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 Sourceelsa.db;CacheShared) // 使用 SQLite 数据库 ) .AddConsoleActivities() // 添加控制台活动如WriteLine .AddHttpActivities() // 添加HTTP活动 .AddControlFlowActivities() // 添加控制流活动 .AddWorkflowHelloWorldWorkflow() // 注册我们即将创建的工作流 ); // 2. 注册 WPF 相关的服务如主窗口 services.AddSingletonMainWindow(); }) .Build(); } // 公开 ServiceProvider以便在 ViewModel 或其他地方获取服务 public IServiceProvider Services _host.Services; protected override async void OnStartup(StartupEventArgs e) { await _host.StartAsync(); // 启动 Host // 从容器中获取并显示主窗口 var mainWindow Services.GetRequiredServiceMainWindow(); 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: 注册不同类型的活动集这样我们在设计工作流时才能使用对应的活动。AddWorkflowHelloWorldWorkflow: 注册一个具体的工作流定义类。这是我们用代码定义工作流的方式之一。使用IHost来管理服务生命周期这是一个在 .NET 中管理后台任务、配置和 DI 的推荐模式。3. 定义第一个工作流Hello World现在我们来创建一个简单的工作流。这个工作流由一个“开始”事件触发然后执行一个“写入行”活动在控制台输出 “Hello from Elsa Workflow!”。在项目中创建一个新类HelloWorldWorkflow.csusing Elsa.Activities.Console; using Elsa.Activities.ControlFlow; using Elsa.Builders; namespace WpfElsaWorkflowDemo.Workflows { // 通过实现 IWorkflow 接口来定义工作流 public class HelloWorldWorkflow : IWorkflow { public void Build(IWorkflowBuilder builder) { builder .StartWithWriteLine(activity activity.Set(x x.Text, Hello from Elsa Workflow!)) .ThenFinish(); } } }代码详解IWorkflow接口要求实现一个Build方法该方法接收一个IWorkflowBuilder。builder.StartWithTActivity指定工作流的第一个活动。这里使用WriteLine活动来自Elsa.Activities.Console。Set方法用于设置活动的属性。我们将WriteLine活动的Text属性设置为我们的问候语。.ThenFinish()表示工作流在执行完WriteLine后进入Finish活动优雅地结束工作流实例。这是一个完全用代码定义的“编程式”工作流。Elsa 也支持从数据库加载由设计器创建的“动态”工作流定义。4. 在 WPF 界面中触发工作流接下来我们需要在 WPF 的主界面中添加一个按钮点击时触发上面定义的工作流。4.1 创建 ViewModel 并注入服务我们采用简单的 MVVM 模式。首先创建一个MainViewModel.csusing 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.GetRequiredServiceIStartsWorkflow(); // 启动我们定义的 HelloWorldWorkflow // 需要提供工作流定义ID或类型。这里我们通过类型启动。 await workflowStarter.StartWorkflowAsyncHelloWorldWorkflow(); } } }注意这里使用了Microsoft.Toolkit.Mvvm的RelayCommand。你需要通过 NuGet 安装CommunityToolkit.Mvvm包。或者你也可以使用 Prism、MVVMLight 等其他框架或自己实现ICommand。4.2 修改 MainWindow 以使用 ViewModel 和数据绑定修改MainWindow.xaml添加一个按钮并绑定命令Window x:ClassWpfElsaWorkflowDemo.MainWindow xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:dhttp://schemas.microsoft.com/expression/blend/2008 xmlns:mchttp://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:localclr-namespace:WpfElsaWorkflowDemo mc:Ignorabled TitleWPF Elsa Workflow Demo Height350 Width525 Grid StackPanel VerticalAlignmentCenter HorizontalAlignmentCenter TextBlock TextElsa Workflow in WPF FontSize24 Margin10/ Button ContentRun Hello World Workflow Command{Binding RunWorkflowCommand} Padding20,10 FontSize14 Margin10/ TextBlock x:NameStatusText TextReady. Margin10 HorizontalAlignmentCenter/ /StackPanel /Grid /Window修改MainWindow.xaml.cs设置其 DataContextusing 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.AddTransientMainViewModel();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.GetServiceIStartsWorkflow()看是否返回null。1. 确认 ViewModel 已正确设置为 Window 的DataContext。2. 确认App.xaml.cs中已调用AddElsa并注册了工作流AddWorkflowHelloWorldWorkflow()。3. 确认所有必要的 Elsa NuGet 包已安装版本兼容。6.2 出现数据库相关异常问题现象可能原因检查方式处理建议程序启动时抛出SqliteException如 “SQLite Error 1: ‘no such table: …’”。1. SQLite 数据库文件路径不可写。2. YesSql 初始化失败表未创建。1. 检查elsa.db文件是否在输出目录生成。2. 检查连接字符串Data Sourceelsa.db;CacheShared。3. 查看完整的异常堆栈信息。1. 确保应用程序对输出目录有写入权限。2. 尝试使用绝对路径如Data SourceC:\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 .StartWithSetVariable(activity activity .Set(x x.VariableName, Greeting) .Set(x x.Value, context $Hello, {context.Input!}!) ) .ThenWriteLine(activity activity .Set(x x.Text, context context.GetVariablestring(Greeting)) ); } }在 ViewModel 中触发并传递输入private async Task RunWorkflowWithInputAsync() { var workflowStarter _serviceProvider.GetRequiredServiceIStartsWorkflow(); var input World; // 从UI获取输入 await workflowStarter.StartWorkflowAsyncGreetingWorkflow(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.GetServiceIUiStatusService(); uiService?.UpdateStatus(StatusText); await CompleteAsync(); } }然后你需要在 DI 容器中注册这个活动services.AddActivityUpdateStatusActivity()并在工作流中使用它。7.4 工作流的持久化与恢复对于长时间运行的工作流如审批流程Elsa 可以自动将工作流实例挂起并持久化到数据库。当事件如用户点击批准触发时可以从数据库恢复实例并继续执行。这需要结合IWorkflowRuntime的TriggerWorkflowAsync和书签Bookmark机制是 Elsa 的高级特性。在 WPF 桌面应用中集成 Elsa 工作流框架核心在于理解其服务模型并将其适配到桌面应用的启动和生命周期管理中。从简单的代码定义工作流开始逐步扩展到可视化设计、自定义活动、复杂输入输出和持久化恢复可以构建出极其灵活和强大的业务流程驱动型桌面应用程序。关键在于将工作流引擎视为一个独立的业务逻辑执行内核而 WPF 界面则作为其触发器和状态显示器。

相关新闻