Files
serein-flow/README.md

243 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 自述
基于Dotnet 8 的流程可视化框架,运行依赖、运行环境、编辑环境全部开源,可二次开发。
不定期在Bilibili个人空间上更新相关的视频。
https://space.bilibili.com/33526379
# 计划任务 2025年7月28日更新
* 完善“流程图转c#代码”功能,正在构思热更新的实现方式。
* 仍在构思如何实现多客户端的远程控制逻辑。
# 如何绘制流程?
* 准备阶段
1. 编译 Serein.Workbench 项目,确保你有一个可运行的工作台。
2. 新增一个类库项目,添加 **Serein.Library** 项目引用也可在Negut上下载
* 开始
1. 类库项目中,添加一个类,使用 **DynamicFlow** 特性标记你的类。
2. 类中,使用 **NodeAction** 特性标记你的方法。
3. 编译项目将生成的Dll文件以拖拽方式放入工作台左侧的“类库”面板中工作台会自动加载该Dll文件并显示出你标记的节点方法。
* 绘制流程
1. 在工作台中,新建一个画布。
2. 右键按住左侧的“类库”面板中的方法,拖拽到画布上,工作台会自动生成一个节点。
3. 创建第二个节点,鼠标放在节点的“连接器”上,按住鼠标左键拖拽到第一个节点的“连接器”上,工作台会自动生成一条连线,表示两个节点之间的连接关系。
4. 创建更多的节点,在它们之间创建链接。
* 运行流程
1. 在当前画布中,确保你有一个起始节点(右键点击节点,选择“设置为起始节点”)。
2. 点击工作台顶部的“运行”按钮,选择从当前画布运行,工作台会自动运行当前画布中的流程(从起始节点开始)。
3. 其它方法:
* 需要单独从某个节点开始运行时,在画布空白区域按住右键移动进行框选。选择你想开始运行的节点(只能选取一个),按下 F5 即可开始运行。
* 在工作台菜单栏的“运行”按钮下拉菜单中,选择“运行所有画布”,工作台会自动运行所有画布中的流程(从每个画布的起始节点开始)。
* 保存项目与加载项目
1. 流程图绘制完成后,在工作台菜单栏的“视图”按钮下拉菜单中,选择“保存项目”,工作台将弹出文件选择器,由你选定本地路径进行保存。
2. 需要加载其它项目时,在工作台菜单栏的“视图”按钮下拉菜单中,选择“加载本地项目”,工作台将弹出文件选择器,由你选定本地路径进行加载。
* 查看输出
1. 在工作台菜单栏的“视图”按钮下拉菜单中,选择“输出窗口”,工作台会自动打开一个输出窗口,显示当前工作台输出的信息(包含绘制异常、运行异常等等)。
# 如何让我的方法成为节点?
使用 **NodeAction** 特性标记你的方法。
* 动作节点 - Action
* 触发器节点 - Flipflop
* UI节点 - UI
# 关于 IFlowContext 说明(重要)
* 基本说明IFlowContext 是节点之间传递数据的接口、载体,其实例由 FlowEnvironment 运行环境自动实现,内部提供全局单例的环境接口,用以注册、获取实例(单例模式),一般情况下,你无须关注 FlowEnvironment 对外暴露的属性方法。
* 重要概念:
* 每个节点其实对应类库中的某一个方法,这些方法组合起来,加上预先设置的逻辑分支,就是一个完整的节点流。在这个节点流当中,从第一个节点开始、直到所有可达的节点,都会通过同一个流程上下文传递、共享数据。为了符合多线程操作的理念,每个运行起来的节点流之间,流程数据并不互通,从根本隔绝了“脏数据”的产生。
* 一些重要的属性:
* RunState - 流程状态:
* 简述:枚举,标识流程运行的状态(初始化,运行中,运行完成)
* 场景:类库代码中创建了运行时间较长的异步任务、或开辟了另一个线程进行循环操作时,可以在方法入参定义一个 IFlowContext 类型入参,然后在代码中使用形成闭包,以及时判断流程是否已经结束。另外的,如果想监听项目停止运行,可以订阅 context.Env.OnFlowRunComplete 事件。
* NextOrientation - 即将进入的分支:
* 简述:流程分支枚举, Upstream上游分支、IsSucceed真分支、IsFail假分支IsError异常分支
* 场景:允许你在类库代码中操作该属性,手动控制当前节点运行完成后,下一个会执行哪一个类别的节点。
* Exit() - 结束流程
* 简述:顾名思义,能够让你在类库代码中提前结束当前流程运行
# 关于 DynamicNodeType 枚举的补充说明。
## 1. 不生成节点控件的枚举值:
* **Init - 初始化方法**
* 入参:**IFlowContext**(有且只有一个参数)。
* 返回值:自定义,但不会处理返回值,支持异步等待。
* 描述在运行时首先被调用。语义类似于构造方法。建议在Init方法内初始化类、注册类等一切需要在构造函数中执行的方法。
* **Loading - 加载方法**
* 入参:**IFlowContext**(有且只有一个参数)。
* 返回值:自定义,但不会处理返回值,支持异步等待。
* 描述当所有Dll的Init方法调用完成后首先调用、也才会调用DLL的Loading方法。建议在Loading方法内进行业务上的初始化例如启动Web启动第三方服务
* **Exit - 结束方法**
* 入参:**IFlowContext**(有且只有一个参数)。
* 返回值:自定义,但不会处理返回值,支持异步等待。
* 描述:当结束/手动结束运行时会调用所有Dll的Exit方法。使用场景类似于终止内部的其它线程通知其它进程关闭例如停止第三方服务。
## 2. 基础节点
* **FLowCall - 流程接口**
* 入参:根据目标节点变化。
* 描述:有时我们需要将流程图的逻辑解耦,单独使用某个画布编写逻辑,抽取出一个通用的处理模板,在其它流程图中像接口一样进行调用。
* 使用方式:
1. 创建两个画布分别命名画布A画布B。
2. 在画布A上选择希望暴露出去的一个节点勾选“全局公开”。
3. 在画布B上拖拽创建“流程接口”节点画布选择“画布A”节点选择公开的节点。
4. 在画布B上正常调用即可。
* 关于“参数共享”设置的说明:
* 如果启用了,那么流程接口对应的节点,入参参数将在全局保持一致,一处地方修改后,处处同步(仅限于启用了这一设置的相同的流程接口节点)。
* 如果没有启用,那么流程接口将会从目标节点拷贝相同的入参,单独使用,并不受目标节点的入参参数修改而修改。
* **Script - 脚本节点**
* 入参:可选可变
* 描述有时我们需要定义一个临时的类对象但又不想在代码中写死属性又或者某些流程操作中因为业务场景需要布置大量的逻辑判断导致流程图变得极为臃肿不堪入目于是引入了脚本节点。脚本节点动态能力强不同于表达式使用递归下降而是基于AST抽象语法树调用相应的C#代码,性能至少差强人意。
* 使用方式:
```
// 入参是一个对象,入参每次是 info ,有 Var 、 Data属性。
class Info{
string PlcName;
string Content;
string LogType;
DateTime LogTime;
}
plc = global("JC-PLC"); // 获取全局数据节点中的数据
log = new Info() { // 创建一个类
Content = plc + " " + info.Var + " - 状态 Value : " + info.Data,
PlcName = plc.Name,
LogType = "info",
LogTime = now(), // 脚本默认挂载的方法,获取当前时间
};
return log; // 返回对象
```
* **GlobalData - 全局数据节点**
* 入参KeyName 在整个流程环境中标识某个数据的key值。
* 描述:有时需要获取其它节点的数据,但如果强行在两个节点之间进行连线,会让项目流程图变得无比丑陋,如果在类库代码中自己对全局数据进行维护,可能也不太优雅,所以引入了全局数据节点(全局变量)
* 使用方式全局数据节点实质上只是一个节点容器这意味着你能将任意节点拖拽到该容器节点上当流程执行到这个容器节点全局数据节点会自动调用容器内部的节点对应的方法并将返回的数据保存在运行环境维护的Map中。
* 其它获取到全局数据的方式:
1. 表达式
~~~
@Get global("DataName") // 使用global表达全局数据 DataName 的标识符
~~~
2. Script代码
~~~~
data = global("DataName"); // 获取全局数据节点中 DataName 的数据
~~~~
## 3. 从DLL生成控件的枚举值
* **Action - 动作**
* 入参自定义。如果入参类型为IFlowContext会传入当前的上下文如果入参类型为IFlowNode会传入节点对应的实体Model。如果不显式指定参数来源参数会尝试获取运行时上一节点返回值并根据当前入参类型尝试进行类型转换。
* 返回值:自定义,支持异步等待。
* 描述:同步执行对应的方法。
* **Flipflop - 触发器**
* 全局触发器
* 入参依照Action节点。
* 返回值Task`<IFlipflopContext<TResult>>`
* 描述运行开始时所有无上级节点的触发器节点在当前分支中作为起始节点分别建立新的线程运行然后异步等待触发如果有。这种触发器拥有独自的IFlowContext上下文共用同一个Ioc执行完成之后会重新从分支起点的触发器开始等待。
* 分支中的触发器
* 入参依照Action节点。
* 返回值Task`<IFlipflopContext<TResult>>`
* 描述:接收上一节点传递的上下文,同样进入异步等待,但执行完成后不会再次等待自身(只会触发一次)。
* 关于 IFlipflopContext`<TResult>` 接口
* 基本说明IFlipflopContext是一个接口你无须关心内部实现。
* 参数描述State状态枚举描述Succeed、Cancel、Error、Cancel如果返回Cancel则不会执行后继分支如果返回其它状态则会获取对应的后继分支开始执行。
* 参数描述Type触发状态描述External外部触发Overtime超时触发当你在代码中的其他地方主动触发了触发器则该次触发类型为External当你在创建触发器后超过了指定时间创建触发器时会要求声明超时时间则会自动触发但触发类型为Overtime触发参数未你在创建触发器时指定的值
* 参数描述Value触发时传递的参数。
* 使用场景:配合 FlowTrigger`<TEnum>` 使用例如定时从PLC中获取状态当某个变量发生改变时会通知相应的触发器如果需要可以传递对应的数据。
* * **ExpOp- 表达式节点**
* 入参: 自定义的表达式。
* 取值表达式:@Get
* 描述有时节点返回了object但下一个节点只需要对象中某个属性而非整个对象。如果修改节点的定义有可能破坏了代码的封装为了解决这个痛点于是增加了表达式节点。
* 使用方法:
1. 获取对象的属性成员:
~~~~
@Get .[property]/[field]
~~~~
2. 获取对象的数组成员中下标为22的项
~~~~
@Get .array[22]
~~~~
3. 获取对象的字典成员中键为“zhangsan”的值
~~~
@Get .dict["zhangsan"]
~~~
* **ExpCondition - 条件表达式节点**
* 入参: 自定义。
* 描述与表达式节点不同条件表达式节点是判断条件是否成立如果成立返回true否则返回false如果表达式执行失败而进入 error 分支。
* 使用方式:
* 入参说明:默认从上一节点获取,也可以显式设定值,也可以参考表达式节点,使用“@Get .[property]/[field]”的方式重新定义入参数据,用以条件表达式判断。
* 条件表达式默认“PASS”代表跳过判断直接进入下一个分支。
* 条件表达式格式“.[property]/[field]< type> [op] value”注意开头必须使用“.”符号,这有助于显然的表达需要从入参对象中取内部某个值。
* 表达式符号说明:
* [property] /[field] : 属性/字段
* [op] : 操作符
1. bool表达式==
2. 数值表达式 ==、>=、 <=、in a-b 表示判断是否在a至b的数值范围内, !in a-b取反
3. 文本表达式:==/equals等于、!=/notequals不等于、c/contains出现过、nc/doesnotcontain没有出现过、sw/startswith开头等于、ew/endswith结尾等于
* < type> :指定需要转换为某个类型,可不选。
* [value] 条件值
* 使用示例:
~~~
场景1上一个节点传入了该对象伪代码
class Data
{
string Name; // 性能
string Age; // 年龄,外部传入了文本类型
string IdentityCardNumber; // 身份证号
}
需求需要判断年龄是否在某个区间例如需要大于18岁小于35岁。注意这里的“data”是入参数据的默认名称。意味着假如你需要在表达式里使用入参数据时就可以使用“data”。
条件表达式:.Age > 18 && data.Age < 35
需求需要判断是否是北京身份证开头为”1100”可以使用string类型中的StartsWith方法这里的匹配的是StartsWith(String)重载。
条件表达式:.IdentityCardNumber.StartsWith("1100")
另一种方法:
入参使用表达式:@Get .IdentityCardNumber
条件表达式sw 1100
~~~
* **UI - 自定义控件**
* 入参:默认使用上一节点返回值。
* 返回值IEmbeddedContent 接口
* 描述将类库中的WPF UserControl嵌入并显示在一个节点上显示在工作台UI中。例如在视觉处理流程中需要即时的显示图片。
* 关于 IEmbeddedContent 接口
* IEmbeddedContent 需要由你实现,框架并不负责
```
[DynamicFlow("[界面显示]")]
internal class FlowControl
{
[NodeAction(NodeType.UI)]
public async Task<IEmbeddedContent> CreateImageControl(FlowContext context)
{
WpfUserControlAdapter adapter = null;
// 其实你也可以直接创建实例
// 但如果你的实例化操作涉及到了对UI元素修改还是建议像这里一样使用异步方法
await context.Env.UIContextOperation.InvokeAsync(() =>
{
var userControl = new UserControl();
adapter = new WpfUserControlAdapter(userControl, userControl);
});
return adapter;
}
}
public class WpfUserControlAdapter : IEmbeddedContent
{
private readonly UserControl userControl;
private readonly IFlowControl flowControl;
public WpfUserControlAdapter(UserControl userControl, IFlowControl flowControl)
{
this.userControl = userControl;
this.flowControl= flowControl;
}
public IFlowControl GetFlowControl()
{
return flowControl;
}
public object GetUserControl()
{
return userControl;
}
}
```
## 演示:
![image1](https://github.com/fhhyyp/serein-flow/blob/cc5f8255135b96c6bb3669bc4aa8d8167a71c262/Image/%E6%BC%94%E7%A4%BA%20-%201.png)
![image2](https://github.com/fhhyyp/serein-flow/blob/cc5f8255135b96c6bb3669bc4aa8d8167a71c262/Image/%E6%BC%94%E7%A4%BA%20-%202.png)
![image3](https://github.com/fhhyyp/serein-flow/blob/8f17b786f3585cabfeef60d9ab871d43b69e5461/Image/%E6%BC%94%E7%A4%BA%20-%203.png)
![image4](https://github.com/fhhyyp/serein-flow/blob/8f17b786f3585cabfeef60d9ab871d43b69e5461/Image/%E6%BC%94%E7%A4%BA%20-%204.png)