{"id":17781678,"url":"https://github.com/fhhyyp/serein-flow","last_synced_at":"2025-03-15T23:30:59.224Z","repository":{"id":251591011,"uuid":"837859053","full_name":"fhhyyp/serein-flow","owner":"fhhyyp","description":"基于WPF（.NET 8）的动态节点流可视化编辑器，支持导入C# DLL生成自定义节点，提供二次开发支持，适合用于可视化编程和流程设计。  A dynamic node flow visual editor based on WPF (.NET 8), supporting C# DLL import to create custom nodes, with secondary development support, ideal for visual programming and flow design.","archived":false,"fork":false,"pushed_at":"2025-03-14T08:04:10.000Z","size":36963,"stargazers_count":49,"open_issues_count":0,"forks_count":8,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-14T09:23:02.871Z","etag":null,"topics":["flow","node","nodeflow","secondary-development","visualprogramming","wpf"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fhhyyp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-04T08:51:56.000Z","updated_at":"2025-03-14T08:04:13.000Z","dependencies_parsed_at":"2024-12-12T13:35:32.956Z","dependency_job_id":"63f4d188-da9a-49bb-b186-00b5403b9fa4","html_url":"https://github.com/fhhyyp/serein-flow","commit_stats":{"total_commits":109,"total_committers":3,"mean_commits":"36.333333333333336","dds":0.04587155963302747,"last_synced_commit":"d646c4e820d38ed8aae2befc42fe2c5bf193d62e"},"previous_names":["fhhyyp/serein-flow"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhhyyp%2Fserein-flow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhhyyp%2Fserein-flow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhhyyp%2Fserein-flow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhhyyp%2Fserein-flow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fhhyyp","download_url":"https://codeload.github.com/fhhyyp/serein-flow/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243801610,"owners_count":20350106,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flow","node","nodeflow","secondary-development","visualprogramming","wpf"],"created_at":"2024-10-27T04:04:09.051Z","updated_at":"2025-03-15T23:30:59.218Z","avatar_url":"https://github.com/fhhyyp.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 自述\n基于Dotnet 8 的流程可视化编辑器，需二次开发。\n不定期在Bilibili个人空间上更新相关的视频。\nhttps://space.bilibili.com/33526379\n\n\n# 计划任务 2024年10月28日更新\n* 重新完善远程管理与远程客户端的功能（目前仅支持远程修改节点属性、添加/移除节点、启动流程、停止流程）\n* 重新完善节点树视图、IOC容器对象视图（目前残废版）\n* 计划实现单步执行（暂未想到如何在不影响异步流程的前提下停止流程）\n* 考虑模仿AST的方式，将流程图以“Node token → C# Code”的方式进行原生代码支持\n# 如何加载我的DLL？\n为你的工程添加**Serein.Library**项目引用（也可在Negut上下载），使用 **DynamicFlow** 特性标记你的类，可以参照 **Net461DllTest** 的实现（该示例工程的设计并不完善，并未做依赖分离，仅做参考）。编译为 Dll文件 后，拖入到软件中即可。\n如果你不想下载整个工程文件，“FLowEdit”目录下放有“FlowEdit可视化流程编辑器.zip”压缩包，可以直接解压使用（但可能需要你安装 .Net8 运行环境）。\n# 如何让我的方法成为节点？\n使用 **NodeAction** 特性标记你的方法。\n* 动作节点 - Action\n* 触发器节点 - Flipflop\n* UI节点 - UI\n# 关于 IDynamicContext 说明（重要）**\n * 基本说明：IDynamicContext 是节点之间传递数据的接口、载体，其实例由 FlowEnvironment 运行环境自动实现，内部提供全局单例的环境接口，用以注册、获取实例（单例模式），一般情况下，你无须关注 FlowEnvironment 对外暴露的属性方法。\n * 重要概念：\n   * 每个节点其实对应类库中的某一个方法，这些方法组合起来，加上预先设置的逻辑分支，就是一个完整的节点流。在这个节点流当中，从第一个节点开始、直到所有可达的节点，都会通过同一个流程上下文传递、共享数据。为了符合多线程操作的理念，每个运行起来的节点流之间，流程数据并不互通，从根本隔绝了“脏数据”的产生。\n * 一些重要的属性：\n    * RunState - 流程状态：\n      * 简述：枚举，标识流程运行的状态（初始化，运行中，运行完成）\n      * 场景：类库代码中创建了运行时间较长的异步任务、或开辟了另一个线程进行循环操作时，可以在方法入参定义一个 IDynamicContext 类型入参，然后在代码中使用形成闭包，以及时判断流程是否已经结束。另外的，如果想监听项目停止运行，可以订阅 context.Env.OnFlowRunComplete 事件。\n    * NextOrientation - 即将进入的分支：\n      * 简述：流程分支枚举， Upstream（上游分支）、IsSucceed（真分支）、IsFail（假分支），IsError（异常分支）。\n      * 场景：允许你在类库代码中操作该属性，手动控制当前节点运行完成后，下一个会执行哪一个类别的节点。\n    * Exit() - 结束流程\n      * 简述：顾名思义，能够让你在类库代码中提前结束当前流程运行\n\n# 关于 DynamicNodeType 枚举的补充说明。\n## 1. 不生成节点控件的枚举值：\n* **Init - 初始化方法**\n  * 入参：**IDynamicContext**（有且只有一个参数）。\n  * 返回值：自定义，但不会处理返回值，支持异步等待。\n  * 描述：在运行时首先被调用。语义类似于构造方法。建议在Init方法内初始化类、注册类等一切需要在构造函数中执行的方法。\n* **Loading - 加载方法**\n  * 入参：**IDynamicContext**（有且只有一个参数）。\n  * 返回值：自定义，但不会处理返回值，支持异步等待。\n  * 描述：当所有Dll的Init方法调用完成后，首先调用、也才会调用DLL的Loading方法。建议在Loading方法内进行业务上的初始化（例如启动Web，启动第三方服务）。\n* **Exit - 结束方法**\n  * 入参：**IDynamicContext**（有且只有一个参数）。\n  * 返回值：自定义，但不会处理返回值，支持异步等待。\n  * 描述：当结束/手动结束运行时，会调用所有Dll的Exit方法。使用场景类似于：终止内部的其它线程，通知其它进程关闭，例如停止第三方服务。\n## 2. 基础节点\n* **Script - 脚本节点**\n  * 入参：可选可变\n  * 描述：有时我们需要定义一个临时的类对象，但又不想在代码中写死属性，又或者某些流程操作中，因为业务场景需要布置大量的逻辑判断，导致流程图变得极为臃肿不堪入目，于是引入了脚本节点。脚本节点动态能力强，不同于表达式使用递归下降，而是基于AST抽象语法树调用相应的C#代码，性能至少差强人意。\n  * 使用方式：\n  ```\n  // 定义一个类\n  class Info{\n    string PlcName;\n    string Content;\n    string LogType;\n    DateTime LogTime;\n  }\n\n   // 获取必要的参数\n  let flow = GetFlowApi(); // 脚本默认挂载的方法，获取脚本流程的API\n  let context = GetFlowContext(); // 脚本解释器内置的方法，用以获取当前流程上下文\n  \n  let plc = flow.GetGlobalData(\"JC-PLC\"); // 流程API对应的方法，获取全局数据\n  let arg = flow.GetArgData(context, 0); // 从当前流程上下文获取第一个入参\n  //let arg = flow.GetFlowData(context); // 从当前流程上下文获取运行时上一个节点的返回对象\n  let varInfo = arg.Var; // 获取入参对象的Var属性\n  let data = arg.Data; // 获取入参对象的Data属性\n\n  let log = new Info(); // 创建一个类\n  log.Content =  plc + \" \" + varInfo + \" - 状态 Value  : \" + data;\n  log.PlcName = plc.Name;\n  log.LogType = \"info\";\n  log.LogTime = GetNow(); // 脚本默认挂载的方法，获取当前时间\n  return log; // 返回对象\n  ```\n* **GlobalData - 全局数据节点**\n  * 入参：KeyName ，在整个流程环境中标识某个数据的key值。\n  * 描述：有时需要获取其它节点的数据，但如果强行在两个节点之间进行连线，会让项目流程图变得无比丑陋，如果在类库代码中自己对全局数据进行维护，可能也不太优雅，所以引入了全局数据节点（全局变量）\n  * 使用方式：全局数据节点实质上只是一个节点容器，这意味着你能将任意节点拖拽到该容器节点上，当流程执行到这个容器节点（全局数据节点）时，会自动调用容器内部的节点对应的方法，并将返回的数据保存在运行环境维护的Map中。\n  * 其它获取到全局数据的方式：\n    1. 表达式 ：\n       ~~~\n       @Get #KeyName# // 使用##符号表达全局数据KeyName的标识符\n       ~~~\n    2. Script代码：\n       ~~~~\n       let flow = GetFlowApi(); // 获取流程API\n       let data = flow.GetGlobalData(\"KeyName\"); // 获取全局数据\n       ~~~~\n    3. C# 代码中（不建议）：\n       ~~~\n       SereinEnv.GetFlowGlobalData(\"KeyName\"); // 获取全局数据\n       SereinEnv.AddOrUpdateFlowGlobalData(\"KeyName\", obj); // 设置/更新全局数据，不建议\n       ~~~\n* **ExpOp- 表达式节点**\n  * 入参: 自定义的表达式。\n  * 取值表达式：@Get\n    * 描述：有时节点返回了object，但下一个节点只需要对象中某个属性，而非整个对象。如果修改节点的定义，有可能破坏了代码的封装，为了解决这个痛点，于是增加了表达式功能。\n    * 使用方法：\n      1. 获取对象的属性成员：\n         ~~~~\n         @Get .[property]/[field]\n         ~~~~\n      2. 获取对象的数组成员中下标为22的项：\n         ~~~~\n         @Get .array[22]\n         ~~~~\n      3. 获取对象的字典成员中键为“33”的值：\n         ~~~\n         @Get .dict[33]\n         ~~~\n      4. 获取对象“ID”属性并转为int：\n         ~~~\n         @Get .ID\u003cint\u003e\n         ~~~\n      5. 获取KeyName为【 MyDevice 】全局数据：\n         ~~~\n         @Get #MyDevice#\n         ~~~\n      6. 从全局数据【 MyDevice 】中获取“IP”属性：\n         ~~~\n         @Get #MyDevice#.IP\n         ~~~\n  * 数据类型转换表达式：@Dtc\n    * 描述：有时需要显式的设置节点参数值，但参数接收了其它的类型，需要经过一次转换，将显式的文本值转为入参数据类型。\n    * 使用方法：\n      ~~~\n      @Dtc \u003clong\u003e1233\n      @Dtc \u003cbool\u003eTrue\n      @Dtc \u003cDateTime\u003e2024-12-24 11:13:42 （注：如果右值为“now”，则自动获取当前时间）\n      ~~~\n* **ExpCondition - 条件表达式节点**\n  * 入参: 自定义。\n  * 描述：与表达式节点不同，条件表达式节点是判断条件是否成立，如果成立，返回true，否则返回false，如果表达式执行失败，而进入 error 分支。\n  * 增加描述：如果入参数据为某个对象，需要得到其属性/字段，可以在表达式输入框使用“ .[property]/[field]\u003c type\u003e [op] value ”的方式判断条件，注意，必须使用“.”符号，这有助于显然的表达需要从入参对象中取内部某个值，另外，也可以在入参数据编辑框，使用“@Get .[property]/[field]”的方式重新定义入参数据。\n  * 表达式符号说明：\n  * [property] /[field] : 属性/字段\n  * [op] : 操作符\n    1. bool表达式：==\n    2. 数值表达式 ：==、\u003e=、 \u003c=、in a-b （表示判断是否在a至b的数值范围内）, !in a-b（取反）；\n    3. 文本表达式：==/equals（等于）、!=/notequals（不等于）、c/contains（出现过）、nc/doesnotcontain（没有出现过）、sw/startswith（开头等于）、ew/endswith（结尾等于）\n  * [value] ： 条件值\n## 3. 从DLL生成控件的枚举值：\n* **Action - 动作**\n  * 入参：自定义。如果入参类型为IDynamicContext，会传入当前的上下文；如果入参类型为NodeBase，会传入节点对应的Model。如果不显式指定参数来源，参数会尝试获取运行时上一节点返回值，并根据当前入参类型尝试进行类型转换。\n  * 返回值：自定义，支持异步等待。\n  * 描述：同步执行对应的方法。\n* **Flipflop - 触发器**\n  * 全局触发器\n    * 入参：依照Action节点。\n    * 返回值：Task`\u003cIFlipflopContext\u003cTResult\u003e\u003e`\n    * 描述：运行开始时，所有无上级节点的触发器节点（在当前分支中作为起始节点），分别建立新的线程运行，然后异步等待触发（如果有）。这种触发器拥有独自的DynamicContext上下文（共用同一个Ioc），执行完成之后，会重新从分支起点的触发器开始等待。\n  * 分支中的触发器\n    * 入参：依照Action节点。\n    * 返回值：Task`\u003cIFlipflopContext\u003cTResult\u003e\u003e`\n    * 描述：接收上一节点传递的上下文，同样进入异步等待，但执行完成后不会再次等待自身（只会触发一次）。\n  * 关于 IFlipflopContext`\u003cTResult\u003e` 接口\n    * 基本说明：IFlipflopContext是一个接口，你无须关心内部实现。\n    * 参数描述：State，状态枚举描述（Succeed、Cancel、Error、Cancel），如果返回Cancel，则不会执行后继分支，如果返回其它状态，则会获取对应的后继分支，开始执行。\n    * 参数描述：Type，触发状态描述（External外部触发，Overtime超时触发），当你在代码中的其他地方主动触发了触发器，则该次触发类型为External，当你在创建触发器后超过了指定时间（创建触发器时会要求声明超时时间），则会自动触发，但触发类型为Overtime，触发参数未你在创建触发器时指定的值）\n    * 参数描述：Value，触发时传递的参数。\n\t* 使用场景：配合 FlowTrigger`\u003cTEnum\u003e` 使用，例如定时从PLC中获取状态，当某个变量发生改变时，会通知相应的触发器，如果需要，可以传递对应的数据。\n* **UI - 自定义控件**\n  * 入参：默认使用上一节点返回值。\n  * 返回值：IEmbeddedContent 接口\n  * 描述：将类库中的WPF UserControl嵌入并显示在一个节点上，显示在工作台UI中。例如在视觉处理流程中，需要即时的显示图片。\n  * 关于 IEmbeddedContent 接口\n    * IEmbeddedContent 需要由你实现，框架并不负责\n```\n\t  [DynamicFlow(\"[界面显示]\")]\n\t  internal class FlowControl\n\t  {\n\t\t  [NodeAction(NodeType.UI)]\n\t\t  public async Task\u003cIEmbeddedContent\u003e CreateImageControl(DynamicContext context)\n\t\t  {\n\t\t\t  WpfUserControlAdapter adapter = null;\n\t\t\t  // 其实你也可以直接创建实例\n\t\t\t  // 但如果你的实例化操作涉及到了对UI元素修改，还是建议像这里一样使用异步方法\n\t\t\t  await context.Env.UIContextOperation.InvokeAsync(() =\u003e\n\t\t\t  {\n\t\t\t\t  var userControl = new UserControl();\n\t\t\t\t  adapter = new WpfUserControlAdapter(userControl, userControl);\n\t\t\t  });\n\t\t\t  return adapter;\n\t\t  }\n\t  }\n\tpublic class WpfUserControlAdapter : IEmbeddedContent\n\t{\n\t\tprivate readonly UserControl userControl;\n\t\tprivate readonly IFlowControl flowControl;\n\n\t\tpublic WpfUserControlAdapter(UserControl userControl, IFlowControl flowControl)\n\t\t{\n\t\t\tthis.userControl = userControl;\n\t\t\tthis.flowControl= flowControl;\n\t\t}\n\n\t\tpublic IFlowControl GetFlowControl()\n\t\t{\n\t\t\treturn flowControl;\n\t\t}\n\n\t\tpublic object GetUserControl()\n\t\t{\n\t\t\treturn userControl;\n\t\t}\n\t}\n```\n  \n## 演示：\n    ![image](https://github.com/fhhyyp/serein-flow/blob/cc5f8255135b96c6bb3669bc4aa8d8167a71c262/Image/%E6%BC%94%E7%A4%BA%20-%201.png)\n    ![image](https://github.com/fhhyyp/serein-flow/blob/cc5f8255135b96c6bb3669bc4aa8d8167a71c262/Image/%E6%BC%94%E7%A4%BA%20-%202.png)\n    ![image](https://github.com/fhhyyp/serein-flow/blob/8f17b786f3585cabfeef60d9ab871d43b69e5461/Image/%E6%BC%94%E7%A4%BA%20-%203.png)\n    ![image](https://github.com/fhhyyp/serein-flow/blob/8f17b786f3585cabfeef60d9ab871d43b69e5461/Image/%E6%BC%94%E7%A4%BA%20-%204.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffhhyyp%2Fserein-flow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffhhyyp%2Fserein-flow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffhhyyp%2Fserein-flow/lists"}