From f7cae3493fcfbb10f21a343a887fe387c6e5a10c Mon Sep 17 00:00:00 2001 From: fengjiayi <12821976+ning_xi@user.noreply.gitee.com> Date: Tue, 27 May 2025 23:46:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=BB=E5=B8=83=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Library/Enums/NodeType.cs | 25 ++--- Library/Extension/FlowModelExtension.cs | 8 +- Library/FlowNode/FlowCanvasDetails.cs | 23 +++- Library/FlowNode/NodeModelBaseData.cs | 40 +++++-- Library/FlowNode/NodeModelBaseFunc.cs | 2 +- Library/FlowNode/SereinProjectData.cs | 5 + Library/Serein.Library.csproj | 7 +- Library/Utils/SereinIoc.cs | 2 +- Library/Utils/UIContextOperation.cs | 19 +++- NodeFlow/Env/FlowEnvironment.cs | 102 ++++++++---------- NodeFlow/Env/RemoteFlowEnvironment.cs | 18 +++- NodeFlow/FlowWorkManagement.cs | 2 +- NodeFlow/Model/SingleFlowCallNode.cs | 33 ++++++ NodeFlow/Model/SingleGlobalDataNode.cs | 4 +- Serein.Library.MyGenerator/Attribute.cs | 2 +- Workbench/App.xaml.cs | 1 + Workbench/Models/TabModel.cs | 7 +- Workbench/Node/View/ActionNodeControl.xaml | 10 +- Workbench/Node/View/ConnectionControl.cs | 2 +- Workbench/Node/View/FlowCallNodeControl.xaml | 16 +++ .../Node/View/FlowCallNodeControl.xaml.cs | 36 +++++++ .../ViewModel/FlowCallNodeControlViewModel.cs | 18 ++++ Workbench/Serein.WorkBench.csproj | 4 + Workbench/Services/FlowNodeService.cs | 25 ++++- Workbench/Services/KeyEventService.cs | 4 +- .../ViewModels/CanvasNodeTreeViewModel.cs | 41 +++++++ Workbench/ViewModels/FlowEditViewModel.cs | 22 ++-- Workbench/ViewModels/Locator.cs | 17 +-- Workbench/Views/CanvasInfoView.xaml | 96 +++++++++++++++++ Workbench/Views/CanvasInfoView.xaml.cs | 32 ++++++ Workbench/Views/FlowCanvasView.xaml.cs | 46 ++++++-- Workbench/Views/FlowEditView.xaml | 14 ++- Workbench/Views/FlowWorkbenchView.xaml | 4 +- 33 files changed, 532 insertions(+), 155 deletions(-) create mode 100644 NodeFlow/Model/SingleFlowCallNode.cs create mode 100644 Workbench/Node/View/FlowCallNodeControl.xaml create mode 100644 Workbench/Node/View/FlowCallNodeControl.xaml.cs create mode 100644 Workbench/Node/ViewModel/FlowCallNodeControlViewModel.cs create mode 100644 Workbench/ViewModels/CanvasNodeTreeViewModel.cs create mode 100644 Workbench/Views/CanvasInfoView.xaml create mode 100644 Workbench/Views/CanvasInfoView.xaml.cs diff --git a/Library/Enums/NodeType.cs b/Library/Enums/NodeType.cs index 37f7fbc..24f0346 100644 --- a/Library/Enums/NodeType.cs +++ b/Library/Enums/NodeType.cs @@ -65,16 +65,6 @@ namespace Serein.Library } - - class UserInfo - { - public string Name; - public int Id; - public string[] PhoneNums; - } - - - /// /// 生成的节点控件 @@ -109,11 +99,7 @@ namespace Serein.Library /// [Description("base")] ExpCondition, - /// - /// 条件节点区域 - /// - [Description("base")] - ConditionRegion, + /// /// 全局数据 /// @@ -129,6 +115,15 @@ namespace Serein.Library /// [Description("base")] NetScript, + + /// + /// 流程调用节点(流程图公开的节点) + /// + [Description("base")] + FlowCall, + + + } } diff --git a/Library/Extension/FlowModelExtension.cs b/Library/Extension/FlowModelExtension.cs index 6e051d6..c930380 100644 --- a/Library/Extension/FlowModelExtension.cs +++ b/Library/Extension/FlowModelExtension.cs @@ -93,7 +93,6 @@ namespace Serein.Library public static NodeInfo ToInfo(this NodeModelBase nodeModel) { // if (MethodDetails == null) return null; - var trueNodes = nodeModel.SuccessorNodes[ConnectionInvokeType.IsSucceed].Select(item => item.Guid); // 真分支 var falseNodes = nodeModel.SuccessorNodes[ConnectionInvokeType.IsFail].Select(item => item.Guid);// 假分支 var errorNodes = nodeModel.SuccessorNodes[ConnectionInvokeType.IsError].Select(item => item.Guid);// 异常分支 @@ -103,8 +102,9 @@ namespace Serein.Library var nodeInfo = new NodeInfo { - CanvasGuid = nodeModel.CanvasGuid, + CanvasGuid = nodeModel.CanvasDetails.Guid, Guid = nodeModel.Guid, + IsPublic = nodeModel.IsPublic, AssemblyName = nodeModel.MethodDetails.AssemblyName, MethodName = nodeModel.MethodDetails?.MethodName, Label = nodeModel.MethodDetails?.MethodAnotherName, @@ -131,18 +131,18 @@ namespace Serein.Library /// 从节点信息加载节点 /// /// + /// /// /// public static void LoadInfo(this NodeModelBase nodeModel, NodeInfo nodeInfo) { - nodeModel.CanvasGuid = nodeInfo.CanvasGuid; nodeModel.Guid = nodeInfo.Guid; nodeModel.Position = nodeInfo.Position ?? new PositionOfUI(0, 0);// 加载位置信息 var md = nodeModel.MethodDetails; // 当前节点的方法说明 nodeModel.MethodDetails.IsProtectionParameter = nodeInfo.IsProtectionParameter; // 保护参数 nodeModel.DebugSetting.IsInterrupt = nodeInfo.IsInterrupt; // 是否中断 nodeModel.DebugSetting.IsEnable = nodeInfo.IsEnable; // 是否使能 - + nodeModel.IsPublic = nodeInfo.IsPublic; // 是否全局公开 if (md != null) { if (md.ParameterDetailss == null) diff --git a/Library/FlowNode/FlowCanvasDetails.cs b/Library/FlowNode/FlowCanvasDetails.cs index 0fa1ef7..c8d6e49 100644 --- a/Library/FlowNode/FlowCanvasDetails.cs +++ b/Library/FlowNode/FlowCanvasDetails.cs @@ -2,6 +2,7 @@ using Serein.Library.FlowNode; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -27,9 +28,21 @@ namespace Serein.Library public IFlowEnvironment Env { get; } /// - /// 标识画布ID + /// 画布拥有的节点 /// [PropertyInfo(IsProtection = true)] + private System.Collections.ObjectModel.ObservableCollection _nodes = []; + + /// + /// 画布公开的节点 + /// + [PropertyInfo(IsProtection = true)] + private System.Collections.ObjectModel.ObservableCollection _publicNodes = []; + + /// + /// 标识画布ID + /// + [PropertyInfo(IsProtection = false)] private string _guid; /// @@ -54,19 +67,19 @@ namespace Serein.Library /// 预览位置X /// [PropertyInfo(IsNotification = true)] - private double _viewX ; + private double _viewX; /// /// 预览位置Y /// [PropertyInfo(IsNotification = true)] - private double _viewY ; + private double _viewY; /// /// 缩放比例X /// [PropertyInfo(IsNotification = true)] - private double _scaleX = 1; + private double _scaleX = 1; /// /// 缩放比例Y @@ -85,7 +98,7 @@ namespace Serein.Library public partial class FlowCanvasDetails { - + } diff --git a/Library/FlowNode/NodeModelBaseData.cs b/Library/FlowNode/NodeModelBaseData.cs index 44efedb..6f5d7be 100644 --- a/Library/FlowNode/NodeModelBaseData.cs +++ b/Library/FlowNode/NodeModelBaseData.cs @@ -14,7 +14,7 @@ namespace Serein.Library /// /// 节点基类(数据) /// - [NodeProperty(ValuePath = NodeValuePath.None)] + [NodeProperty(ValuePath = NodeValuePath.Node)] public abstract partial class NodeModelBase : IDynamicFlowNode { /// @@ -35,13 +35,11 @@ namespace Serein.Library [PropertyInfo(IsProtection = true)] private NodeControlType _controlType; - /// /// 所属画布 /// - [PropertyInfo(IsProtection = true)] - private string _canvasGuid ; - + [PropertyInfo(IsProtection = true)] + private FlowCanvasDetails _canvasDetails ; /// /// 在画布中的位置 @@ -56,10 +54,10 @@ namespace Serein.Library private string _displayName; /// - /// 是否为起点控件 + /// 是否公开 /// - [PropertyInfo] - private bool _isStart; + [PropertyInfo(IsNotification = true, CustomCodeAtEnd = "NodePublicStateChanged();")] + private bool _isPublic; /// /// 附加的调试功能 @@ -68,7 +66,7 @@ namespace Serein.Library private NodeDebugSetting _debugSetting ; /// - /// 方法描述。不包含Method与委托,需要通过MethodName从环境中获取委托进行调用。 + /// 方法描述。包含参数信息。不包含Method与委托,如若需要调用对应的方法,需要通过MethodName从环境中获取委托进行调用。 /// [PropertyInfo(IsProtection = true)] private MethodDetails _methodDetails ; @@ -122,7 +120,29 @@ namespace Serein.Library /// public List ChildrenNode { get; } - + /// + /// 节点公开状态发生改变 + /// + private void NodePublicStateChanged() + { + + if (IsPublic) + { + // 公开节点 + if (!CanvasDetails.PublicNodes.Contains(this)) + { + CanvasDetails.PublicNodes.Add(this); + } + } + else + { + // 取消公开 + if (CanvasDetails.PublicNodes.Contains(this)) + { + CanvasDetails.PublicNodes.Remove(this); + } + } + } } } diff --git a/Library/FlowNode/NodeModelBaseFunc.cs b/Library/FlowNode/NodeModelBaseFunc.cs index fa3a573..0c734dc 100644 --- a/Library/FlowNode/NodeModelBaseFunc.cs +++ b/Library/FlowNode/NodeModelBaseFunc.cs @@ -23,7 +23,7 @@ namespace Serein.Library /// - /// 节点基类(数据):条件控件,动作控件,条件区域,动作区域 + /// 节点基类 /// public abstract partial class NodeModelBase : IDynamicFlowNode { diff --git a/Library/FlowNode/SereinProjectData.cs b/Library/FlowNode/SereinProjectData.cs index 3cd8754..8d7944f 100644 --- a/Library/FlowNode/SereinProjectData.cs +++ b/Library/FlowNode/SereinProjectData.cs @@ -197,6 +197,11 @@ namespace Serein.Library /// public string Guid { get; set; } + /// + /// 是否全局公开 + /// + public bool IsPublic { get; set; } + /// /// 节点方法所属的程序集名称 /// diff --git a/Library/Serein.Library.csproj b/Library/Serein.Library.csproj index 64b0a6e..761a9f1 100644 --- a/Library/Serein.Library.csproj +++ b/Library/Serein.Library.csproj @@ -24,12 +24,15 @@ + + + @@ -51,10 +54,6 @@ - - - - True diff --git a/Library/Utils/SereinIoc.cs b/Library/Utils/SereinIoc.cs index 8642eff..f0b2ce8 100644 --- a/Library/Utils/SereinIoc.cs +++ b/Library/Utils/SereinIoc.cs @@ -198,7 +198,7 @@ namespace Serein.Library.Utils public string Name { get; set; } public Type Type { get; set; } } - private const string FlowBaseClassName = "<>$FlowBaseClass!@#"; + private const string FlowBaseClassName = "@FlowBaseClass"; public Dictionary> BuildDependencyTree() diff --git a/Library/Utils/UIContextOperation.cs b/Library/Utils/UIContextOperation.cs index d377792..3771379 100644 --- a/Library/Utils/UIContextOperation.cs +++ b/Library/Utils/UIContextOperation.cs @@ -54,10 +54,11 @@ namespace Serein.Library.Utils } /// - /// 同步方式进行调用方法 + /// 同步方式在UI线程上进行调用方法 /// /// 要执行的UI操作 - public void Invoke(Action uiAction) + /// 异常发生时的回调 + public void Invoke(Action uiAction, Action onException = null) { if(context is null && getUiContext != null) { @@ -65,7 +66,15 @@ namespace Serein.Library.Utils } context?.Post(state => { - uiAction?.Invoke(); + try + { + uiAction?.Invoke(); + } + catch (Exception ex) + { + if(onException != null) onException(ex); + Debug.WriteLine(ex); + } }, null); } @@ -73,8 +82,9 @@ namespace Serein.Library.Utils /// 异步方式进行调用 /// /// 要执行的UI操作 + /// 异常发生时的回调 /// - public Task InvokeAsync(Action uiAction) + public Task InvokeAsync(Action uiAction, Action onException = null) { if (context is null && getUiContext != null) { @@ -92,6 +102,7 @@ namespace Serein.Library.Utils catch (Exception ex) { tcs.SetException(ex); + if (onException != null) onException(ex); Debug.WriteLine(ex); } }, null); diff --git a/NodeFlow/Env/FlowEnvironment.cs b/NodeFlow/Env/FlowEnvironment.cs index 1c84fb0..67c7b7c 100644 --- a/NodeFlow/Env/FlowEnvironment.cs +++ b/NodeFlow/Env/FlowEnvironment.cs @@ -6,6 +6,7 @@ using Serein.Library.Utils.SereinExpression; using Serein.NodeFlow.Model; using Serein.NodeFlow.Tool; using System; +using System.Diagnostics; using System.Reactive; using System.Reflection; using System.Text; @@ -52,7 +53,7 @@ namespace Serein.NodeFlow.Env NodeMVVMManagement.RegisterModel(NodeControlType.Flipflop, typeof(SingleFlipflopNode)); // 触发器节点 NodeMVVMManagement.RegisterModel(NodeControlType.ExpOp, typeof(SingleExpOpNode)); // 表达式节点 NodeMVVMManagement.RegisterModel(NodeControlType.ExpCondition, typeof(SingleConditionNode)); // 条件表达式节点 - NodeMVVMManagement.RegisterModel(NodeControlType.ConditionRegion, typeof(CompositeConditionNode)); // 条件区域 + //NodeMVVMManagement.RegisterModel(NodeControlType.ConditionRegion, typeof(CompositeConditionNode)); // 条件区域 NodeMVVMManagement.RegisterModel(NodeControlType.GlobalData, typeof(SingleGlobalDataNode)); // 全局数据节点 NodeMVVMManagement.RegisterModel(NodeControlType.Script, typeof(SingleScriptNode)); // 脚本节点 NodeMVVMManagement.RegisterModel(NodeControlType.NetScript, typeof(SingleNetScriptNode)); // 脚本节点 @@ -309,36 +310,6 @@ namespace Serein.NodeFlow.Env private List FlipflopNodes { get; } = []; - /* - /// - /// 起始节点私有属性 - /// - private NodeModelBase? _startNode = null; - - /// - /// 起始节点 - /// - private NodeModelBase? StartNode - { - get - { - return _startNode; - } - set - { - if (value is null) - { - return; - } - if (_startNode is not null) - { - _startNode.IsStart = false; - } - value.IsStart = true; - _startNode = value; - } - }*/ - /// /// 流程任务管理 /// @@ -385,7 +356,7 @@ namespace Serein.NodeFlow.Env SereinEnv.WriteLine(InfoType.WARN, $"画布不存在,停止运行。{canvasGuid}"); isBreak = true; } - var count = NodeModels.Values.Count(n => n.CanvasGuid.Equals(canvasGuid)); + var count = NodeModels.Values.Count(n => n.CanvasDetails.Guid.Equals(canvasGuid)); if(count == 0) { SereinEnv.WriteLine(InfoType.WARN, $"画布没有节点,停止运行。{canvasGuid}"); @@ -414,7 +385,7 @@ namespace Serein.NodeFlow.Env return false; } var ft = new FlowTask(); - ft.GetNodes = () => NodeModels.Values.Where(node => node.CanvasGuid.Equals(guid)).ToList(); + ft.GetNodes = () => NodeModels.Values.Where(node => node.CanvasDetails.Guid.Equals(guid)).ToList(); var startNodeModel = NodeModels.GetValueOrDefault(canvasModel.StartNode); if(startNodeModel is null) { @@ -867,7 +838,7 @@ namespace Serein.NodeFlow.Env var model = new FlowCanvasDetails(this); model.LoadInfo(info); FlowCanvass.Add(model.Guid, model); - UIContextOperation.InvokeAsync(() => + UIContextOperation.Invoke(() => { OnCanvasCreate.Invoke(new CanvasCreateEventArgs(model)); }); @@ -886,7 +857,7 @@ namespace Serein.NodeFlow.Env { return false; } - var count = NodeModels.Values.Count(node => node.CanvasGuid.Equals(canvasGuid)); + var count = NodeModels.Values.Count(node => node.CanvasDetails.Guid.Equals(canvasGuid)); if(count > 0) { SereinEnv.WriteLine(InfoType.WARN, "无法删除具有节点的画布"); @@ -894,7 +865,7 @@ namespace Serein.NodeFlow.Env } if (FlowCanvass.Remove(canvasGuid)) { - await UIContextOperation.InvokeAsync(() => + UIContextOperation.Invoke(() => { OnCanvasRemove.Invoke(new CanvasRemoveEventArgs(canvasGuid)); }); @@ -942,10 +913,24 @@ namespace Serein.NodeFlow.Env nodeInfo.Guid = string.Empty; continue; } - nodeModel.LoadInfo(nodeInfo); // 创建节点model - TryAddNode(nodeModel); // 加载项目时将节点加载到环境中 - - await UIContextOperation.InvokeAsync(() => + if(FlowCanvass.TryGetValue(nodeInfo.CanvasGuid, out var canvasModel)) + { + + // 节点与画布互相绑定 + // 需要在UI线程上进行添加,否则会报 “不支持从调度程序线程以外的线程对其 SourceCollection 进行的更改”异常 + nodeModel.CanvasDetails = canvasModel; + UIContextOperation.Invoke(() => canvasModel.Nodes.Add(nodeModel)); + + nodeModel.LoadInfo(nodeInfo); // 创建节点model + TryAddNode(nodeModel); // 加载项目时将节点加载到环境中 + } + else + { + SereinEnv.WriteLine(InfoType.ERROR, $"加载节点[{nodeInfo.Guid}]时发生异常,画布[{nodeInfo.CanvasGuid}]不存在"); + return; + } + + UIContextOperation.Invoke(() => OnNodeCreate?.Invoke(new NodeCreateEventArgs(nodeInfo.CanvasGuid, nodeModel, nodeInfo.Position))); // 添加到UI上 } #endregion @@ -971,7 +956,7 @@ namespace Serein.NodeFlow.Env var result = nodeContainer.PlaceNode(nodeModel); if (result) { - await UIContextOperation.InvokeAsync(() => OnNodePlace?.Invoke( + UIContextOperation.Invoke(() => OnNodePlace?.Invoke( new NodePlaceEventArgs(nodeInfo.CanvasGuid, nodeModel.Guid, containerNode.Guid))); } @@ -1036,7 +1021,7 @@ namespace Serein.NodeFlow.Env #region 确定节点之间的参数调用关系 foreach (var toNode in NodeModels.Values) { - var canvasGuid = toNode.CanvasGuid; + var canvasGuid = toNode.CanvasDetails.Guid; if (toNode.MethodDetails.ParameterDetailss == null) { continue; @@ -1055,9 +1040,9 @@ namespace Serein.NodeFlow.Env #endregion - await UIContextOperation.InvokeAsync(() => + UIContextOperation.Invoke(() => { - UIContextOperation?.Invoke(() => OnProjectLoaded?.Invoke(new ProjectLoadedEventArgs())); + OnProjectLoaded?.Invoke(new ProjectLoadedEventArgs()); }); return; @@ -1075,7 +1060,7 @@ namespace Serein.NodeFlow.Env PositionOfUI position, MethodDetailsInfo? methodDetailsInfo = null) { - if (!TryGetCanvasModel(canvasGuid,out var cavnasModel)) + if (!TryGetCanvasModel(canvasGuid,out var canvasModel)) { return Task.FromResult(null); } @@ -1097,9 +1082,9 @@ namespace Serein.NodeFlow.Env return Task.FromResult(null); } } - + nodeModel.CanvasDetails = canvasModel; + canvasModel.Nodes.Add(nodeModel); // 节点与画布互相绑定 TryAddNode(nodeModel); - nodeModel.CanvasGuid = canvasGuid; // 设置所属于的画布 nodeModel.Position = position; // 设置位置 // 通知UI更改 @@ -1107,9 +1092,9 @@ namespace Serein.NodeFlow.Env // 因为需要UI先布置了元素,才能通知UI变更特效 // 如果不存在流程起始控件,默认设置为流程起始控件 - if (cavnasModel.StartNode is null) + if (canvasModel.StartNode is null) { - SetStartNode(cavnasModel, nodeModel); + SetStartNode(canvasModel, nodeModel); } var nodeInfo = nodeModel.ToInfo(); return Task.FromResult(nodeInfo); @@ -1147,7 +1132,7 @@ namespace Serein.NodeFlow.Env var result = nodeContainer.PlaceNode(nodeModel); // 放置在容器节点 if (result) { - _ = UIContextOperation?.InvokeAsync(() => + UIContextOperation.Invoke(() => { OnNodePlace?.Invoke(new NodePlaceEventArgs(canvasGuid, nodeGuid, containerNodeGuid)); // 通知UI更改节点放置位置 }); @@ -1179,7 +1164,7 @@ namespace Serein.NodeFlow.Env var result = nodeContainer.TakeOutNode(nodeModel); // 从容器节点取出 if (result) { - _ = UIContextOperation?.InvokeAsync(() => + UIContextOperation.Invoke(() => { OnNodeTakeOut?.Invoke(new NodeTakeOutEventArgs(canvasGuid, nodeGuid)); // 重新放置在画布上 }); @@ -1198,7 +1183,7 @@ namespace Serein.NodeFlow.Env /// public async Task RemoveNodeAsync(string canvasGuid, string nodeGuid) { - if (!FlowCanvass.ContainsKey(canvasGuid)) + if (!TryGetCanvasModel(canvasGuid,out var canvasModel)) { return false; } @@ -1247,10 +1232,12 @@ namespace Serein.NodeFlow.Env } } - - // 从集合中移除节点 + + // 从集合中移除节点,解除与画布的绑定关系 NodeModels.Remove(nodeGuid); + UIContextOperation?.Invoke(() => canvasModel.Nodes.Remove(remoteNode)); + UIContextOperation?.Invoke(() => OnNodeRemove?.Invoke(new NodeRemoveEventArgs(canvasGuid, nodeGuid))); return true; } @@ -1752,7 +1739,7 @@ namespace Serein.NodeFlow.Env if (OperatingSystem.IsWindows()) { - await UIContextOperation.InvokeAsync(() => OnNodeConnectChange?.Invoke( + UIContextOperation.Invoke(() => OnNodeConnectChange?.Invoke( new NodeConnectChangeEventArgs( canvasGuid, fromNode.Guid, @@ -1785,7 +1772,7 @@ namespace Serein.NodeFlow.Env if (OperatingSystem.IsWindows()) { - await UIContextOperation.InvokeAsync(() => OnNodeConnectChange?.Invoke( + UIContextOperation.Invoke(() => OnNodeConnectChange?.Invoke( new NodeConnectChangeEventArgs( canvasGuid, fromNode.Guid, @@ -1807,6 +1794,7 @@ namespace Serein.NodeFlow.Env { nodeModel.Guid ??= Guid.NewGuid().ToString(); NodeModels.TryAdd(nodeModel.Guid, nodeModel); + // 如果是触发器,则需要添加到专属集合中 if (nodeModel is SingleFlipflopNode flipflopNode) @@ -2005,7 +1993,7 @@ namespace Serein.NodeFlow.Env } toNode.MethodDetails.ParameterDetailss[argIndex].ArgDataSourceNodeGuid = fromNode.Guid; toNode.MethodDetails.ParameterDetailss[argIndex].ArgDataSourceType = connectionArgSourceType; - await UIContextOperation.InvokeAsync(() => + UIContextOperation.Invoke(() => OnNodeConnectChange?.Invoke( new NodeConnectChangeEventArgs( canvasGuid, diff --git a/NodeFlow/Env/RemoteFlowEnvironment.cs b/NodeFlow/Env/RemoteFlowEnvironment.cs index 1e3abee..88ada4d 100644 --- a/NodeFlow/Env/RemoteFlowEnvironment.cs +++ b/NodeFlow/Env/RemoteFlowEnvironment.cs @@ -87,6 +87,12 @@ namespace Serein.NodeFlow.Env public UIContextOperation UIContextOperation { get; } public NodeMVVMManagement NodeMVVMManagement { get; } + + /// + /// 运行环境加载的画布集合 + /// + private Dictionary FlowCanvass { get; } = []; + /// /// 标示是否正在加载项目 /// @@ -893,7 +899,11 @@ namespace Serein.NodeFlow.Env //MethodDetailss.TryGetValue(methodDetailsInfo.MethodName, out var methodDetails);// 加载项目时尝试获取方法信息 var nodeModel = FlowNodeExtension.CreateNode(this, nodeControlType, methodDetails); // 远程环境下加载节点 - nodeModel.LoadInfo(nodeInfo); + + if (FlowCanvass.TryGetValue(nodeInfo.CanvasGuid, out var canvasModel)) + { + } + nodeModel.LoadInfo(nodeInfo); // 创建节点model TryAddNode(nodeModel); IsLoadingNode = false; @@ -1214,6 +1224,10 @@ namespace Serein.NodeFlow.Env nodeInfo.Guid = string.Empty; continue; } + + if (FlowCanvass.TryGetValue(nodeInfo.CanvasGuid, out var canvasModel)) + { + } nodeModel.LoadInfo(nodeInfo); // 创建节点model TryAddNode(nodeModel); // 加载项目时将节点加载到环境中 @@ -1300,7 +1314,7 @@ namespace Serein.NodeFlow.Env if (!string.IsNullOrEmpty(pd.ArgDataSourceNodeGuid) && NodeModels.TryGetValue(pd.ArgDataSourceNodeGuid, out var fromNode)) { - var canvasGuid = toNode.CanvasGuid; + var canvasGuid = toNode.CanvasDetails.Guid; UIContextOperation?.Invoke(() => OnNodeConnectChange?.Invoke( new NodeConnectChangeEventArgs( diff --git a/NodeFlow/FlowWorkManagement.cs b/NodeFlow/FlowWorkManagement.cs index 53d2f11..fe19718 100644 --- a/NodeFlow/FlowWorkManagement.cs +++ b/NodeFlow/FlowWorkManagement.cs @@ -227,7 +227,7 @@ namespace Serein.NodeFlow { var env = WorkOptions.Environment; var flipflopNodes = flow.GetNodes().Where(item => item is SingleFlipflopNode node - && !node.IsStart + && node.DebugSetting.IsEnable && node.NotExitPreviousNode()) .Select(item => (SingleFlipflopNode)item); diff --git a/NodeFlow/Model/SingleFlowCallNode.cs b/NodeFlow/Model/SingleFlowCallNode.cs new file mode 100644 index 0000000..36f0cf6 --- /dev/null +++ b/NodeFlow/Model/SingleFlowCallNode.cs @@ -0,0 +1,33 @@ +using Serein.Library; +using Serein.Library.Api; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.NodeFlow.Model +{ + /// + /// 流程调用节点 + /// + public class SingleFlowCallNode : NodeModelBase + { + public SingleFlowCallNode(IFlowEnvironment environment) : base(environment) + { + + } + + /// + /// 需要调用其它流程图中的某个节点 + /// + /// + /// + /// + public override async Task ExecutingAsync(IDynamicContext context, CancellationToken token) + { + await base.ExecutingAsync(context, token); + return new FlowResult(this, context, null); + } + } +} \ No newline at end of file diff --git a/NodeFlow/Model/SingleGlobalDataNode.cs b/NodeFlow/Model/SingleGlobalDataNode.cs index be3a5ba..4ba93de 100644 --- a/NodeFlow/Model/SingleGlobalDataNode.cs +++ b/NodeFlow/Model/SingleGlobalDataNode.cs @@ -96,7 +96,7 @@ namespace Serein.NodeFlow.Model { foreach (var nodeModel in ChildrenNode) { - await nodeModel.Env.TakeOutNodeToContainerAsync(nodeModel.CanvasGuid, nodeModel.Guid); + await nodeModel.Env.TakeOutNodeToContainerAsync(nodeModel.CanvasDetails.Guid, nodeModel.Guid); } DataNode = null; } @@ -178,7 +178,7 @@ namespace Serein.NodeFlow.Model return; } // 移除数据节点 - _ = this.Env.RemoveNodeAsync(DataNode.CanvasGuid, DataNode.Guid); + _ = this.Env.RemoveNodeAsync(DataNode.CanvasDetails.Guid, DataNode.Guid); } } diff --git a/Serein.Library.MyGenerator/Attribute.cs b/Serein.Library.MyGenerator/Attribute.cs index ba8c22a..13c260e 100644 --- a/Serein.Library.MyGenerator/Attribute.cs +++ b/Serein.Library.MyGenerator/Attribute.cs @@ -56,7 +56,7 @@ namespace Serein.Library public sealed class PropertyInfoAttribute : Attribute { /// - /// 是否通知UI + /// 是否通知远程环境(如果在远程环境下) /// public bool IsNotification = false; diff --git a/Workbench/App.xaml.cs b/Workbench/App.xaml.cs index b6b9831..4bbcf63 100644 --- a/Workbench/App.xaml.cs +++ b/Workbench/App.xaml.cs @@ -34,6 +34,7 @@ namespace Serein.Workbench collection.AddSingleton(); collection.AddTransient(); // 画布 + collection.AddTransient(); // 画布节点树视图 } public static void AddWorkbenchServices(this IServiceCollection collection) diff --git a/Workbench/Models/TabModel.cs b/Workbench/Models/TabModel.cs index c1fad2b..6197c24 100644 --- a/Workbench/Models/TabModel.cs +++ b/Workbench/Models/TabModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using Newtonsoft.Json.Linq; +using Serein.Library; using Serein.Workbench.ViewModels; using Serein.Workbench.Views; using System; @@ -19,7 +20,7 @@ namespace Serein.Workbench.Models /// /// tab 名称 /// - public string Name + /* public string Name { get { @@ -34,6 +35,10 @@ namespace Serein.Workbench.Models OnPropertyChanged(nameof(Name)); } } +*/ + + [ObservableProperty] + private FlowCanvasDetails _model; /// diff --git a/Workbench/Node/View/ActionNodeControl.xaml b/Workbench/Node/View/ActionNodeControl.xaml index 9b4d134..12b4954 100644 --- a/Workbench/Node/View/ActionNodeControl.xaml +++ b/Workbench/Node/View/ActionNodeControl.xaml @@ -91,7 +91,13 @@ - + + + + + + + @@ -107,6 +113,8 @@ + + diff --git a/Workbench/Node/View/ConnectionControl.cs b/Workbench/Node/View/ConnectionControl.cs index 07080b9..5eacfb9 100644 --- a/Workbench/Node/View/ConnectionControl.cs +++ b/Workbench/Node/View/ConnectionControl.cs @@ -238,7 +238,7 @@ namespace Serein.Workbench.Node.View { Canvas.Children.Remove(BezierLine); var env = Start.MyNode.Env; - var canvasGuid = Start.MyNode.CanvasGuid; + var canvasGuid = Start.MyNode.CanvasDetails.Guid; if (Start.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke) { env.RemoveConnectInvokeAsync(canvasGuid, Start.MyNode.Guid, End.MyNode.Guid, InvokeType); diff --git a/Workbench/Node/View/FlowCallNodeControl.xaml b/Workbench/Node/View/FlowCallNodeControl.xaml new file mode 100644 index 0000000..b5eccaf --- /dev/null +++ b/Workbench/Node/View/FlowCallNodeControl.xaml @@ -0,0 +1,16 @@ + + + + + + diff --git a/Workbench/Node/View/FlowCallNodeControl.xaml.cs b/Workbench/Node/View/FlowCallNodeControl.xaml.cs new file mode 100644 index 0000000..3eb41de --- /dev/null +++ b/Workbench/Node/View/FlowCallNodeControl.xaml.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Serein.Workbench.Node.View +{ + /// + /// FlowCallNodeControl.xaml 的交互逻辑 + /// + public partial class FlowCallNodeControl : NodeControlBase, INodeJunction + { + public FlowCallNodeControl() + { + InitializeComponent(); + } + + public JunctionControlBase ExecuteJunction => throw new NotImplementedException(); + + public JunctionControlBase NextStepJunction => throw new NotImplementedException(); + + public JunctionControlBase[] ArgDataJunction => throw new NotImplementedException(); + + public JunctionControlBase ReturnDataJunction => throw new NotImplementedException(); + } +} diff --git a/Workbench/Node/ViewModel/FlowCallNodeControlViewModel.cs b/Workbench/Node/ViewModel/FlowCallNodeControlViewModel.cs new file mode 100644 index 0000000..4171b87 --- /dev/null +++ b/Workbench/Node/ViewModel/FlowCallNodeControlViewModel.cs @@ -0,0 +1,18 @@ +using Serein.NodeFlow.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Workbench.Node.ViewModel +{ + public partial class FlowCallNodeControlViewModel : NodeControlViewModelBase + { + public new SingleFlowCallNode NodelModel { get; } + public FlowCallNodeControlViewModel(SingleFlowCallNode node) : base(node) + { + this.NodelModel = node; + } + } +} diff --git a/Workbench/Serein.WorkBench.csproj b/Workbench/Serein.WorkBench.csproj index 1ce1737..0561aaf 100644 --- a/Workbench/Serein.WorkBench.csproj +++ b/Workbench/Serein.WorkBench.csproj @@ -31,6 +31,8 @@ + + @@ -43,6 +45,8 @@ + + diff --git a/Workbench/Services/FlowNodeService.cs b/Workbench/Services/FlowNodeService.cs index 5d837e3..0476c55 100644 --- a/Workbench/Services/FlowNodeService.cs +++ b/Workbench/Services/FlowNodeService.cs @@ -36,14 +36,31 @@ namespace Serein.Workbench.Services /// 添加了节点 /// public Action OnCreateNode { get; set; } - + + /// + /// 查看的画布发生改变 + /// + public Action OnViewCanvasChanged{ get; set; } + #endregion #region 创建节点相关的属性 + + + private FlowCanvasView currentSelectCanvas; /// /// 当前查看的画布 /// - public FlowCanvasView CurrentSelectCanvas { get; set; } + public FlowCanvasView CurrentSelectCanvas { get => currentSelectCanvas; set + { + if (value == null || value.Equals(currentSelectCanvas)) + { + return; + } + currentSelectCanvas = value; + OnViewCanvasChanged?.Invoke(value); + } + } /// /// 当前拖动的方法信息 @@ -128,7 +145,7 @@ namespace Serein.Workbench.Services flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.Flipflop, typeof(FlipflopNodeControl), typeof(FlipflopNodeControlViewModel)); flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.ExpOp, typeof(ExpOpNodeControl), typeof(ExpOpNodeControlViewModel)); flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.ExpCondition, typeof(ConditionNodeControl), typeof(ConditionNodeControlViewModel)); - flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.ConditionRegion, typeof(ConditionRegionControl), typeof(ConditionRegionNodeControlViewModel)); + //flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.ConditionRegion, typeof(ConditionRegionControl), typeof(ConditionRegionNodeControlViewModel)); flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.GlobalData, typeof(GlobalDataControl), typeof(GlobalDataNodeControlViewModel)); flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.Script, typeof(ScriptNodeControl), typeof(ScriptNodeControlViewModel)); flowEnvironment.NodeMVVMManagement.RegisterUI(NodeControlType.NetScript, typeof(NetScriptNodeControl), typeof(NetScriptNodeControlViewModel)); @@ -648,7 +665,7 @@ namespace Serein.Workbench.Services return; } - _ = flowEnvironment.RemoveNodeAsync(model.CanvasGuid, model.Guid); + _ = flowEnvironment.RemoveNodeAsync(model.CanvasDetails.Guid, model.Guid); } #endregion diff --git a/Workbench/Services/KeyEventService.cs b/Workbench/Services/KeyEventService.cs index 88c6ec2..865f6f6 100644 --- a/Workbench/Services/KeyEventService.cs +++ b/Workbench/Services/KeyEventService.cs @@ -78,14 +78,14 @@ namespace Serein.Workbench.Services { KeysState[(int)key] = true; OnKeyDown?.Invoke(key); - Debug.WriteLine($"按键按下事件:{key}"); + //Debug.WriteLine($"按键按下事件:{key}"); } public void KeyUp(Key key) { KeysState[(int)key] = false; OnKeyUp?.Invoke(key); - Debug.WriteLine($"按键抬起事件:{key}"); + //Debug.WriteLine($"按键抬起事件:{key}"); } } diff --git a/Workbench/ViewModels/CanvasNodeTreeViewModel.cs b/Workbench/ViewModels/CanvasNodeTreeViewModel.cs new file mode 100644 index 0000000..26186a1 --- /dev/null +++ b/Workbench/ViewModels/CanvasNodeTreeViewModel.cs @@ -0,0 +1,41 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Serein.Library; +using Serein.Workbench.Services; +using Serein.Workbench.Views; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Workbench.ViewModels +{ + internal partial class CanvasInfoViewModel : ObservableObject + { + private readonly FlowNodeService flowNodeService; + + /// + /// 画布数据实体 + /// + [ObservableProperty] + private FlowCanvasDetails _model; + + public CanvasInfoViewModel(FlowNodeService flowNodeService) + { + this.flowNodeService = flowNodeService; + this.flowNodeService.OnViewCanvasChanged += OnViewCanvasChanged; + } + + /// + /// 查看的画布发生改变 + /// + /// + private void OnViewCanvasChanged(FlowCanvasView flowCanvas) + { + if (flowCanvas.DataContext is FlowCanvasViewModel vm) + { + Model = vm.Model; + } + } + } +} diff --git a/Workbench/ViewModels/FlowEditViewModel.cs b/Workbench/ViewModels/FlowEditViewModel.cs index 45a4d33..f993307 100644 --- a/Workbench/ViewModels/FlowEditViewModel.cs +++ b/Workbench/ViewModels/FlowEditViewModel.cs @@ -22,8 +22,11 @@ namespace Serein.Workbench.ViewModels /// public partial class FlowEditViewModel : ObservableObject { - public ObservableCollection CanvasTabs { get; set; } = []; - + /// + /// 画布集合 + /// + [ObservableProperty] + private ObservableCollection _canvasTabs = []; /// /// 当前选择的画布 @@ -42,11 +45,8 @@ namespace Serein.Workbench.ViewModels flowNodeService.OnRemoveFlowCanvasView += OnRemoveFlowCanvasView; // 移除了画布 this.PropertyChanged += OnPropertyChanged; - } - - private void OnPropertyChanged(object? value, PropertyChangedEventArgs e) { if (this.SelectedTab is null) return; @@ -56,9 +56,13 @@ namespace Serein.Workbench.ViewModels #region 响应环境事件 private void OnCreateFlowCanvasView(FlowCanvasView canvas) { - var model = new FlowEditorTabModel(canvas); - CanvasTabs.Add(model); - SelectedTab = model; + var tab = new FlowEditorTabModel(canvas); + if(canvas is IFlowCanvas flowCanvas) + { + tab.Model = flowCanvas.Model; + } + CanvasTabs.Add(tab); + SelectedTab = tab; } private void OnRemoveFlowCanvasView(FlowCanvasView canvas) { @@ -102,7 +106,7 @@ namespace Serein.Workbench.ViewModels if (tab != null) { tab.IsEditing = false; - if(tab.Name != newName && !string.IsNullOrWhiteSpace(newName)) tab.Name = newName; // 名称合法时设置新名称 + if(tab.Model.Name != newName && !string.IsNullOrWhiteSpace(newName)) tab.Model.Name = newName; // 名称合法时设置新名称 OnPropertyChanged(nameof(CanvasTabs)); // 刷新Tabs集合 } } diff --git a/Workbench/ViewModels/Locator.cs b/Workbench/ViewModels/Locator.cs index eae5f75..322d6f1 100644 --- a/Workbench/ViewModels/Locator.cs +++ b/Workbench/ViewModels/Locator.cs @@ -15,25 +15,18 @@ namespace Serein.Workbench.ViewModels ServiceProvider = serviceProvider; } - //private IServiceProvider GetService() - //{ - // var service = new ServiceCollection(); - // service.AddSingleton(); - // service.AddSingleton(); - // service.AddSingleton(); - // service.AddSingleton(); - // service.AddSingleton(); - // service.AddTransient(); - // return service.BuildServiceProvider(); - //} - + public MainViewModel MainViewModel => App.GetService() ?? throw new NotImplementedException(); public MainMenuBarViewModel MainMenuBarViewModel => App.GetService() ?? throw new NotImplementedException(); public FlowWorkbenchViewModel FlowWorkbenchViewModel => App.GetService() ?? throw new NotImplementedException(); public BaseNodesViewModel BaseNodesViewModel => App.GetService() ?? throw new NotImplementedException(); public FlowLibrarysViewModel FlowLibrarysViewModel => App.GetService() ?? throw new NotImplementedException(); public FlowEditViewModel FlowEditViewModel => App.GetService() ?? throw new NotImplementedException(); + + + public FlowCanvasViewModel FlowCanvasViewModel => App.GetService() ?? throw new NotImplementedException(); + public CanvasInfoViewModel CanvasNodeTreeViewModel => App.GetService() ?? throw new NotImplementedException(); public IServiceProvider ServiceProvider { get; } } diff --git a/Workbench/Views/CanvasInfoView.xaml b/Workbench/Views/CanvasInfoView.xaml new file mode 100644 index 0000000..7b515e4 --- /dev/null +++ b/Workbench/Views/CanvasInfoView.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Workbench/Views/CanvasInfoView.xaml.cs b/Workbench/Views/CanvasInfoView.xaml.cs new file mode 100644 index 0000000..3bf02f0 --- /dev/null +++ b/Workbench/Views/CanvasInfoView.xaml.cs @@ -0,0 +1,32 @@ +using Serein.Workbench.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Serein.Workbench.Views +{ + /// + /// CanvasNodeTreeView.xaml 的交互逻辑 + /// + public partial class CanvasInfoView : UserControl + { + private readonly CanvasInfoViewModel ViewModel; + public CanvasInfoView() + { + this.ViewModel = App.GetService(); + this.DataContext = this.ViewModel; + InitializeComponent(); + } + } +} diff --git a/Workbench/Views/FlowCanvasView.xaml.cs b/Workbench/Views/FlowCanvasView.xaml.cs index 06b6871..2d24c00 100644 --- a/Workbench/Views/FlowCanvasView.xaml.cs +++ b/Workbench/Views/FlowCanvasView.xaml.cs @@ -374,14 +374,14 @@ namespace Serein.Workbench.Views // 准备放置条件表达式控件 if (nodeControl.ViewModel.NodeModel.ControlType == NodeControlType.ExpCondition) { - ConditionRegionControl? conditionRegion = WpfFuncTool.GetParentOfType(hitElement); + /* ConditionRegionControl? conditionRegion = WpfFuncTool.GetParentOfType(hitElement); if (conditionRegion is not null) { targetNodeControl = conditionRegion; //// 如果存在条件区域容器 //conditionRegion.AddCondition(nodeControl); return true; - } + }*/ } else @@ -408,13 +408,24 @@ namespace Serein.Workbench.Views /// private void KeyEventService_OnKeyDown(Key key) { - if (!flowNodeService.CurrentSelectCanvas.Guid.Equals(Guid)) + + if (flowNodeService.CurrentSelectCanvas is null || !flowNodeService.CurrentSelectCanvas.Guid.Equals(Guid)) { return; } + + + if (key == Key.F5) + { + // F5 调试当前流程 + _ = flowEnvironment.StartFlowAsync([Guid]); + } + + if (key == Key.Escape) { + // 退出连线、选取状态 IsControlDragging = false; IsCanvasDragging = false; SelectionRectangle.Visibility = Visibility.Collapsed; @@ -427,16 +438,33 @@ namespace Serein.Workbench.Views if (selectNodeControls.Count > 0 && key == Key.C && (keyEventService.GetKeyState(Key.LeftCtrl) || keyEventService.GetKeyState(Key.RightCtrl))) { var text = flowNodeService.CpoyNodeInfo([.. selectNodeControls.Select(c => c.ViewModel.NodeModel)]); - Clipboard.SetDataObject(text, true); // 复制,持久性设置 - return; + //Clipboard.SetText(text); // 复制,持久性设置 + try + { + Clipboard.SetDataObject(text, true); // 复制,持久性设置 + } + catch + { + Clipboard.SetText(text); + return; + } } - + // 粘贴节点 if (key == Key.V && (keyEventService.GetKeyState(Key.LeftCtrl) || keyEventService.GetKeyState(Key.RightCtrl))) { string clipboardText = Clipboard.GetText(TextDataFormat.Text); // 获取复制的文本 - var jobject = JObject.Parse(clipboardText); - var nodesText = jobject["nodes"]?.ToString(); + string nodesText = ""; + try + { + var jobject = JObject.Parse(clipboardText); + nodesText = jobject["nodes"]?.ToString(); + } + catch (Exception ex) + { + return; + } + if (!string.IsNullOrWhiteSpace(nodesText)) { @@ -596,7 +624,7 @@ namespace Serein.Workbench.Views { NodeControlType nodeControlType = droppedType switch { - Type when typeof(ConditionRegionControl).IsAssignableFrom(droppedType) => NodeControlType.ConditionRegion, // 条件区域 + //Type when typeof(ConditionRegionControl).IsAssignableFrom(droppedType) => NodeControlType.ConditionRegion, // 条件区域 Type when typeof(ConditionNodeControl).IsAssignableFrom(droppedType) => NodeControlType.ExpCondition, Type when typeof(ExpOpNodeControl).IsAssignableFrom(droppedType) => NodeControlType.ExpOp, Type when typeof(GlobalDataControl).IsAssignableFrom(droppedType) => NodeControlType.GlobalData, diff --git a/Workbench/Views/FlowEditView.xaml b/Workbench/Views/FlowEditView.xaml index ea1d1d0..f208415 100644 --- a/Workbench/Views/FlowEditView.xaml +++ b/Workbench/Views/FlowEditView.xaml @@ -4,11 +4,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Serein.Workbench.Views" + xmlns:converters="clr-namespace:Serein.Workbench.Tool.Converters" xmlns:vm="clr-namespace:Serein.Workbench.ViewModels" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance vm:FlowEditViewModel}" - xmlns:converters="clr-namespace:Serein.Workbench.Tool.Converters" Background="#E7F0F6"> @@ -17,19 +17,20 @@ - - @@ -37,10 +38,7 @@ - - - - + diff --git a/Workbench/Views/FlowWorkbenchView.xaml b/Workbench/Views/FlowWorkbenchView.xaml index 75fde79..24ed111 100644 --- a/Workbench/Views/FlowWorkbenchView.xaml +++ b/Workbench/Views/FlowWorkbenchView.xaml @@ -39,6 +39,8 @@ - + + +