ScriptToolView.xaml.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.IO;
  5. using System.Threading;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Media;
  9. using CodeForge;
  10. using TeamAAS.FlowEditor.Models;
  11. using TeamAAS.FlowEditor.Plugins;
  12. using Plugins.Script.Models;
  13. using Plugins.Script.Core;
  14. namespace Plugins.Script.Views
  15. {
  16. /// <summary>错误/警告列表项(ListBox 绑定)。</summary>
  17. public class DiagnosticItem
  18. {
  19. public string SeverityText { get; set; }
  20. public Brush SeverityBrush { get; set; }
  21. public string Display { get; set; }
  22. public int Line { get; set; }
  23. public int Column { get; set; }
  24. public bool IsWarning { get; set; }
  25. }
  26. /// <summary>
  27. /// C# 脚本节点的自定义编辑视图(承载在 PluginViewDialog,DataContext 由宿主注入为 ScriptToolModel)。
  28. /// 中部是 CodeForge 完整代码编辑器 <see cref="CodeEditorControl"/>:自由写命名空间/多个类/方法,
  29. /// 智能补全、编译诊断波浪线、代码配色、鸟瞰图全部可用;顶部「引用」按钮打开程序集引用管理(添加/移除 DLL)。
  30. /// 用户代码是一个完整插件类(继承 BasePlugin&lt;ScriptToolModel&gt;,PluginRun/DeclareOutputs 预写好)。
  31. /// - IPluginViewSave:确定时把代码回写模型。
  32. /// </summary>
  33. public partial class ScriptToolView : UserControl, IPluginViewSave, IPluginViewTip
  34. {
  35. static ScriptToolView()
  36. {
  37. // XAML 里用到 CodeForge 控件,按 CodeForge 说明书在解析前确保其内嵌依赖解析器已注册(幂等)
  38. try { CodeForgeRuntime.Initialize(); } catch { /* 模块初始化器通常已自动注册 */ }
  39. }
  40. private readonly ObservableCollection<DiagnosticItem> _diagnostics = new ObservableCollection<DiagnosticItem>();
  41. private ScriptToolModel _model;
  42. private ScriptToolModel Model { get { return _model; } }
  43. private bool _initialized;
  44. private bool _refsAdded;
  45. public ScriptToolView()
  46. {
  47. InitializeComponent();
  48. DataContextChanged += (s, e) => _model = e.NewValue as ScriptToolModel;
  49. ErrorListBox.ItemsSource = _diagnostics;
  50. Editor.DiagnosticMessage += (s, msg) => Dispatcher.BeginInvoke(new Action(() => AppendOutput(msg)));
  51. Editor.CompileRequested += (s, e) => Dispatcher.BeginInvoke(new Action(CompileCode));
  52. Loaded += OnLoaded;
  53. Unloaded += OnUnloaded;
  54. }
  55. private void OnLoaded(object sender, RoutedEventArgs e)
  56. {
  57. if (_model != null)
  58. {
  59. TxtTitle.Text = string.IsNullOrWhiteSpace(_model.NodeName) ? "C#脚本" : _model.NodeName;
  60. Editor.Code = string.IsNullOrEmpty(_model.Code) ? ScriptToolModel.DefaultCode : _model.Code;
  61. }
  62. else
  63. {
  64. Editor.Code = ScriptToolModel.DefaultCode;
  65. }
  66. if (!_refsAdded)
  67. {
  68. // 批量添加引用:BeginBatch 作用域内挂起 ReferencesChanged,Dispose 时只触发一次。
  69. // 否则每加一个引用都会重启一次 Roslyn 预热(N 个引用=N 次并发预热),打开脚本编辑器会卡顿数秒。
  70. using (Editor.Engine.References.BeginBatch())
  71. AddPlatformReferences();
  72. _refsAdded = true;
  73. }
  74. try { var _ = Editor.WarmupRoslynAsync(); } catch { }
  75. _initialized = true;
  76. RefreshErrorCount();
  77. AppendOutput("就绪 · 顶部「引用」=程序集引用管理 · 编译(F5)/运行 · 入口=继承 BasePlugin<ScriptToolModel> 的类(默认 MyScript)");
  78. }
  79. private void OnUnloaded(object sender, RoutedEventArgs e)
  80. {
  81. try { Editor.Dispose(); } catch { }
  82. }
  83. /// <summary>给编辑器补全/编译环境加入平台程序集引用(TeamAAS 全家桶 + 本插件 + 用户归档的 References\)。</summary>
  84. private void AddPlatformReferences()
  85. {
  86. try { Editor.AddReference(typeof(ScriptToolModel).Assembly); } catch { }
  87. var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  88. foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
  89. {
  90. try
  91. {
  92. if (asm.IsDynamic) continue;
  93. var name = asm.GetName().Name ?? "";
  94. if (!name.StartsWith("TeamAAS", StringComparison.OrdinalIgnoreCase)) continue;
  95. var loc = asm.Location;
  96. if (string.IsNullOrEmpty(loc) || !File.Exists(loc)) continue;
  97. if (!seen.Add(loc)) continue;
  98. Editor.AddReferenceFromFile(loc);
  99. }
  100. catch { /* 忽略单个引用失败 */ }
  101. }
  102. // 用户 DLL 归档目录:根目录 References\(CodeForge 归档约定处)+ Runtime\References\(历史 PostBuild 搬移遗留位置)
  103. var refDirs = new[]
  104. {
  105. Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "References"),
  106. Path.Combine(TeamAAS.PathHelper.RuntimeDir, "References"),
  107. };
  108. foreach (var dir in refDirs)
  109. {
  110. try
  111. {
  112. if (!Directory.Exists(dir)) continue;
  113. foreach (var dll in Directory.GetFiles(dir, "*.dll"))
  114. {
  115. try { Editor.AddReferenceFromFile(dll); } catch { }
  116. }
  117. }
  118. catch { }
  119. }
  120. }
  121. #region 工具栏事件
  122. private void BtnRefs_Click(object sender, RoutedEventArgs e)
  123. {
  124. var owner = Window.GetWindow(this);
  125. var dlg = new ScriptReferenceDialog(Editor, owner);
  126. if (owner != null)
  127. {
  128. dlg.Owner = owner;
  129. dlg.WindowStartupLocation = WindowStartupLocation.CenterOwner;
  130. }
  131. dlg.ShowDialog();
  132. }
  133. private void BtnCompile_Click(object sender, RoutedEventArgs e) { CompileCode(); }
  134. private void BtnRun_Click(object sender, RoutedEventArgs e) { RunScript(); }
  135. private void BtnClearOutput_Click(object sender, RoutedEventArgs e) { OutputTextBox.Text = ""; }
  136. #endregion
  137. #region 编译 / 运行
  138. private void CompileCode()
  139. {
  140. ClearDiagnostics();
  141. AppendOutput("======== 编译 " + DateTime.Now.ToString("HH:mm:ss") + " ========");
  142. try
  143. {
  144. var c = Editor.Compile();
  145. Editor.ShowCompileDiagnostics(c);
  146. if (c != null)
  147. {
  148. bool hasSpan = c.Spans != null && c.Spans.Count > 0;
  149. if (hasSpan)
  150. {
  151. foreach (var s in c.Spans) AddDiagnostic(s.IsWarning, s.Message, s.Line, s.Column);
  152. }
  153. else if (c.Errors != null)
  154. {
  155. foreach (var er in c.Errors) AddDiagnostic(false, er, 0, 0);
  156. }
  157. if (c.Warnings != null) foreach (var w in c.Warnings) AppendOutput(" [警告] " + w);
  158. AppendOutput(c.Success
  159. ? " ✅ 编译成功(" + (c.AssemblyBytes != null ? c.AssemblyBytes.Length : 0) + " bytes)"
  160. : " ❌ 编译失败");
  161. }
  162. }
  163. catch (Exception ex)
  164. {
  165. AppendOutput(" 💥 编译异常:" + ex.Message);
  166. AddDiagnostic(false, ex.Message, 0, 0);
  167. }
  168. RefreshErrorCount();
  169. }
  170. private void RunScript()
  171. {
  172. ClearDiagnostics();
  173. AppendOutput("======== 运行 " + DateTime.Now.ToString("HH:mm:ss") + " ========");
  174. AppendOutput("(设计期:Service/CoreManager 可用;上游流程变量无数据;Log 写入平台日志)");
  175. var tempModel = new ScriptToolModel { Code = Editor.Code ?? "" };
  176. SetDesignTimeNames(tempModel);
  177. List<string> errs, warns;
  178. BasePlugin<ScriptToolModel> script;
  179. try
  180. {
  181. script = ScriptCompiler.CreateScript(Editor.Code ?? "", tempModel,
  182. TeamAAS.Global.ServiceRegistry.Default, null, out errs, out warns);
  183. }
  184. catch (Exception ex) { AppendOutput("💥 编译异常:" + ex.Message); return; }
  185. if (script == null)
  186. {
  187. if (errs != null) foreach (var er in errs) { AppendOutput(" [错误] " + er); AddDiagnostic(false, er, 0, 0); }
  188. RefreshErrorCount();
  189. AppendOutput(" ❌ 编译失败,未运行");
  190. return;
  191. }
  192. if (warns != null) foreach (var w in warns) AppendOutput(" [警告] " + w);
  193. Dictionary<string, object> results = null;
  194. NodeRunStatus status = NodeRunStatus.Failed;
  195. var oldOut = Console.Out;
  196. var oldErr = Console.Error;
  197. var sw = new StringWriter();
  198. try
  199. {
  200. Console.SetOut(sw);
  201. Console.SetError(sw);
  202. status = script.PluginRun(CancellationToken.None, out results);
  203. }
  204. catch (Exception ex) { AppendOutput("💥 运行异常:" + ex); }
  205. finally { Console.SetOut(oldOut); Console.SetError(oldErr); }
  206. var cout = sw.ToString();
  207. if (!string.IsNullOrEmpty(cout)) AppendOutput(cout.TrimEnd());
  208. AppendOutput(" 状态 = " + status);
  209. if (results != null)
  210. foreach (var kv in results) AppendOutput(" " + kv.Key + " = " + (kv.Value ?? "null"));
  211. }
  212. /// <summary>设计期临时模型的 FlowName/NodeName 是 internal set,反射赋值以免脚本 Log 时取到 null。</summary>
  213. private static void SetDesignTimeNames(ScriptToolModel m)
  214. {
  215. SetProp(m, "FlowName", "脚本测试");
  216. SetProp(m, "NodeName", "设计期运行");
  217. }
  218. private static void SetProp(object obj, string propName, object value)
  219. {
  220. try
  221. {
  222. var p = typeof(TeamAAS.FlowEditor.Plugins.BasePluginModel).GetProperty(propName);
  223. if (p == null) return;
  224. var setter = p.GetSetMethod(true);
  225. if (setter != null) setter.Invoke(obj, new object[] { value });
  226. }
  227. catch { /* 设计期辅助,失败忽略 */ }
  228. }
  229. #endregion
  230. #region 错误列表
  231. private void ClearDiagnostics() { _diagnostics.Clear(); }
  232. private void AddDiagnostic(bool isWarning, string message, int line, int column)
  233. {
  234. _diagnostics.Add(new DiagnosticItem
  235. {
  236. IsWarning = isWarning,
  237. SeverityText = isWarning ? "警告" : "错误",
  238. SeverityBrush = isWarning ? Brushes.Goldenrod : Brushes.IndianRed,
  239. Line = Math.Max(1, line),
  240. Column = Math.Max(1, column),
  241. Display = (line > 0 ? "L" + line + ",C" + column + " " : "") + message
  242. });
  243. }
  244. private void RefreshErrorCount()
  245. {
  246. int err = 0, warn = 0;
  247. foreach (var d in _diagnostics) { if (d.IsWarning) warn++; else err++; }
  248. ErrorCount.Text = "(" + err + " 错误 / " + warn + " 警告)";
  249. }
  250. private void ErrorListBox_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
  251. {
  252. try
  253. {
  254. var it = ErrorListBox.SelectedItem as DiagnosticItem;
  255. if (it == null || it.Line < 1) return;
  256. Editor.ScrollToLine(it.Line);
  257. }
  258. catch { /* 跳转失败忽略 */ }
  259. }
  260. #endregion
  261. #region IPluginViewSave
  262. public void SaveChanges()
  263. {
  264. if (_model == null) return;
  265. _model.Code = Editor.Code ?? "";
  266. }
  267. #endregion
  268. #region IPluginViewTip
  269. private TeamAAS.Dialogs.Dialogs.PluginViewDialog HostDialog
  270. {
  271. get { return Window.GetWindow(this) as TeamAAS.Dialogs.Dialogs.PluginViewDialog; }
  272. }
  273. public void ShowTip(string message) { var d = HostDialog; if (d != null) d.ShowTip(message); }
  274. public void ShowHoverHint(string message) { var d = HostDialog; if (d != null) d.ShowHoverHint(message); }
  275. #endregion
  276. private void AppendOutput(string text)
  277. {
  278. OutputTextBox.AppendText((text ?? "") + Environment.NewLine);
  279. OutputTextBox.ScrollToEnd();
  280. }
  281. }
  282. }