| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using Newtonsoft.Json;
- using TeamAAS.FlowEditor.Execution;
- using TeamAAS.FlowEditor.Models;
- using TeamAAS.FlowEditor.ProductModels;
- using TeamAAS.FlowEngine.Execution;
- using TeamAAS.Communication;
- using TeamAAS;
- namespace TeamAAS.FlowEditor
- {
- /// <summary>
- /// 产品与流程文件的持久化管理器。
- /// 一个产品可以包含多个流程文件,当前编辑产品和实际运行产品分别维护,
- /// 以支持编辑态与运行态的切换。
- /// 相关数据模型(ProductMeta / HomeFrameItem / FrameArrangeMode / FlowMeta)
- /// 见 TeamAAS.FlowEditor.ProductModels 命名空间。
- /// </summary>
- public class ProductManager
- {
- private static ProductManager _instance;
- public static ProductManager Instance => _instance ?? (_instance = new ProductManager());
- /// <summary>
- /// 产品列表发生变化(新增/删除/重命名)时触发,供首页等订阅方刷新产品型号下拉。
- /// </summary>
- public event Action ProductListChanged;
- private void RaiseProductListChanged()
- {
- try { ProductListChanged?.Invoke(); } catch { }
- }
- /// <summary>Products 根目录(统一由 PathHelper 管理,跟随 PathHelper.Root)。</summary>
- public static string ProductsPath => PathHelper.ProductsDir;
- /// <summary>流程文件统一后缀(导入/导出与每个流程存储都用它)。</summary>
- public const string FlowExtension = ".aas";
- public string CurrentProductName { get; private set; } = "";
- private string _runProductName = "";
- public string RunProductName
- {
- get => _runProductName;
- private set => _runProductName = value;
- }
- public bool IsEditMode => CurrentProductName != RunProductName;
- /// <summary>当前编辑器外壳(切换产品时登记;FlowRunner 用它判断是否与编辑态共享流程图)</summary>
- public FlowEditorShellViewModel ActiveEditorShell { get; private set; }
- /// <summary>
- /// 编辑器切换产品时要显示的流程列表:始终从磁盘加载编辑副本。
- /// 监控模式(FlowEditorShellViewModel.IsMonitorMode=true)由 Shell 主动挂接
- /// FlowRunner.RunTabs,不再由这里隐式挂接,避免编辑态被运行态覆盖。
- /// </summary>
- private List<FlowTabItem> LoadEditorTabs(string productName)
- {
- return LoadAllFlows(productName);
- }
- public string GetProductPath(string productName) => Path.Combine(ProductsPath, productName);
- public string GetProductMetaPath(string productName) =>
- Path.Combine(ProductsPath, productName, "product.json");
- public ProductMeta LoadProductMeta(string productName)
- {
- try
- {
- var path = GetProductMetaPath(productName);
- if (File.Exists(path))
- {
- var json = File.ReadAllText(path);
- var meta = JsonConvert.DeserializeObject<ProductMeta>(json);
- if (meta != null)
- {
- meta.Name = productName; // 目录名即产品名(防历史 json 里名称不一致)
- meta.MigrateLegacyFrames(); // 旧版画面配置自动迁移为画面列表
- return meta;
- }
- }
- }
- catch { }
- return new ProductMeta { Name = productName };
- }
- public void SaveProductMeta(ProductMeta meta)
- {
- try
- {
- if (string.IsNullOrWhiteSpace(meta.Name)) return;
- var dir = Path.Combine(ProductsPath, meta.Name);
- if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
- var path = GetProductMetaPath(meta.Name);
- // 旧版画面字段已迁移进 HomeFrames,置空避免再次触发迁移
- meta.HomeFrameCount = 0;
- meta.HomeFrameNames = "";
- meta.HomeFrameShowDot = "";
- var json = JsonConvert.SerializeObject(meta, Formatting.Indented);
- File.WriteAllText(path, json);
- }
- catch { }
- }
- public List<string> GetProductList()
- {
- var list = new List<string>();
- try
- {
- if (Directory.Exists(ProductsPath))
- {
- list = Directory.GetDirectories(ProductsPath)
- .Select(Path.GetFileName)
- .OrderBy(n => n)
- .ToList();
- }
- }
- catch { }
- return list;
- }
- public List<ProductMeta> GetProductMetaList()
- {
- var result = new List<ProductMeta>();
- try
- {
- if (Directory.Exists(ProductsPath))
- {
- foreach (var dir in Directory.GetDirectories(ProductsPath))
- {
- var name = Path.GetFileName(dir);
- var meta = LoadProductMeta(name);
- if (meta == null) meta = new ProductMeta { Name = name };
- result.Add(meta);
- }
- result = result.OrderBy(m => m.Sequence).ThenBy(m => m.Name).ToList();
- }
- }
- catch { }
- return result;
- }
- public ProductMeta GetProductBySequence(int sequence)
- {
- return GetProductMetaList().FirstOrDefault(m => m.Sequence == sequence);
- }
- public bool CreateProduct(string productName, int sequence = 0)
- {
- if (string.IsNullOrWhiteSpace(productName)) return false;
- try
- {
- string safeName = SanitizeName(productName);
- string path = Path.Combine(ProductsPath, safeName);
- if (Directory.Exists(path)) return false;
- Directory.CreateDirectory(path);
- var meta = new ProductMeta { Name = safeName, Sequence = sequence };
- SaveProductMeta(meta);
- RaiseProductListChanged();
- return true;
- }
- catch { return false; }
- }
- public bool DeleteProduct(string productName)
- {
- if (string.IsNullOrWhiteSpace(productName)) return false;
- try
- {
- string path = Path.Combine(ProductsPath, productName);
- if (!Directory.Exists(path)) return false;
- Directory.Delete(path, true);
- if (CurrentProductName == productName)
- CurrentProductName = "";
- if (RunProductName == productName)
- RunProductName = "";
- RaiseProductListChanged();
- return true;
- }
- catch { return false; }
- }
- public bool RenameProduct(string oldName, string newName)
- {
- if (string.IsNullOrWhiteSpace(oldName) || string.IsNullOrWhiteSpace(newName)) return false;
- try
- {
- string safeNew = SanitizeName(newName);
- string oldPath = Path.Combine(ProductsPath, oldName);
- string newPath = Path.Combine(ProductsPath, safeNew);
- if (!Directory.Exists(oldPath)) return false;
- if (Directory.Exists(newPath)) return false;
- Directory.Move(oldPath, newPath);
- var oldMeta = LoadProductMeta(oldName);
- if (oldMeta != null)
- {
- oldMeta.Name = safeNew;
- SaveProductMeta(oldMeta);
- }
- if (CurrentProductName == oldName)
- CurrentProductName = safeNew;
- if (RunProductName == oldName)
- RunProductName = safeNew;
- RaiseProductListChanged();
- return true;
- }
- catch { return false; }
- }
- public bool SetProductSequence(string productName, int sequence)
- {
- var meta = LoadProductMeta(productName);
- if (meta == null) return false;
- meta.Sequence = sequence;
- SaveProductMeta(meta);
- return true;
- }
- /// <summary>主页画面配置变化(当前产品的画面数/排列/名称被修改)通知</summary>
- public event Action HomeFrameConfigChanged;
- /// <summary>当前编辑产品的画面配置(读不到产品时返回默认值;不落盘)</summary>
- public ProductMeta GetHomeFrameConfig(string productName)
- {
- var meta = string.IsNullOrEmpty(productName) ? null : LoadProductMeta(productName);
- return meta ?? new ProductMeta { Name = productName ?? "" };
- }
- /// <summary>保存画面配置到产品并广播变化(首页按新配置重建布局)</summary>
- public bool SaveHomeFrameConfig(ProductMeta meta)
- {
- if (meta == null || string.IsNullOrWhiteSpace(meta.Name)) return false;
- SaveProductMeta(meta);
- HomeFrameConfigChanged?.Invoke();
- return true;
- }
- public List<string> GetFlowFiles(string productName)
- {
- var list = new List<string>();
- try
- {
- string path = Path.Combine(ProductsPath, productName);
- if (Directory.Exists(path))
- {
- var flowNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
- foreach (var f in Directory.GetFiles(path, "*" + FlowExtension))
- flowNames.Add(Path.GetFileNameWithoutExtension(f));
- list = flowNames.OrderBy(n => n).ToList();
- }
- }
- catch { }
- return list;
- }
- /// <summary>
- /// 产品流程文件的变更指纹(文件数 + 最新修改时间Ticks),用于判断运行态流程是否需要从磁盘重载,
- /// 避免高频触发时每次都反序列化。涵盖 .aas 流程文件与 flows.meta.json。
- /// </summary>
- public string GetProductFlowStamp(string productName)
- {
- try
- {
- if (string.IsNullOrWhiteSpace(productName)) return "";
- var dir = Path.Combine(ProductsPath, productName);
- if (!Directory.Exists(dir)) return "";
- long maxTicks = 0;
- int count = 0;
- foreach (var f in Directory.GetFiles(dir))
- {
- var name = Path.GetFileName(f);
- var ext = Path.GetExtension(f);
- bool isFlow = ext.Equals(FlowExtension, StringComparison.OrdinalIgnoreCase);
- bool isMeta = name.Equals("flows.meta.json", StringComparison.OrdinalIgnoreCase);
- if (!isFlow && !isMeta) continue;
- count++;
- var t = File.GetLastWriteTimeUtc(f).Ticks;
- if (t > maxTicks) maxTicks = t;
- }
- return count.ToString() + "|" + maxTicks.ToString();
- }
- catch { return ""; }
- }
- public string GetFlowMetaPath(string productName) =>
- Path.Combine(ProductsPath, productName, "flows.meta.json");
- /// <summary>
- /// 把所有流程 Tab 的元数据(触发类型/循环次数/画布尺寸)写入 flows.meta.json。
- /// 在 SaveAllFlows / SaveFlow 末尾调用,保证 Tab 属性变化能持久化。
- /// </summary>
- public void SaveFlowMetaMap(string productName, IEnumerable<FlowTabItem> tabs)
- {
- // 空产品名会让 Path.Combine(ProductsPath, "", "flows.meta.json") 退化为 Products/flows.meta.json
- // (根目录凭空多一份),必须在写文件前拦截。
- if (string.IsNullOrWhiteSpace(productName)) return;
- try
- {
- var dir = Path.Combine(ProductsPath, productName);
- if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
- var list = new List<FlowMeta>();
- foreach (var tab in tabs ?? Enumerable.Empty<FlowTabItem>())
- {
- var meta = new FlowMeta
- {
- Name = tab.Name,
- TriggerType = (int)tab.TriggerType,
- LoopCount = tab.LoopCount,
- CanvasWidth = tab.CanvasWidth,
- CanvasHeight = tab.CanvasHeight,
- LoopIntervalMs = tab.LoopIntervalMs
- };
- list.Add(meta);
- // 调试日志:记录保存时的 TriggerType 值
- AppLogger.Info($"SaveFlowMeta: {tab.Name} TriggerType={tab.TriggerType} (int={(int)tab.TriggerType})", nameof(ProductManager));
- }
- var json = JsonConvert.SerializeObject(list, Formatting.Indented);
- File.WriteAllText(GetFlowMetaPath(productName), json);
- AppLogger.Info($"SaveFlowMeta: wrote {list.Count} entries to {GetFlowMetaPath(productName)}", nameof(ProductManager));
- }
- catch (Exception ex) { AppLogger.Error("SaveFlowMeta 异常", ex, nameof(ProductManager)); }
- }
- /// <summary>
- /// 读取 flows.meta.json,按 Name 索引返回字典。文件不存在返回空字典(兼容老产品目录)。
- /// </summary>
- public Dictionary<string, FlowMeta> LoadFlowMetaMap(string productName)
- {
- var map = new Dictionary<string, FlowMeta>(StringComparer.Ordinal);
- if (string.IsNullOrWhiteSpace(productName)) return map; // 空名会误读 Products 根目录的残留文件
- try
- {
- var path = GetFlowMetaPath(productName);
- if (!File.Exists(path))
- {
- AppLogger.Info($"LoadFlowMeta: 文件不存在 {path}", nameof(ProductManager));
- return map;
- }
- var json = File.ReadAllText(path);
- var list = JsonConvert.DeserializeObject<List<FlowMeta>>(json);
- if (list != null)
- {
- foreach (var m in list)
- {
- if (!string.IsNullOrEmpty(m?.Name))
- {
- map[m.Name] = m;
- AppLogger.Info($"LoadFlowMeta: {m.Name} TriggerType={m.TriggerType} (int={m.TriggerType})", nameof(ProductManager));
- }
- }
- }
- AppLogger.Info($"LoadFlowMeta: loaded {map.Count} entries from {path}", nameof(ProductManager));
- }
- catch (Exception ex) { AppLogger.Error("LoadFlowMeta 异常", ex, nameof(ProductManager)); }
- return map;
- }
- public bool SaveFlow(string productName, FlowTabItem tab)
- {
- if (string.IsNullOrWhiteSpace(productName) || tab == null) return false;
- try
- {
- string dir = Path.Combine(ProductsPath, productName);
- if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
- string filePath = Path.Combine(dir, tab.Name + FlowExtension); // 统一 .aas
- bool ok = tab.EditorVm?.ExportFlow(tab.Name, filePath) ?? false;
- if (ok)
- {
- SaveFlowMetaSingle(productName, tab); // 同步更新单条流程元数据
- }
- return ok;
- }
- catch { return false; }
- }
- /// <summary>
- /// 单条流程保存时更新其元数据:读现有 flows.meta.json,按 Name 替换/添加后写回。
- /// </summary>
- public void SaveFlowMetaSingle(string productName, FlowTabItem tab)
- {
- if (tab == null) return;
- try
- {
- var map = LoadFlowMetaMap(productName);
- map[tab.Name] = new FlowMeta
- {
- Name = tab.Name,
- TriggerType = (int)tab.TriggerType,
- LoopCount = tab.LoopCount,
- CanvasWidth = tab.CanvasWidth,
- CanvasHeight = tab.CanvasHeight,
- LoopIntervalMs = tab.LoopIntervalMs
- };
- var dir = Path.Combine(ProductsPath, productName);
- if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
- var json = JsonConvert.SerializeObject(map.Values.ToList(), Formatting.Indented);
- File.WriteAllText(GetFlowMetaPath(productName), json);
- }
- catch { }
- }
- public bool SaveAllFlows(string productName, IEnumerable<FlowTabItem> tabs)
- {
- if (string.IsNullOrWhiteSpace(productName) || tabs == null) return false;
- try
- {
- string dir = Path.Combine(ProductsPath, productName);
- if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
- var tabNames = new HashSet<string>(tabs.Select(t => t.Name), StringComparer.OrdinalIgnoreCase);
- foreach (var tab in tabs)
- {
- string filePath = Path.Combine(dir, tab.Name + FlowExtension); // 统一 .aas
- tab.EditorVm?.ExportFlow(tab.Name, filePath);
- }
- // 删除磁盘上已不存在于 Tab 列表中的流程文件
- // (例如用户删掉了"流程2"后点保存,切换产品再切回来就不会复活了)
- foreach (var file in Directory.GetFiles(dir, "*" + FlowExtension))
- {
- string name = Path.GetFileNameWithoutExtension(file);
- if (!tabNames.Contains(name))
- {
- try { File.Delete(file); }
- catch { /* 文件被占用等异常忽略,下次保存再清理 */ }
- }
- }
- // 同步写入流程 Tab 元数据(触发类型/循环次数/画布尺寸)
- SaveFlowMetaMap(productName, tabs);
- // 同步写入该产品的全局变量快照(Product 作用域变量值)
- GlobalVariableManager.Instance.SaveSnapshot(GetGlobalVarsPath(productName));
- return true;
- }
- catch { return false; }
- }
- public FlowTabItem LoadFlow(string productName, string flowName)
- => LoadFlow(productName, flowName, null, null);
- public FlowTabItem LoadFlow(string productName, string flowName, FlowMeta meta, ResultRegistry registry = null)
- {
- if (string.IsNullOrWhiteSpace(productName) || string.IsNullOrWhiteSpace(flowName)) return null;
- try
- {
- string filePath = Path.Combine(ProductsPath, productName, flowName + FlowExtension); // 统一 .aas
- if (!File.Exists(filePath)) return null;
- var tab = new FlowTabItem(flowName);
- // 应用磁盘上保存的 Tab 元数据(触发类型/循环次数/画布尺寸/循环间隔);meta 为 null 时用默认值
- if (meta != null)
- {
- tab.TriggerType = (TriggerType)meta.TriggerType;
- tab.LoopCount = meta.LoopCount;
- tab.CanvasWidth = meta.CanvasWidth;
- tab.CanvasHeight = meta.CanvasHeight;
- tab.LoopIntervalMs = meta.LoopIntervalMs;
- }
- FlowGraph result = FlowFileStore.Load(filePath);
- if (result != null && tab.EditorVm != null)
- {
- var graph = tab.EditorVm.Graph;
- // 绑定结果注册表:运行态=Main,编辑/调试态=Debug
- graph.Registry = registry ?? ResultRegistry.Debug;
- graph.Registry.ClearFlow(graph.GraphId);
- result.GraphId = graph.GraphId;
- result.GraphName = graph.GraphName;
- var idMap = new Dictionary<string, string>();
- foreach (var item in result.Nodes)
- {
- string oldNodeId = item.NodeId;
- item.InitializeNode(graph.GraphId, graph.GraphName, graph.Registry);
- if (!string.IsNullOrEmpty(oldNodeId))
- idMap[oldNodeId] = item.NodeId;
- }
- if (result.Connections != null && idMap.Count > 0)
- {
- foreach (var conn in result.Connections)
- {
- if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId))
- conn.SourceNodeId = newSrcId;
- if (idMap.TryGetValue(conn.TargetNodeId, out var newTgtId))
- conn.TargetNodeId = newTgtId;
- }
- }
- graph.Nodes.Clear();
- foreach (var n in result.Nodes)
- graph.Nodes.Add(n);
- graph.Connections.Clear();
- foreach (var c in result.Connections)
- graph.Connections.Add(c);
- tab.EditorVm.GraphDataChanged?.Invoke(graph, null);
- tab.EditorVm?.MarkClean(); // 刚加载的内容视为"已保存"状态
- }
- return tab;
- }
- catch { return null; }
- }
- public List<FlowTabItem> LoadAllFlows(string productName, ResultRegistry registry = null)
- {
- var tabs = new List<FlowTabItem>();
- var flowFiles = GetFlowFiles(productName);
- // 读一次元数据字典,按 Name 应用到对应 Tab(兼容老产品目录:无文件则全用默认值)
- var metaMap = LoadFlowMetaMap(productName);
- foreach (var flowName in flowFiles)
- {
- metaMap.TryGetValue(flowName, out var meta);
- if (meta != null)
- AppLogger.Info($"LoadAllFlows: {flowName} 找到 meta,TriggerType={meta.TriggerType}", nameof(ProductManager));
- else
- AppLogger.Info($"LoadAllFlows: {flowName} 未找到 meta,使用默认值", nameof(ProductManager));
- var tab = LoadFlow(productName, flowName, meta, registry);
- if (tab != null)
- {
- AppLogger.Info($"LoadAllFlows: {flowName} 加载完成,TriggerType={tab.TriggerType}", nameof(ProductManager));
- tabs.Add(tab);
- }
- }
- return tabs;
- }
- public string GetGlobalVarsPath(string productName) =>
- Path.Combine(ProductsPath, productName, "global_vars.json");
- /// <summary>
- /// 当前内存里的全局变量归属哪个产品。加载快照/切换产品时更新。
- /// 内存变量是进程级共享的,编辑态与运行态都可能改它,
- /// 只按 CurrentProductName 存会存错产品(例如主页已切到运行产品但编辑态还停在另一个)。
- /// </summary>
- public string GlobalVarsOwnerProduct { get; private set; } = "";
- /// <summary>
- /// 保存“当前归属产品”的全局变量快照。
- /// 产品切换前、以及设置页手动保存时调用;无归属产品时回退到编辑态当前产品。
- /// </summary>
- public bool SaveGlobalVarsForOwner()
- {
- var owner = !string.IsNullOrEmpty(GlobalVarsOwnerProduct) ? GlobalVarsOwnerProduct : CurrentProductName;
- return SaveGlobalVarsFor(owner);
- }
- /// <summary>
- /// 把 Product 作用域变量保存到指定产品的快照文件(Products\<名>\global_vars.json)。
- /// 产品切换前、以及产品变量弹窗点确定时调用。
- /// </summary>
- public bool SaveGlobalVarsFor(string productName)
- {
- if (string.IsNullOrEmpty(productName)) return false;
- try
- {
- GlobalVariableManager.Instance.SaveSnapshot(GetGlobalVarsPath(productName));
- GlobalVarsOwnerProduct = productName;
- return true;
- }
- catch { return false; }
- }
- /// <summary>
- /// 从目标产品快照加载 Product 作用域变量到 GlobalVariableManager(Global 子集不受影响)。
- /// 快照不存在时清空 Product 子集(切到新产品应从空开始,而非继承上个产品的变量)。
- /// Variables 是 ObservableCollection 绑定 UI,必须在 UI 线程调用。
- /// </summary>
- public bool LoadProductGlobalVars(string productName)
- {
- if (string.IsNullOrEmpty(productName)) return false;
- try
- {
- GlobalVarsOwnerProduct = productName;
- return GlobalVariableManager.Instance.LoadSnapshot(GetGlobalVarsPath(productName));
- }
- catch { return false; }
- }
- /// <summary>
- /// 释放一组 FlowTab 持有的资源:节点插件、结果字典、图节点引用。
- /// 切换产品 / 卸载运行产品 / 清理编辑态时统一调用。
- /// 安全保证:跳过仍被 FlowRunner.RunTabs 持有的运行态 Tab——
- /// 监控模式下编辑器的 FlowTabs 挂的就是这批对象,若在此清空会导致
- /// 切换产品时正在运行的流程画布变空、主页面运行也被打断。
- /// </summary>
- internal static void DisposeFlowTabs(IEnumerable<FlowTabItem> tabs)
- {
- if (tabs == null) return;
- var runTabs = FlowRunner.Instance.RunTabs;
- foreach (var tab in tabs)
- {
- if (tab == null) continue;
- if (runTabs != null && runTabs.Contains(tab)) continue;
- // 1. 释放节点插件(相机句柄、连接流、大对象等)
- if (tab.Graph != null)
- {
- foreach (var node in tab.Graph.Nodes)
- {
- try { node.PluginModel?.Dispose(); }
- catch { /* 单个插件释放失败不影响整体 */ }
- try
- {
- node.ResultItems?.Clear();
- node.ExecutionHistory?.Clear();
- }
- catch { }
- }
- // 2. 清掉该流程绑定的 ResultRegistry 实例中本流程的全部结果
- // (运行态图 → Main,编辑/调试态图 → Debug,互不干扰)
- if (!string.IsNullOrEmpty(tab.Graph.GraphId))
- tab.Graph.Registry?.ClearFlow(tab.Graph.GraphId);
- tab.Graph.Nodes.Clear();
- tab.Graph.Connections.Clear();
- }
- // 3. 清 EditorVm 引用
- if (tab.EditorVm != null)
- {
- try { tab.EditorVm.SelectedNode = null; } catch { }
- }
- }
- // 释放完成后强制全代 GC,确保插件引用的大对象被回收
- GC.Collect(2, GCCollectionMode.Forced, true);
- GC.WaitForPendingFinalizers();
- GC.Collect(2, GCCollectionMode.Forced, true);
- }
- /// <summary>
- /// 编辑模式:切换产品进行编辑(不影响主程序运行)。
- /// 注意:不再自动保存——是否保存由调用方(ProductViewModel)根据脏标记提示用户确认。
- /// </summary>
- public void SwitchProduct(string productName, FlowEditorShellViewModel shellVm)
- {
- if (shellVm == null) return;
- // 切换前:保存当前产品的全局变量快照和流程元数据
- SaveGlobalVarsForOwner();
- SaveFlowMetaMap(CurrentProductName, shellVm.FlowTabs);
- // 释放旧产品的资源(节点插件 Dispose + ResultRegistry 清理 + 图引用解除)
- DisposeFlowTabs(shellVm.FlowTabs);
- // 切换编辑产品:完全清空 Debug(含上一产品各流程与子流程残留),避免跨产品数据堆积
- ResultRegistry.Debug.ClearAllFlows();
- shellVm.FlowTabs.Clear();
- CurrentProductName = productName;
- ActiveEditorShell = shellVm;
- if (!string.IsNullOrEmpty(productName))
- {
- var flows = LoadEditorTabs(productName);
- foreach (var flow in flows)
- {
- shellVm.FlowTabs.Add(flow);
- }
- }
- if (shellVm.FlowTabs.Count == 0)
- {
- var defaultTab = new FlowTabItem("流程1");
- shellVm.FlowTabs.Add(defaultTab);
- }
- shellVm.ActiveTab = shellVm.FlowTabs.FirstOrDefault();
- // 加载目标产品的全局变量快照(Variables 是 ObservableCollection,UI 线程同步修改)
- LoadProductGlobalVars(productName);
- // 切换产品后刷新监控选项可用性 + 自动进入/退出监控(运行产品且运行中→默认监控)
- shellVm.ApplyMonitorAvailability();
- // 强制全代 GC + 等待终结器,确保 LOH 大对象(图像/位图源/CogRecord 等)被回收
- GC.Collect(2, GCCollectionMode.Forced, true);
- GC.WaitForPendingFinalizers();
- GC.Collect(2, GCCollectionMode.Forced, true);
- }
- /// <summary>
- /// 编辑模式:切换产品(异步版)。文件 IO(加载新产品)在后台线程执行,
- /// FlowTabs 等绑定 UI 的集合变更始终留在调用线程(UI 线程)。
- /// 不自动保存;是否保存由调用方在切换前询问用户。
- /// </summary>
- public async System.Threading.Tasks.Task SwitchProductAsync(string productName, FlowEditorShellViewModel shellVm)
- {
- if (shellVm == null) return;
- // 切换前:在 UI 线程保存当前产品全局变量快照和流程元数据
- SaveGlobalVarsForOwner();
- SaveFlowMetaMap(CurrentProductName, shellVm.FlowTabs);
- // 释放旧产品的资源(节点插件 Dispose + ResultRegistry 清理 + 图引用解除)
- DisposeFlowTabs(shellVm.FlowTabs);
- // 切换编辑产品:完全清空 Debug(含上一产品各流程与子流程残留),避免跨产品数据堆积
- ResultRegistry.Debug.ClearAllFlows();
- shellVm.FlowTabs.Clear();
- // 后台线程:解析目标产品流程(始终读盘,监控挂接由 Shell 主动控制)
- List<FlowTabItem> flows = null;
- await System.Threading.Tasks.Task.Run(() =>
- {
- if (!string.IsNullOrEmpty(productName))
- flows = LoadEditorTabs(productName);
- });
- // 调用线程(UI):应用集合变更
- shellVm.FlowTabs.Clear();
- CurrentProductName = productName;
- ActiveEditorShell = shellVm;
- if (flows != null)
- {
- foreach (var flow in flows)
- shellVm.FlowTabs.Add(flow);
- }
- if (shellVm.FlowTabs.Count == 0)
- shellVm.FlowTabs.Add(new FlowTabItem("流程1"));
- shellVm.ActiveTab = shellVm.FlowTabs.FirstOrDefault();
- // UI 线程:加载目标产品全局变量快照
- LoadProductGlobalVars(productName);
- // 切换产品后刷新监控选项可用性 + 自动进入/退出监控(运行产品且运行中→默认监控)
- shellVm.ApplyMonitorAvailability();
- // 强制全代 GC + 等待终结器,确保 LOH 大对象(图像/位图源/CogRecord 等)被回收
- GC.Collect(2, GCCollectionMode.Forced, true);
- GC.WaitForPendingFinalizers();
- GC.Collect(2, GCCollectionMode.Forced, true);
- }
- /// <summary>
- /// 运行模式:加载产品作为当前运行程序(从编辑显式加载)
- /// </summary>
- public void LoadProductToRun(string productName)
- {
- // 切换前先把上一个归属产品的全局变量落盘:
- // 设置页改的值只存在内存里,不先存就会随产品切换丢失
- if (!string.IsNullOrEmpty(GlobalVarsOwnerProduct) && GlobalVarsOwnerProduct != productName)
- SaveGlobalVarsForOwner();
- RunProductName = productName;
- // 主页加载运行产品:同步加载该产品的全局变量快照(与编辑模式一致)
- LoadProductGlobalVars(productName);
- }
- /// <summary>
- /// 基于序号切换产品(供外部调用,如 PLC 触发)。
- /// 产品切换经 LoadRunProduct → LoadProductGlobalVars 自动载入该产品的 Product 变量,
- /// 故此处无需再单独切换全局变量配方(配方机制已退休)。
- /// </summary>
- public bool SwitchProductBySequence(int sequence)
- {
- var meta = GetProductBySequence(sequence);
- if (meta == null) return false;
- RunProductName = meta.Name;
- // 加载为运行产品(FlowRunner 会复制一份运行态流程,不影响编辑态)
- TeamAAS.FlowEngine.Execution.FlowRunner.Instance.LoadRunProduct(meta.Name);
- return true;
- }
- private static string SanitizeName(string name)
- => TeamAAS.PathHelper.SanitizeFileName(name);
- }
- }
|