From f5893778a386267a0484a2628814bbb53bc45028 Mon Sep 17 00:00:00 2001 From: GG Z <903524121@qq.com> Date: Sat, 28 Feb 2026 21:04:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MSAddinTest.sln | 51 +++ MSAddinTest/App.config | 6 + MSAddinTest/App.xaml | 8 + MSAddinTest/App.xaml.cs | 17 + MSAddinTest/Core/Command/CommandBase.cs | 31 ++ MSAddinTest/Core/Command/ICommandInvoker.cs | 16 + MSAddinTest/Core/Command/IPluginCommand.cs | 15 + MSAddinTest/Core/Command/InstallCommand.cs | 39 ++ MSAddinTest/Core/Command/LoadPluginCommand.cs | 85 +++++ .../Command/LoadPluginsWhenStartupCommand.cs | 37 ++ .../Core/Command/ReloadPluginCommand.cs | 32 ++ MSAddinTest/Core/Command/RunPluginCommand.cs | 54 +++ MSAddinTest/Core/Command/UninstallCommand.cs | 40 ++ .../Core/Command/UnloadPluginCommand.cs | 37 ++ .../Core/Command/UpdateSettingsCommand.cs | 87 +++++ MSAddinTest/Core/Executor/AddinExecutor.cs | 23 ++ MSAddinTest/Core/Executor/ClassExecutor.cs | 51 +++ MSAddinTest/Core/Executor/ExecutorBase.cs | 76 ++++ .../Core/Executor/StaticMethodExecutor.cs | 59 +++ MSAddinTest/Core/FuncResult.cs | 60 +++ MSAddinTest/Core/Loader/AutoReloader.cs | 80 ++++ MSAddinTest/Core/Loader/LoaderSetup.cs | 46 +++ .../Core/Loader/PluginAssemblyLoader.cs | 243 ++++++++++++ .../Loader/PluginAssemblyLoader_Events.cs | 71 ++++ .../Loader/PluginAssemblyLoader_Methods.cs | 49 +++ MSAddinTest/Core/Message/MessageManager.cs | 27 ++ MSAddinTest/Core/PluginDomainContainer.cs | 30 ++ MSAddinTest/Core/PluginDomainLoader.cs | 108 ++++++ MSAddinTest/Core/PluginManager.cs | 50 +++ MSAddinTest/Core/Settings/PluginSetting.cs | 182 +++++++++ MSAddinTest/Core/Settings/UpdateSettingArg.cs | 47 +++ .../Core/Settings/defaultSettings.json | 10 + MSAddinTest/Core/StatusCode.cs | 19 + MSAddinTest/Interfaces/IMSTest.cs | 17 + MSAddinTest/Interfaces/ITestClass.cs | 21 ++ MSAddinTest/Interfaces/ITestStaticMethod.cs | 15 + MSAddinTest/Interfaces/MSTestAttribute.cs | 34 ++ MSAddinTest/Interfaces/TestAddin.cs | 47 +++ MSAddinTest/MSAddinTest.csproj | 54 +++ MSAddinTest/MSAddinTest.nuspec | 17 + MSAddinTest/MainWindow.xaml | 52 +++ MSAddinTest/MainWindow.xaml.cs | 63 ++++ MSAddinTest/Properties/AssemblyInfo.cs | 55 +++ MSAddinTest/Properties/Resources.Designer.cs | 63 ++++ MSAddinTest/Properties/Resources.resx | 117 ++++++ MSAddinTest/Properties/Settings.Designer.cs | 26 ++ MSAddinTest/Properties/Settings.settings | 7 + MSAddinTest/Startup/Commands.xml | 30 ++ MSAddinTest/Startup/KeyinFuncs.cs | 76 ++++ MSAddinTest/Startup/MSAddin.cs | 39 ++ MSAddinTest/Utils/Ex_Json.cs | 31 ++ MSAddinTest/Utils/FileHelper.cs | 29 ++ MSUtils/MSUtils.csproj | 66 ++++ MSUtils/Properties/AssemblyInfo.cs | 36 ++ MSUtils/TestClass.cs | 20 + Readme.md | 353 ++++++++++++++++++ TestAddinPlugin/App.config | 6 + TestAddinPlugin/Properties/AssemblyInfo.cs | 36 ++ TestAddinPlugin/TestAddin/Commands.xml | 20 + TestAddinPlugin/TestAddin/PluginAddin.cs | 36 ++ TestAddinPlugin/TestAddin/PluginKeyinFuncs.cs | 24 ++ TestAddinPlugin/TestAddinPlugin.csproj | 58 +++ TestAddinPlugin/TestClassExecutor.cs | 22 ++ TestAddinPlugin/TestStaticMethodExecutor.cs | 57 +++ 64 files changed, 3313 insertions(+) create mode 100644 MSAddinTest.sln create mode 100644 MSAddinTest/App.config create mode 100644 MSAddinTest/App.xaml create mode 100644 MSAddinTest/App.xaml.cs create mode 100644 MSAddinTest/Core/Command/CommandBase.cs create mode 100644 MSAddinTest/Core/Command/ICommandInvoker.cs create mode 100644 MSAddinTest/Core/Command/IPluginCommand.cs create mode 100644 MSAddinTest/Core/Command/InstallCommand.cs create mode 100644 MSAddinTest/Core/Command/LoadPluginCommand.cs create mode 100644 MSAddinTest/Core/Command/LoadPluginsWhenStartupCommand.cs create mode 100644 MSAddinTest/Core/Command/ReloadPluginCommand.cs create mode 100644 MSAddinTest/Core/Command/RunPluginCommand.cs create mode 100644 MSAddinTest/Core/Command/UninstallCommand.cs create mode 100644 MSAddinTest/Core/Command/UnloadPluginCommand.cs create mode 100644 MSAddinTest/Core/Command/UpdateSettingsCommand.cs create mode 100644 MSAddinTest/Core/Executor/AddinExecutor.cs create mode 100644 MSAddinTest/Core/Executor/ClassExecutor.cs create mode 100644 MSAddinTest/Core/Executor/ExecutorBase.cs create mode 100644 MSAddinTest/Core/Executor/StaticMethodExecutor.cs create mode 100644 MSAddinTest/Core/FuncResult.cs create mode 100644 MSAddinTest/Core/Loader/AutoReloader.cs create mode 100644 MSAddinTest/Core/Loader/LoaderSetup.cs create mode 100644 MSAddinTest/Core/Loader/PluginAssemblyLoader.cs create mode 100644 MSAddinTest/Core/Loader/PluginAssemblyLoader_Events.cs create mode 100644 MSAddinTest/Core/Loader/PluginAssemblyLoader_Methods.cs create mode 100644 MSAddinTest/Core/Message/MessageManager.cs create mode 100644 MSAddinTest/Core/PluginDomainContainer.cs create mode 100644 MSAddinTest/Core/PluginDomainLoader.cs create mode 100644 MSAddinTest/Core/PluginManager.cs create mode 100644 MSAddinTest/Core/Settings/PluginSetting.cs create mode 100644 MSAddinTest/Core/Settings/UpdateSettingArg.cs create mode 100644 MSAddinTest/Core/Settings/defaultSettings.json create mode 100644 MSAddinTest/Core/StatusCode.cs create mode 100644 MSAddinTest/Interfaces/IMSTest.cs create mode 100644 MSAddinTest/Interfaces/ITestClass.cs create mode 100644 MSAddinTest/Interfaces/ITestStaticMethod.cs create mode 100644 MSAddinTest/Interfaces/MSTestAttribute.cs create mode 100644 MSAddinTest/Interfaces/TestAddin.cs create mode 100644 MSAddinTest/MSAddinTest.csproj create mode 100644 MSAddinTest/MSAddinTest.nuspec create mode 100644 MSAddinTest/MainWindow.xaml create mode 100644 MSAddinTest/MainWindow.xaml.cs create mode 100644 MSAddinTest/Properties/AssemblyInfo.cs create mode 100644 MSAddinTest/Properties/Resources.Designer.cs create mode 100644 MSAddinTest/Properties/Resources.resx create mode 100644 MSAddinTest/Properties/Settings.Designer.cs create mode 100644 MSAddinTest/Properties/Settings.settings create mode 100644 MSAddinTest/Startup/Commands.xml create mode 100644 MSAddinTest/Startup/KeyinFuncs.cs create mode 100644 MSAddinTest/Startup/MSAddin.cs create mode 100644 MSAddinTest/Utils/Ex_Json.cs create mode 100644 MSAddinTest/Utils/FileHelper.cs create mode 100644 MSUtils/MSUtils.csproj create mode 100644 MSUtils/Properties/AssemblyInfo.cs create mode 100644 MSUtils/TestClass.cs create mode 100644 Readme.md create mode 100644 TestAddinPlugin/App.config create mode 100644 TestAddinPlugin/Properties/AssemblyInfo.cs create mode 100644 TestAddinPlugin/TestAddin/Commands.xml create mode 100644 TestAddinPlugin/TestAddin/PluginAddin.cs create mode 100644 TestAddinPlugin/TestAddin/PluginKeyinFuncs.cs create mode 100644 TestAddinPlugin/TestAddinPlugin.csproj create mode 100644 TestAddinPlugin/TestClassExecutor.cs create mode 100644 TestAddinPlugin/TestStaticMethodExecutor.cs diff --git a/MSAddinTest.sln b/MSAddinTest.sln new file mode 100644 index 0000000..2b8df81 --- /dev/null +++ b/MSAddinTest.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSAddinTest", "MSAddinTest\MSAddinTest.csproj", "{21D13145-F4B6-41D7-9DDD-0D9E4C128C09}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAddinPlugin", "TestAddinPlugin\TestAddinPlugin.csproj", "{013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSUtils", "MSUtils\MSUtils.csproj", "{E687EF99-6F1E-4255-A31F-F235257A9391}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Debug|x64.ActiveCfg = Debug|x64 + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Debug|x64.Build.0 = Debug|x64 + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Release|Any CPU.Build.0 = Release|Any CPU + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Release|x64.ActiveCfg = Release|x64 + {21D13145-F4B6-41D7-9DDD-0D9E4C128C09}.Release|x64.Build.0 = Release|x64 + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Debug|x64.ActiveCfg = Debug|x64 + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Debug|x64.Build.0 = Debug|x64 + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Release|Any CPU.Build.0 = Release|Any CPU + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Release|x64.ActiveCfg = Release|x64 + {013A0D0A-5C3F-4510-ACC0-CCB8C86CC4FC}.Release|x64.Build.0 = Release|x64 + {E687EF99-6F1E-4255-A31F-F235257A9391}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E687EF99-6F1E-4255-A31F-F235257A9391}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E687EF99-6F1E-4255-A31F-F235257A9391}.Debug|x64.ActiveCfg = Debug|x64 + {E687EF99-6F1E-4255-A31F-F235257A9391}.Debug|x64.Build.0 = Debug|x64 + {E687EF99-6F1E-4255-A31F-F235257A9391}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E687EF99-6F1E-4255-A31F-F235257A9391}.Release|Any CPU.Build.0 = Release|Any CPU + {E687EF99-6F1E-4255-A31F-F235257A9391}.Release|x64.ActiveCfg = Release|x64 + {E687EF99-6F1E-4255-A31F-F235257A9391}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {71F5818C-C2CB-4579-801C-8AF3A2E5E13D} + EndGlobalSection +EndGlobal diff --git a/MSAddinTest/App.config b/MSAddinTest/App.config new file mode 100644 index 0000000..4bfa005 --- /dev/null +++ b/MSAddinTest/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/MSAddinTest/App.xaml b/MSAddinTest/App.xaml new file mode 100644 index 0000000..ea3e790 --- /dev/null +++ b/MSAddinTest/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/MSAddinTest/App.xaml.cs b/MSAddinTest/App.xaml.cs new file mode 100644 index 0000000..3035c76 --- /dev/null +++ b/MSAddinTest/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace MSAddinTest +{ + /// + /// App.xaml 的交互逻辑 + /// + public partial class App : Application + { + } +} diff --git a/MSAddinTest/Core/Command/CommandBase.cs b/MSAddinTest/Core/Command/CommandBase.cs new file mode 100644 index 0000000..d3457e9 --- /dev/null +++ b/MSAddinTest/Core/Command/CommandBase.cs @@ -0,0 +1,31 @@ +using MSAddinTest.Core.Settings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + /// + /// 将 init 的通用代码抽象到父类中 + /// + internal abstract class CommandBase: IPluginCommand + { + protected PluginDomainContainer PluginContainer { get; private set; } + protected PluginSetting PluginSetting { get; private set; } + + /// + /// 供 PluginManager 初始化 Command 使用 + /// + /// + /// + public void Init(PluginDomainContainer pluginDomains, PluginSetting pluginSetting) + { + PluginContainer = pluginDomains; + PluginSetting = pluginSetting; + } + + public abstract FuncResult Start(); + } +} diff --git a/MSAddinTest/Core/Command/ICommandInvoker.cs b/MSAddinTest/Core/Command/ICommandInvoker.cs new file mode 100644 index 0000000..67167b3 --- /dev/null +++ b/MSAddinTest/Core/Command/ICommandInvoker.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + /// + /// 命令执行接口 + /// + internal interface ICommandInvoker + { + FuncResult InvokeCommand(IPluginCommand command); + } +} diff --git a/MSAddinTest/Core/Command/IPluginCommand.cs b/MSAddinTest/Core/Command/IPluginCommand.cs new file mode 100644 index 0000000..37bf0fa --- /dev/null +++ b/MSAddinTest/Core/Command/IPluginCommand.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + internal interface IPluginCommand + { + void Init(PluginDomainContainer pluginDomains, Settings.PluginSetting pluginSetting); + + FuncResult Start(); + } +} diff --git a/MSAddinTest/Core/Command/InstallCommand.cs b/MSAddinTest/Core/Command/InstallCommand.cs new file mode 100644 index 0000000..40759e8 --- /dev/null +++ b/MSAddinTest/Core/Command/InstallCommand.cs @@ -0,0 +1,39 @@ +using Bentley.DgnPlatformNET; +using Bentley.MstnPlatformNET; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + internal class InstallCommand : CommandBase + { + public override FuncResult Start() + { + // 安装 + // 修改自动加载 + string personalConfPath = Path.Combine(ConfigurationManager.GetVariable("_USTN_HOMEROOT"), "prefs\\Personal.ucf"); + + if (!File.Exists(personalConfPath)) return new FuncResult(false, "用户配置文件不存在"); + + // 读取值并修改 + string configContent = File.ReadAllText(personalConfPath); + //string autoloadSentence = "\r\n%level Organization\r\nMS_DGNAPPS > MSAddinTest"; + string autoloadSentence = "\r\nMS_DGNAPPS > MSAddinTest"; + if (!configContent.Contains(autoloadSentence)) + { + // 添加语句使其自动加载 + StreamWriter configWriter = new StreamWriter(personalConfPath, true); + configWriter.Write(autoloadSentence); + configWriter.Close(); + } + + MessageCenter.Instance.ShowInfoMessage("成功设置 MSAddinTest 自动加载!", "", false); + + return new FuncResult(true); + } + } +} diff --git a/MSAddinTest/Core/Command/LoadPluginCommand.cs b/MSAddinTest/Core/Command/LoadPluginCommand.cs new file mode 100644 index 0000000..654c5b4 --- /dev/null +++ b/MSAddinTest/Core/Command/LoadPluginCommand.cs @@ -0,0 +1,85 @@ +using Bentley.MstnPlatformNET; +using Microsoft.Win32; +using MSAddinTest.Core.Loader; +using MSAddinTest.Core.Settings; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace MSAddinTest.Core.Command +{ + /// + /// 加载插件 + /// + internal class LoadPluginCommand : CommandBase + { + // 执行命令 + public override FuncResult Start() + { + // 打开选择窗体 + OpenFileDialog openFileDialog = new OpenFileDialog + { + Filter = "类库文件|*.dll", + Multiselect = true, + }; + var result = openFileDialog.ShowDialog(); + if (result == null || !(bool)result) return new FuncResult(false) + { + StatusCode = StatusCode.Failed + }; + + var dllPaths = openFileDialog.FileNames; + + foreach (var dllPath in dllPaths) + { + string pluginName = Path.GetFileNameWithoutExtension(dllPath); + + if (PluginContainer.TryGetValue(pluginName,out var existLoader)) + { + // 重载插件 + existLoader.Reload(); + MessageCenter.Instance.ShowInfoMessage($"找到相同插件 {existLoader.Setup.PluginName},重载成功!", "", false); + continue; + } + + var setup = new LoaderSetup() + { + PluginName = pluginName, + DllFullPath = dllPath, + PluginSetting = PluginSetting, + }; + + var loader = new PluginAssemblyLoader(setup); + var loaderRes = loader.LoadAssembly(); + if (loaderRes.NotOk) + { + MessageBox.Show(loaderRes.Message); + return loaderRes; + } + PluginContainer.Add(loader); + + AssemblyLoaded(loader); + + // 输出提示 + MessageCenter.Instance.ShowInfoMessage($"{setup.PluginName} 插件加载成功!", "", false); + } + + return new FuncResult(true); + } + + /// + /// 保存到文件记录中 + /// + /// + protected virtual void AssemblyLoaded(PluginAssemblyLoader loader) + { + // 加载成功后,将加载记录添加到本地设置中,方便下次调用 + PluginSetting.AddPlugin(loader.Setup.PluginName, loader.Setup.DllFullPath, true); + PluginSetting.Save(); + } + } +} diff --git a/MSAddinTest/Core/Command/LoadPluginsWhenStartupCommand.cs b/MSAddinTest/Core/Command/LoadPluginsWhenStartupCommand.cs new file mode 100644 index 0000000..61afa13 --- /dev/null +++ b/MSAddinTest/Core/Command/LoadPluginsWhenStartupCommand.cs @@ -0,0 +1,37 @@ +using Bentley.MstnPlatformNET; +using MSAddinTest.Core.Loader; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + /// + /// 初始化时加载插件 + /// + internal class LoadPluginsWhenStartupCommand : CommandBase + { + public override FuncResult Start() + { + // 读取所有插件需要自动加载的插件 + var loaderSetups = PluginSetting.GetAutoLoadLoaderSetups(); + foreach (var setup in loaderSetups) + { + var loader = new PluginAssemblyLoader(setup); + var loaderRes = loader.LoadAssembly(); + if (loaderRes.NotOk) + { + continue; + } + + PluginContainer.Add(loader); + MessageCenter.Instance.ShowInfoMessage($"{loader.Setup.PluginName} 已自动加载!", "", false); + } + + return new FuncResult(true); + } + } +} diff --git a/MSAddinTest/Core/Command/ReloadPluginCommand.cs b/MSAddinTest/Core/Command/ReloadPluginCommand.cs new file mode 100644 index 0000000..b2c60ee --- /dev/null +++ b/MSAddinTest/Core/Command/ReloadPluginCommand.cs @@ -0,0 +1,32 @@ +using Bentley.MstnPlatformNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + /// + /// 重新加载插件 + /// + internal class ReloadPluginCommand : CommandBase + { + private string _pluginName; + public ReloadPluginCommand(string pluginName) + { + _pluginName = pluginName; + } + + public override FuncResult Start() + { + if (PluginContainer.TryGetValue(_pluginName, out var container)) + { + container.Reload(); + MessageCenter.Instance.ShowInfoMessage($"{_pluginName} 重载成功!", "", false); + } + + return new FuncResult(true); + } + } +} diff --git a/MSAddinTest/Core/Command/RunPluginCommand.cs b/MSAddinTest/Core/Command/RunPluginCommand.cs new file mode 100644 index 0000000..7ee0fc4 --- /dev/null +++ b/MSAddinTest/Core/Command/RunPluginCommand.cs @@ -0,0 +1,54 @@ +using System; +using System.Windows; + +namespace MSAddinTest.Core.Command +{ + /// + /// 执行插件 + /// + internal class RunPluginCommand : CommandBase + { + private string _executorName; + + /// + /// 用户端初始化时,传入执行器的名称和执行器的初始化参数 + /// + /// + /// + public RunPluginCommand(string excutorName) + { + _executorName = excutorName; + } + + /// + /// 特性控制可以跨程序集捕获异常 + /// + /// + //[HandleProcessCorruptedStateExceptions,SecurityCritical] + public override FuncResult Start() + { + int executorsCount = 0; + + // 全部包裹在 TryCatch中 + try + { + foreach (var kv in PluginContainer) + { + var result = kv.Value.Execute(_executorName); + if (result.Data is int count) executorsCount += count; + } + } + catch (Exception ex) + { + Message.MessageManager.ShowException(ex); + } + + if (executorsCount == 0) + { + MessageBox.Show($"未找到名为:{_executorName} 的执行器"); + } + + return new FuncResult(executorsCount > 0); + } + } +} diff --git a/MSAddinTest/Core/Command/UninstallCommand.cs b/MSAddinTest/Core/Command/UninstallCommand.cs new file mode 100644 index 0000000..91bee27 --- /dev/null +++ b/MSAddinTest/Core/Command/UninstallCommand.cs @@ -0,0 +1,40 @@ +using Bentley.DgnPlatformNET; +using Bentley.MstnPlatformNET; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + internal class UninstallCommand : CommandBase + { + public override FuncResult Start() + { + // 卸载 + // 修改自动加载 + string personalConfPath = Path.Combine(ConfigurationManager.GetVariable("_USTN_HOMEROOT"), "prefs\\Personal.ucf"); + + if (!File.Exists(personalConfPath)) return new FuncResult(false, "用户配置文件不存在"); + + // 读取值并修改 + string configContent = File.ReadAllText(personalConfPath); + string autoloadSentence = "\r\nMS_DGNAPPS > MSAddinTest"; + if (configContent.Contains(autoloadSentence)) + { + configContent = configContent.Replace(autoloadSentence, ""); + // 重新保存配置文件 + // 添加语句使其自动加载 + StreamWriter configWriter = new StreamWriter(personalConfPath, false,Encoding.UTF8); + configWriter.Write(configContent); + configWriter.Close(); + } + + MessageCenter.Instance.ShowInfoMessage("成功取消 MSAddinTest 自动加载!", "", false); + + return new FuncResult(true); + } + } +} diff --git a/MSAddinTest/Core/Command/UnloadPluginCommand.cs b/MSAddinTest/Core/Command/UnloadPluginCommand.cs new file mode 100644 index 0000000..0c816c7 --- /dev/null +++ b/MSAddinTest/Core/Command/UnloadPluginCommand.cs @@ -0,0 +1,37 @@ +using Bentley.MstnPlatformNET; +using MSAddinTest.Core.Settings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + /// + /// 卸载插件 + /// + internal class UnloadPluginCommand : CommandBase + { + public UnloadPluginCommand(string pluginName) + { + _pluginNameToUnload = pluginName; + } + /// + /// 待卸载的插件名称 + /// + private readonly string _pluginNameToUnload; + + public override FuncResult Start() + { + // 移除插件域,让 GC 回收 + PluginContainer.Remove(_pluginNameToUnload); + + // 从存储中去掉记录 + PluginSetting.RemovePluginSetting(_pluginNameToUnload); + + MessageCenter.Instance.ShowInfoMessage($"{_pluginNameToUnload} 已卸载!", "", false); + return new FuncResult(true); + } + } +} diff --git a/MSAddinTest/Core/Command/UpdateSettingsCommand.cs b/MSAddinTest/Core/Command/UpdateSettingsCommand.cs new file mode 100644 index 0000000..338d566 --- /dev/null +++ b/MSAddinTest/Core/Command/UpdateSettingsCommand.cs @@ -0,0 +1,87 @@ +using Bentley.MstnPlatformNET; +using MSAddinTest.Core.Settings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Command +{ + /// + /// 更新设置 + /// + internal class UpdateSettingsCommand : CommandBase + { + // 允许更新的字段名称 + private List _allowUpdatingFields = new List() + { + "autoLoad","autoReload" + }; + + private List _updateSettings; + /// + /// 传入更新字符串
+ /// 格式为 plugin1Name.field1Name = value,field2Name=value,plugin2Name.field3Name=value + ///
+ /// + public UpdateSettingsCommand(string updateString) + { + // 将字符串解析为对象 + _updateSettings = new List(); + + // 清除等号两侧的空格 + var regex = new Regex(@"\s*=+\s*"); + updateString = regex.Replace(updateString, "="); + var settings = updateString.Split(','); + foreach (var setting in settings) + { + var args = setting.Split('='); + if (args.Length != 2) continue; + + var names = args[0].Split('.'); + if (names.Length == 0) continue; + + if (names.Length == 1 && _updateSettings.Count == 0) continue; + + if (names.Length == 1) + { + _updateSettings.Add(new UpdateSettingArg() + { + Name = _updateSettings.Last().Name, + FieldName = args[0], + Value = args[1] + }); + } + else if (names.Length == 2) + { + _updateSettings.Add(new UpdateSettingArg() + { + Name = names[0], + FieldName = names[1], + Value = args[1], + }); + } + } + } + + public override FuncResult Start() + { + if (_updateSettings.Count < 1) return new FuncResult(false); + _updateSettings = _updateSettings.FindAll(x => _allowUpdatingFields.Contains(x.FieldName)); + + // 开始进行设置 + foreach(var setting in _updateSettings) + { + PluginSetting.UpdatePluginSetting(setting); + } + + // 保存设置 + PluginSetting.Save(); + + MessageCenter.Instance.ShowInfoMessage("设置已保存!", "", false); + return new FuncResult(true); + } + } +} diff --git a/MSAddinTest/Core/Executor/AddinExecutor.cs b/MSAddinTest/Core/Executor/AddinExecutor.cs new file mode 100644 index 0000000..6f0997e --- /dev/null +++ b/MSAddinTest/Core/Executor/AddinExecutor.cs @@ -0,0 +1,23 @@ +using System; +using System.Reflection; + +namespace MSAddinTest.Core.Executor +{ + internal class AddinExecutor : StaticMethodExecutor + { + public override int Priority { get; } = 100; + + public AddinExecutor(Type type, string methodName) : base(type, methodName) + { + Description = "Keyin"; + } + + public override void Execute(string arg) + { + if (MethodInfo == null) return; + + // 调用静态方法 + MethodInfo.Invoke(null, new object[] { arg }); + } + } +} diff --git a/MSAddinTest/Core/Executor/ClassExecutor.cs b/MSAddinTest/Core/Executor/ClassExecutor.cs new file mode 100644 index 0000000..30d7e5b --- /dev/null +++ b/MSAddinTest/Core/Executor/ClassExecutor.cs @@ -0,0 +1,51 @@ +using MSAddinTest.Interfaces; +using System; +using System.Linq; +using System.Reflection; + +namespace MSAddinTest.Core.Executor +{ + /// + /// 类执行器 + /// + internal class ClassExecutor : ExecutorBase + { + public ClassExecutor(Type type) : base(type) + { + // 获取Plugin特定 + var pluginAttr = type.GetCustomAttributes(typeof(MSTestAttribute)).FirstOrDefault(); + if (pluginAttr is MSTestAttribute attr) + { + Names.Add(attr.Name); + Description = attr.Description; + } + else + { + // 如果没有特性,就使用类名 + Names.Add(type.Name); + } + } + + public override void Execute(string arg) + { + var instance = Activator.CreateInstance(Type, BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance | BindingFlags.NonPublic, null, new object[] { }, null); + if (instance is ITestClass plugin) + { + plugin.Execute(arg); + } + } + + /// + /// 对于类执行器,与静态执行器不冲突 + /// 只要不是类执行器,都返回false + /// + /// + /// + public override bool IsSame(ExecutorBase executor) + { + if (!(executor is ClassExecutor)) return false; + + return base.IsSame(executor); + } + } +} diff --git a/MSAddinTest/Core/Executor/ExecutorBase.cs b/MSAddinTest/Core/Executor/ExecutorBase.cs new file mode 100644 index 0000000..6532f37 --- /dev/null +++ b/MSAddinTest/Core/Executor/ExecutorBase.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MSAddinTest.Core.Executor +{ + /// + /// 主程序可以获取数据 + /// + public abstract class ExecutorBase : MarshalByRefObject + { + /// + /// 优先级 + /// 如果重名,则取优先级高的执行器 + /// + public virtual int Priority { get; } + + public ExecutorBase(Type type) + { + Type = type; + } + + /// + /// 类型 + /// + protected Type Type { get; set; } + + /// + /// 执行器名称 + /// 通过类的特性来获取,如果没有特性,就为类型名称/静态方法名称 + /// 名称可以有多个,对于 addin,包含 keyin 和特性定义的名称 + /// + public List Names { get; set; } = new List(); + + /// + /// 描述 + /// + public string Description { get; set; } + + public abstract void Execute(string arg); + + /// + /// 通过输入字符串匹配当前执行器 + /// + /// 用户输入的字符串 + /// + /// + /// + public bool IsMatch(string inputStr, out string name, out string args) + { + args = inputStr; + name = Names.Find(x => inputStr.ToLower().StartsWith(x.ToLower())); + if (name == null) return false; + + // 生成参数 + args = inputStr.Substring(inputStr.ToLower().IndexOf(name.ToLower()) + name.Length).Trim(); + + return true; + } + + /// + /// 是否是相同命令 + /// 只要有一个名称相同,就说明是同一个命令 + /// + /// + /// + public virtual bool IsSame(ExecutorBase executor) + { + if (executor == null) return false; + + if (executor.Type.FullName != Type.FullName) return false; + + return Names.Intersect(executor.Names).Count() > 0; + } + } +} diff --git a/MSAddinTest/Core/Executor/StaticMethodExecutor.cs b/MSAddinTest/Core/Executor/StaticMethodExecutor.cs new file mode 100644 index 0000000..a5e4ada --- /dev/null +++ b/MSAddinTest/Core/Executor/StaticMethodExecutor.cs @@ -0,0 +1,59 @@ +using MSAddinTest.Interfaces; +using System; +using System.Linq; +using System.Reflection; + +namespace MSAddinTest.Core.Executor +{ + /// + /// 静态方法执行器 + /// 传入此类的方法必须是绑定了特性的 + /// + internal class StaticMethodExecutor : ExecutorBase + { + public override int Priority { get; } = 50; + + protected MethodInfo MethodInfo { get; private set; } + public StaticMethodExecutor(MethodInfo methodInfo) : base(null) + { + SetMthodInfo(methodInfo); + } + + public StaticMethodExecutor(Type type, string methodName) : base(type) + { + // 获取 MethodInfo + var methodInfo = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public) ?? throw new NullReferenceException($"无法在 {type} 中找到静态方法 {methodName}"); + + SetMthodInfo(methodInfo); + } + + /// + /// 在此处解析方法上的特性 + /// 如果有特性,用特性中的值覆盖 + /// + /// + private void SetMthodInfo(MethodInfo methodInfo) + { + MethodInfo = methodInfo; + + // 保存类型 + Type = MethodInfo.DeclaringType; + + // 获取静态方法上面的特性 + var attribute = methodInfo.GetCustomAttributes(typeof(MSTestAttribute)).FirstOrDefault(); + if (attribute is MSTestAttribute attr) + { + Names.Add(attr.Name); + Description = attr.Description; + } + } + + public override void Execute(string arg) + { + if (MethodInfo == null) return; + + // 调用静态方法 + MethodInfo.Invoke(null, new object[] { arg }); + } + } +} diff --git a/MSAddinTest/Core/FuncResult.cs b/MSAddinTest/Core/FuncResult.cs new file mode 100644 index 0000000..8efb2be --- /dev/null +++ b/MSAddinTest/Core/FuncResult.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core +{ + public class FuncResult + { + /// + /// 默认为 false + /// + public FuncResult() + { + + } + + public FuncResult(bool ok):this() + { + Ok= ok; + } + + public FuncResult(bool ok,string message):this(ok) + { + Message = message; + } + + /// + /// 返回正确 + /// + public bool Ok { get; set; } + + /// + /// 返回错误 + /// + public bool NotOk => !Ok; + + /// + /// 消息 + /// + public string Message { get; set; } + + /// + /// 状态码 + /// + public StatusCode StatusCode { get; set; }= StatusCode.Success; + + /// + /// 数据 + /// + public object Data { get; set; } + + // 与 bool 类型的隐式转换 + public static implicit operator bool(FuncResult result) + { + return result.Ok; + } + } +} diff --git a/MSAddinTest/Core/Loader/AutoReloader.cs b/MSAddinTest/Core/Loader/AutoReloader.cs new file mode 100644 index 0000000..d0cfdbc --- /dev/null +++ b/MSAddinTest/Core/Loader/AutoReloader.cs @@ -0,0 +1,80 @@ +using Bentley.MstnPlatformNET; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Timers; +using System.Windows; + +namespace MSAddinTest.Core.Loader +{ + /// + /// 自动重新加载 + /// + internal class AutoReloader + { + private readonly FileSystemWatcher _watcher; + private readonly PluginAssemblyLoader _assemblyLoader; + private readonly Timer _reloadTimer = new Timer(1000) + { + Enabled = false, + AutoReset = true + }; + + public AutoReloader(PluginAssemblyLoader assemblyLoader) + { + _assemblyLoader = assemblyLoader; + + _watcher = new FileSystemWatcher(Path.GetDirectoryName(assemblyLoader.Setup.DllFullPath)) + { + Filter = "*.dll", + NotifyFilter = NotifyFilters.LastWrite + }; + _watcher.Changed += Watcher_Changed; + _reloadTimer.Elapsed += ReloadTimer_Elapsed; + + // 从设置中获取是否自动重载 + var setting = _assemblyLoader.Setup.PluginSetting; + _watcher.EnableRaisingEvents = setting.IsAutoReload(_assemblyLoader.Setup.PluginName); + + // 监听数据变化 + setting.SettingChanged += (arg) => + { + if (arg.FieldName == "autoReload" && bool.TryParse(arg.Value, out bool result)) + { + _watcher.EnableRaisingEvents = result; + } + }; + + } + + private void Watcher_Changed(object sender, FileSystemEventArgs e) + { + // 只有当前文件才触发 + if (e.FullPath != _assemblyLoader.Setup.DllFullPath) + { + return; + } + + // 多次触发时,重置计时器 + _reloadTimer.Stop(); + _reloadTimer.Start(); + } + + private void ReloadTimer_Elapsed(object sender, ElapsedEventArgs e) + { + _reloadTimer.Stop(); + // 事件是在另一个线程触发的,直接重载会报错 + // 切换到主线程执行 + Application.Current.Dispatcher.Invoke(() => + { + // 文件更改后,需要重新加载 + _assemblyLoader.Reload(); + + }); + } + } +} diff --git a/MSAddinTest/Core/Loader/LoaderSetup.cs b/MSAddinTest/Core/Loader/LoaderSetup.cs new file mode 100644 index 0000000..15ad00c --- /dev/null +++ b/MSAddinTest/Core/Loader/LoaderSetup.cs @@ -0,0 +1,46 @@ +using Bentley.DgnPlatformNET; +using MSAddinTest.Core.Settings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Loader +{ + /// + /// 插件域的设置 + /// 配置参考:https://docs.microsoft.com/en-us/dotnet/api/system.appdomainsetup?view=netframework-4.8 + /// + public class LoaderSetup + { + /// + /// 插件名称 + /// + public string PluginName { get; set; } + + /// + /// Dll 全路径 + /// + private string _dllFullPath; + public string DllFullPath + { + get => _dllFullPath; + set + { + _dllFullPath = value; + BaseDirectory = System.IO.Path.GetDirectoryName(_dllFullPath); + } + } + + /// + /// 当前程序集根目录 + /// + public string BaseDirectory { get; private set; } + + /// + /// 插件设置 + /// + public PluginSetting PluginSetting { get; set; } + } +} diff --git a/MSAddinTest/Core/Loader/PluginAssemblyLoader.cs b/MSAddinTest/Core/Loader/PluginAssemblyLoader.cs new file mode 100644 index 0000000..11a8f0a --- /dev/null +++ b/MSAddinTest/Core/Loader/PluginAssemblyLoader.cs @@ -0,0 +1,243 @@ +using Bentley.MstnPlatformNET; +using MSAddinTest.Core.Executor; +using MSAddinTest.Interfaces; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace MSAddinTest.Core.Loader +{ + /// + /// 程序集动态加载 + /// + public partial class PluginAssemblyLoader + { + public LoaderSetup Setup { get; private set; } + + /// + /// 初始化 + /// + /// + public PluginAssemblyLoader(LoaderSetup pluginDomainSetup) + { + Setup = pluginDomainSetup; + + // 监听事件 + RegistryEvents(); + + // 获取所有 dll 文件 + _allFileNames = Directory.GetFiles(Setup.BaseDirectory, "*.dll", SearchOption.AllDirectories).ToList(); + _allFileNames.AddRange(Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.AllDirectories).ToList()); + + _autoReloader = new AutoReloader(this); + } + + // 自动重新加载 + private readonly AutoReloader _autoReloader; + + // 所有根目录下的 dll 文件 + private readonly List _allFileNames; + + // 从程序集中读取的执行器 + private readonly List _executors = new List(); + + private Assembly _currentAssembly; + public FuncResult LoadAssembly() + { + try + { + // 判断文件是否存在 + if (!File.Exists(Setup.DllFullPath)) + { + return new FuncResult(false, "文件不存在"); + } + + // 验证文件 hash 值 + //var newFileHash = FileHelper.GetFileHash(Setup.DllFullPath); + //if (_lastFileHash == newFileHash) + // return new FuncResult(false, "文件未改变"); + //else + // _lastFileHash = newFileHash; + + // 执行卸载逻辑 + _msAddins.ForEach(x => x.NotifyOnUnloaded(new AddIn.UnloadedEventArgs(AddIn.UnloadReasons.ExitByOtherApp))); + _msAddins.Clear(); + + // 读取文件然后加载 + byte[] bytes = File.ReadAllBytes(Setup.DllFullPath); + _currentAssembly = Assembly.Load(bytes); + + _executors.Clear(); + BuilderExecutors(_currentAssembly); + + return new FuncResult(true); + } + catch (Exception ex) + { + return new FuncResult(false, ex.Message + ex.StackTrace); + } + } + + /// + /// 生成执行器 + /// + /// + protected virtual IEnumerable BuilderExecutors(Assembly assembly) + { + var results = new List(); + + // 类执行器 + AddExecutors(GenerateClassExecutor(assembly)); + + // 静态方法执行器 + AddExecutors(GenerateStaticMethodExecutor(assembly)); + + // 添加 addin 执行器 + AddExecutors(GenerateAddinExecutor(assembly)); + + return results; + } + + private void AddExecutors(IEnumerable executors) + { + foreach (var executor in executors) + { + var executorTemp = _executors.Find(x => x.IsSame(executor)); + // 如果没找到或者优先级比原来高,则添加 + if (executorTemp == null || executorTemp.Priority < executor.Priority) + { + if (executorTemp != null) _executors.Remove(executorTemp); + _executors.Add(executor); + } + } + } + + // 获取类执行器 + private IEnumerable GenerateClassExecutor(Assembly assembly) + { + List results = new List(); + + // 生成运行数据 + var iPluginType = typeof(ITestClass); + var pluginTypes = assembly.GetTypes().Where(x => !x.IsInterface && !x.IsAbstract && iPluginType.IsAssignableFrom(x)); + // 获取非 addin 插件 + var commonPluginTypes = pluginTypes; + foreach (var pluginType in commonPluginTypes) + { + var classExecutor = new ClassExecutor(pluginType); + results.Add(classExecutor); + } + + return results; + } + + // 读取静态执行器 + // 条件要求: + // 1-继承接口 IMSTest_StaticMethod + // 2-具有 MSTestAttribute 属性 + // 3-有一个 string 类型的参数 + private IEnumerable GenerateStaticMethodExecutor(Assembly assembly) + { + List results = new List(); + + // 生成运行数据 + var iPluginType = typeof(ITestStaticMethod); + var pluginTypes = assembly.GetTypes().Where(x => !x.IsInterface && !x.IsAbstract && iPluginType.IsAssignableFrom(x)); + // 获取静态方法执行器 + { + var commonPluginTypes = pluginTypes; + foreach (var pluginType in commonPluginTypes) + { + var methodInfos = pluginType.GetMethods().Where(x => x.GetCustomAttribute(typeof(MSTestAttribute)) != null); + foreach (var methodInfo in methodInfos) + { + // 获取参数 + var paraInfos = methodInfo.GetParameters(); + if (paraInfos.Length != 1 || !typeof(string).IsAssignableFrom(paraInfos[0].ParameterType)) + { + MessageCenter.Instance.ShowErrorMessage($"静态方法 {methodInfo.Name} 的参数个数必须有且只有一个 string 参数", "", false); + continue; + }; + + var classExecutor = new StaticMethodExecutor(methodInfo); + + results.Add(classExecutor); + } + } + } + + return results; + } + + private readonly List _msAddins = new List(); + // 读取addin执行器 + private IEnumerable GenerateAddinExecutor(Assembly assembly) + { + // 生成运行数据 + var iPluginType = typeof(TestAddin); + var allTypes = assembly.GetTypes(); + var pluginTypes = allTypes.Where(x => x.IsSubclassOf(iPluginType)); + + foreach (var pluginType in pluginTypes) + { + // 找到后立即进行初始化 + try + { + var addin = Activator.CreateInstance(pluginType, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, + new object[] { IntPtr.Zero }, CultureInfo.CurrentCulture) as TestAddin; + // 完成后调用 run + // 在此处调用初始化内容 + addin.Init(addin); + + _msAddins.Add(addin); + } + catch (Exception ex) + { + Message.MessageManager.ShowException(ex, "Addin 初始化失败:"); + } + } + + // 开始读取命令表 + var resourceNames = assembly.GetManifestResourceNames().Where(x => x.EndsWith(".xml")); + + var results = new List(); + + // 读取所有的命令表 + foreach (var resourceName in resourceNames) + { + var xmlStream = assembly.GetManifestResourceStream(resourceName); + var xDoc = XElement.Load(xmlStream); + XNamespace ns = "http://www.bentley.com/schemas/1.0/MicroStation/AddIn/KeyinTree.xsd"; + var childerens = xDoc.Descendants(ns + "KeyinHandler"); + // 获取属性 + foreach (XElement xElement in childerens) + { + var keyin = xElement.Attribute("Keyin").Value; + var function = xElement.Attribute("Function").Value; + + // 通过这两个参数生成执行器 + var lastIndex = function.LastIndexOf("."); + var fullTypeName = function.Substring(0, lastIndex); + var functionName = function.Substring(lastIndex + 1); + + var functionType = allTypes.FirstOrDefault(x => x.FullName == fullTypeName); + if (functionType == null) continue; + + var addinExecutor = new AddinExecutor(functionType, functionName); + addinExecutor.Names.Add(keyin); + + results.Add(addinExecutor); + } + } + + // 通过命令表查到静态方法 + return results; + } + } +} diff --git a/MSAddinTest/Core/Loader/PluginAssemblyLoader_Events.cs b/MSAddinTest/Core/Loader/PluginAssemblyLoader_Events.cs new file mode 100644 index 0000000..eda9e7b --- /dev/null +++ b/MSAddinTest/Core/Loader/PluginAssemblyLoader_Events.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace MSAddinTest.Core.Loader +{ + /// + /// 加载器的事件 + /// + public partial class PluginAssemblyLoader + { + private void RegistryEvents() + { + var appDomain = AppDomain.CurrentDomain; + appDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + appDomain.TypeResolve += AppDomain_TypeResolve; + appDomain.UnhandledException += CurrentDomain_UnhandledException; + appDomain.FirstChanceException += AppDomain_FirstChanceException; + } + + private void AppDomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) + { + // Message.MessageManager.ShowException(e.Exception); + } + + #region 加载需要的程序集 + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + MessageBox.Show(e.ExceptionObject.ToString()); + } + + // 某个类型未找到时,去加载类型 + private Assembly AppDomain_TypeResolve(object sender, ResolveEventArgs args) + { + return CurrentDomain_AssemblyResolve(sender, args); + } + + private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + // 获取最新版本的程序集 + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + string assemblyName = args.Name.Split(',')[0].Trim(); + Assembly targetAssembly = assemblies.LastOrDefault(x => x.FullName.Contains(assemblyName)); + + if (targetAssembly != null) return targetAssembly; + + // 找到文件,然后加载 + string assemblyFileName = $"{assemblyName}.dll"; + string fileName = _allFileNames.Find(x => x.EndsWith(assemblyFileName)); + if (fileName == null) return null; + + byte[] bytes = File.ReadAllBytes(fileName); + var assembly = Assembly.Load(bytes); + + // 判断版本是否一致,不一致返回 null,交给其它订阅处理 + if (assembly != null) + { + if (assembly.GetName().FullName == args.Name) return assembly; + return null; + } + + return assembly; + } + #endregion + } +} diff --git a/MSAddinTest/Core/Loader/PluginAssemblyLoader_Methods.cs b/MSAddinTest/Core/Loader/PluginAssemblyLoader_Methods.cs new file mode 100644 index 0000000..28f5dc7 --- /dev/null +++ b/MSAddinTest/Core/Loader/PluginAssemblyLoader_Methods.cs @@ -0,0 +1,49 @@ +using Bentley.MstnPlatformNET; + +namespace MSAddinTest.Core.Loader +{ + public partial class PluginAssemblyLoader + { + /// + /// 实例化执行对象并执行 + /// + /// + /// + public FuncResult Execute(string nameWithParams) + { + // 使用正则表达式将多个空格转换成一个空格 + nameWithParams = System.Text.RegularExpressions.Regex.Replace(nameWithParams, @"\s+", " "); + var nameTemp = nameWithParams.Trim(); + // 对名称进行匹配 + // 如果是 keyin ,通过匹配前缀是否为 keyin 来确定 + // 名称不区分大小写 + var executors = _executors.FindAll(x => x.IsMatch(nameTemp, out _, out _)); + + foreach (var executor in executors) + { + // 获取参数 + executor.IsMatch(nameTemp, out var executorName, out var strArg); + executor.Execute(strArg); + } + + return new FuncResult(true) + { + Data = executors.Count, + }; + } + + + /// + /// 重新加载 + /// + public void Reload() + { + var reloadResult = LoadAssembly(); + if (reloadResult) + { + // 提示成功 + MessageCenter.Instance.ShowInfoMessage($"{Setup.PluginName} 插件重载成功!", "", false); + } + } + } +} diff --git a/MSAddinTest/Core/Message/MessageManager.cs b/MSAddinTest/Core/Message/MessageManager.cs new file mode 100644 index 0000000..40f9070 --- /dev/null +++ b/MSAddinTest/Core/Message/MessageManager.cs @@ -0,0 +1,27 @@ +using Bentley.MstnPlatformNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace MSAddinTest.Core.Message +{ + internal class MessageManager + { + /// + /// 显示异常错误 + /// + /// + /// + public static void ShowException(Exception exception, string titlePrefix = "") + { + if (exception.InnerException != null) exception = exception.InnerException; + + var briefMessage = $"{titlePrefix}{exception.Message}"; + MessageBox.Show(briefMessage); + MessageCenter.Instance.ShowErrorMessage(briefMessage, exception.StackTrace, false); + } + } +} diff --git a/MSAddinTest/Core/PluginDomainContainer.cs b/MSAddinTest/Core/PluginDomainContainer.cs new file mode 100644 index 0000000..6207a21 --- /dev/null +++ b/MSAddinTest/Core/PluginDomainContainer.cs @@ -0,0 +1,30 @@ +using MSAddinTest.Core.Loader; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core +{ + /// + /// 保存 + /// + internal class PluginDomainContainer : Dictionary + { + public new PluginAssemblyLoader this[string name] + { + get + { + if (TryGetValue(name, out PluginAssemblyLoader loader)) return loader; + + return null; + } + } + + public void Add(PluginAssemblyLoader domainLoader) + { + Add(domainLoader.Setup.PluginName, domainLoader); + } + } +} diff --git a/MSAddinTest/Core/PluginDomainLoader.cs b/MSAddinTest/Core/PluginDomainLoader.cs new file mode 100644 index 0000000..5286311 --- /dev/null +++ b/MSAddinTest/Core/PluginDomainLoader.cs @@ -0,0 +1,108 @@ +using MSAddinTest.PluginInterface; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace MSAddinTest.Core.DomainLoader +{ + /// + /// 程序集动态加载 + /// + public class PluginDomainLoader + { + public PluginDomainSetup PluginDomainSetup { get; private set; } + + // 新建的程序域 + private AppDomain _appDomain; + + // 程序集加载器 + private RemoteLoader _remoteLoader; + + /// + /// 初始化程序域 + /// + /// + public PluginDomainLoader(PluginDomainSetup pluginDomainSetup) + { + PluginDomainSetup = pluginDomainSetup; + } + + /// + /// 在程序域中加载程序集 + /// + /// + public bool LoadAssembly() + { + var currentDomainBaseDir = AppDomain.CurrentDomain.BaseDirectory; + // 配置参考:https://docs.microsoft.com/en-us/dotnet/api/system.appdomainsetup?view=netframework-4.8 + AppDomainSetup setup = new AppDomainSetup + { + ApplicationName = PluginDomainSetup.ApplicationName, + ApplicationBase = PluginDomainSetup.ApplicationBase, + PrivateBinPath = PluginDomainSetup.PrivateBinPath, + CachePath = PluginDomainSetup.CachePath, + ShadowCopyFiles = "true", + ShadowCopyDirectories = currentDomainBaseDir, + }; + + // 设置原因参考:https://www.cnblogs.com/changrulin/p/4762816.html + AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles = "true"; + + try + { + _appDomain = AppDomain.CreateDomain(PluginDomainSetup.ApplicationName, null, setup); + var name = Assembly.GetExecutingAssembly().GetName().FullName; + + _remoteLoader = (RemoteLoader)_appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName); + _remoteLoader.SetAssemblyResolver(AppDomain.CurrentDomain.BaseDirectory); + _remoteLoader.LoadAssembly(PluginDomainSetup.DllFullPath); + + return true; + } + catch (Exception e) + { + return false; + } + } + + /// + /// 卸载应用程序域 + /// + public void Unload() + { + if (_appDomain == null) return; + AppDomain.Unload(_appDomain); + _appDomain = null; + _remoteLoader = null; + + // 删除程序生成的文件 + Directory.Delete(Path.Combine(PluginDomainSetup.CachePath, PluginDomainSetup.ApplicationName), true); + } + + /// + /// 重新加载 + /// + public void Reload() + { + // 先卸载程序 + Unload(); + + // 重新加载程序 + LoadAssembly(); + } + + #region remoteLoader中介 + /// + /// 执行类型方法 + /// + /// + /// + /// + public object Execute(string name, PluginArg arg) + { + return _remoteLoader.Execute(name, arg); + } + #endregion + } +} diff --git a/MSAddinTest/Core/PluginManager.cs b/MSAddinTest/Core/PluginManager.cs new file mode 100644 index 0000000..d0d802c --- /dev/null +++ b/MSAddinTest/Core/PluginManager.cs @@ -0,0 +1,50 @@ +using MSAddinTest.Core.Command; +using MSAddinTest.Core.Settings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core +{ + /// + /// 插件管理器 + /// + internal class PluginManager: ICommandInvoker + { + public static PluginManager Manager { get; private set; } + + private object _args; + + /// + /// 插件设置 + /// + private PluginSetting _pluginSetting; + + private PluginDomainContainer _pluginDomains = new PluginDomainContainer(); + + public PluginManager(object args) + { + Manager = this; + _args = args; + + // 读取设置 + _pluginSetting = new PluginSetting(_args); + } + + #region 插件相关 + /// + /// 调用命令 + /// + /// + /// + public FuncResult InvokeCommand(IPluginCommand command) + { + // 调用插件 + command.Init(_pluginDomains, _pluginSetting); + return command.Start(); + } + #endregion + } +} diff --git a/MSAddinTest/Core/Settings/PluginSetting.cs b/MSAddinTest/Core/Settings/PluginSetting.cs new file mode 100644 index 0000000..36a07ab --- /dev/null +++ b/MSAddinTest/Core/Settings/PluginSetting.cs @@ -0,0 +1,182 @@ +using Bentley.DgnPlatformNET; +using MSAddinTest.Core.Loader; +using MSAddinTest.Utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Settings +{ + /// + /// 设置 + /// + public class PluginSetting + { + private JObject _jsonConfig; + private readonly string _savePath; + + public PluginSetting(object addin) + { + _savePath = Path.Combine(ConfigurationManager.GetVariable("_USTN_HOMEROOT"), $@"MSAddinTest\config.json"); + // 监听Ord关闭事件,关闭之前要保存配置 + Startup.MSAddin.Instance.ExitDesignFileStateEvent += Instance_ExitDesignFileStateEvent; + + // 从指定目录读取配置文件 + Read(); + } + + private void Instance_ExitDesignFileStateEvent(Bentley.MstnPlatformNET.AddIn sender, EventArgs eventArgs) + { + // 退出文件时,保存配置 + Save(); + } + + private void Read() + { + // 从本地读取设置 + // 保存的位置位于用户目录 + if (!File.Exists(_savePath)) + { + _jsonConfig = new JObject(); + return; + } + + // 从文件中读取 + var content = File.ReadAllText(_savePath); + if (string.IsNullOrEmpty(content)) + { + _jsonConfig = new JObject(); + return; + } + + _jsonConfig = JObject.Parse(content); + } + + /// + /// 保存配置 + /// + public void Save() + { + // 保存设置到本地 + Directory.CreateDirectory(Path.GetDirectoryName(_savePath)); + using (var fileStream = File.Create(_savePath)) + { + var sr = new StreamWriter(fileStream); + var content = JsonConvert.SerializeObject(_jsonConfig, Formatting.Indented); + sr.Write(content); + sr.Close(); + } + } + + #region 属性更改相关逻辑 + public event Action SettingChanged; + private void TriggerSettingChangedEvent(UpdateSettingArg arg) + { + SettingChanged?.Invoke(arg); + } + #endregion + + #region 设置属性 + + // 添加插件 + public void AddPlugin(string pluginName, string dllFullPath, bool isAutoReload) + { + // 获取插件目录 + JArray plugins = _jsonConfig.SelectTokenOrDefault("plugins", null); + if (plugins == null) + { + // 添加配置 + plugins = new JArray(); + _jsonConfig.Add("plugins", plugins); + } + + // 查找是否已经存在配置 + JToken pluginObj = plugins.SelectToken($"[?(@.name=='{pluginName}')]"); + + if (pluginObj == null) plugins.Add(new JObject() + { + new JProperty("name",pluginName), + new JProperty("dllFullPath",dllFullPath), + new JProperty("autoReload",isAutoReload), + new JProperty("autoLoad",false), + }); + } + + /// + /// 更新插件设置 + /// + /// + public void UpdatePluginSetting(UpdateSettingArg settingParams) + { + // 更新插件设置 + var plugins = _jsonConfig.SelectTokens($"$..plugins[?(@.name=='{settingParams.Name}')]"); + foreach (var plugin in plugins) + { + settingParams.UpdateSetting(plugin); + // 触发更新 + TriggerSettingChangedEvent(settingParams); + } + } + + public void RemovePluginSetting(string pluginName) + { + // 找到插件 + var jToen = _jsonConfig.SelectToken($"$.plugins[?(@.name=='{pluginName}')]"); + if (jToen == null) return; + + jToen.Remove(); + + // 保存 + Save(); + } + #endregion + + #region 获取设置 + /// + /// 是否自动重载 + /// + /// + /// + public bool IsAutoReload(string pluginName) + { + var path = $"$.plugins[?(@.name=='{pluginName}')].autoReload"; + var value = _jsonConfig.SelectToken(path); + if (value == null) return true; + + return value.ToObject(); + } + + /// + /// 获取自动加载的插件 + /// + /// + public List GetAutoLoadLoaderSetups() + { + var path = "$..plugins[?(@.autoLoad==true)]"; + var plugins = _jsonConfig.SelectTokens(path); + var results = new List(); + foreach (var plugin in plugins) + { + var loader = new LoaderSetup() + { + PluginName = plugin.SelectTokenOrDefault("name", string.Empty), + DllFullPath = plugin.SelectTokenOrDefault("dllFullPath", string.Empty), + PluginSetting = this, + }; + if (string.IsNullOrEmpty(loader.PluginName)) continue; + // 判断文件是否存在 + if (!File.Exists(loader.DllFullPath)) continue; + + results.Add(loader); + } + + return results; + } + #endregion + } +} diff --git a/MSAddinTest/Core/Settings/UpdateSettingArg.cs b/MSAddinTest/Core/Settings/UpdateSettingArg.cs new file mode 100644 index 0000000..5b06bca --- /dev/null +++ b/MSAddinTest/Core/Settings/UpdateSettingArg.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core.Settings +{ + public class UpdateSettingArg + { + /// + /// 插件名 + /// + public string Name { get; set; } + + /// + /// 字段名 + /// + public string FieldName { get; set; } + + /// + /// 值 + /// + public string Value { get; set; } + + public void UpdateSetting(JToken jToken) + { + if (jToken == null) return; + + // 判断数据类型 + if (double.TryParse(Value,out var number)) + { + jToken[FieldName] = number; + return; + } + + if(bool.TryParse(Value,out var boolValue)) + { + jToken[FieldName] = boolValue; + return; + } + + jToken[FieldName] = Value; + } + } +} diff --git a/MSAddinTest/Core/Settings/defaultSettings.json b/MSAddinTest/Core/Settings/defaultSettings.json new file mode 100644 index 0000000..540f3b9 --- /dev/null +++ b/MSAddinTest/Core/Settings/defaultSettings.json @@ -0,0 +1,10 @@ +{ + "plugins": [ + //{ + // "name": "", + // "dllFullPath": "", + // "autoReload": true, + // "autoLoad": false + //} + ] +} diff --git a/MSAddinTest/Core/StatusCode.cs b/MSAddinTest/Core/StatusCode.cs new file mode 100644 index 0000000..98d1438 --- /dev/null +++ b/MSAddinTest/Core/StatusCode.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Core +{ + /// + /// 状态码 + /// + public enum StatusCode + { + Success, + Failed, + NotFound, + AlreadyLoaded, + } +} diff --git a/MSAddinTest/Interfaces/IMSTest.cs b/MSAddinTest/Interfaces/IMSTest.cs new file mode 100644 index 0000000..f8d61cc --- /dev/null +++ b/MSAddinTest/Interfaces/IMSTest.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Interfaces +{ + /// + /// 插件接口 + /// 继承该接口必须有一个无参的构造函数供管理器初始化 + /// + public interface IMSTest + { + + } +} diff --git a/MSAddinTest/Interfaces/ITestClass.cs b/MSAddinTest/Interfaces/ITestClass.cs new file mode 100644 index 0000000..b79c4ab --- /dev/null +++ b/MSAddinTest/Interfaces/ITestClass.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Interfaces +{ + /// + /// 类执行器的插件接口 + /// + public interface ITestClass:IMSTest + { + /// + /// 执行插件方法 + /// + /// 用于初始化的参数 + /// 执行结果json串 + void Execute(string arg); + } +} diff --git a/MSAddinTest/Interfaces/ITestStaticMethod.cs b/MSAddinTest/Interfaces/ITestStaticMethod.cs new file mode 100644 index 0000000..dc77ae6 --- /dev/null +++ b/MSAddinTest/Interfaces/ITestStaticMethod.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Interfaces +{ + /// + /// 静态方法的入口 + /// + public interface ITestStaticMethod : IMSTest + { + } +} diff --git a/MSAddinTest/Interfaces/MSTestAttribute.cs b/MSAddinTest/Interfaces/MSTestAttribute.cs new file mode 100644 index 0000000..de2acb3 --- /dev/null +++ b/MSAddinTest/Interfaces/MSTestAttribute.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Interfaces +{ + /// + /// Plugin 特性 + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] + public class MSTestAttribute : Attribute + { + /// + /// 内部会将 name.Trim() 作为 Name + /// + /// + public MSTestAttribute(string name) + { + Name = name.Trim(); + } + + /// + /// 必须是全局唯一的 + /// + public string Name { get;private set; } + + /// + /// 描述 + /// + public string Description { get; set; } + } +} diff --git a/MSAddinTest/Interfaces/TestAddin.cs b/MSAddinTest/Interfaces/TestAddin.cs new file mode 100644 index 0000000..6990e35 --- /dev/null +++ b/MSAddinTest/Interfaces/TestAddin.cs @@ -0,0 +1,47 @@ +using Bentley.MstnPlatformNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static Bentley.MstnPlatformNET.AddIn; + +namespace MSAddinTest.Interfaces +{ + /// + /// Addin 插件 + /// + public abstract class TestAddin : IMSTest + { + protected TestAddin(IntPtr mdlDescriptor) + { + } + + public virtual void Init(AddIn addIn) + { + Run(new string[] { }); + } + + protected abstract int Run(string[] commandLine); + + /// + /// 卸载后手动清除向 Addin 中注册的一些事件 + /// + protected virtual void OnUnloaded(UnloadedEventArgs eventArgs) { } + + /// + /// 发送卸载完成通知 + /// + /// + public void NotifyOnUnloaded(UnloadedEventArgs eventArgs) => OnUnloaded(eventArgs); + + /// + /// 隐式转换成 addin + /// + /// + public static implicit operator AddIn(TestAddin testAddin) + { + return Startup.MSAddin.Instance; + } + } +} diff --git a/MSAddinTest/MSAddinTest.csproj b/MSAddinTest/MSAddinTest.csproj new file mode 100644 index 0000000..d2f0620 --- /dev/null +++ b/MSAddinTest/MSAddinTest.csproj @@ -0,0 +1,54 @@ + + + net48 + Library + false + true + AnyCPU;x64 + true + C:\Program Files\Bentley\MicroStation 2024\MicroStation + + + x64 + $(Microstation)\MicroStation\Mdlapps + + + + $(Microstation)\Mdlapps\ + + + bin\x64\Release\ + + + + False + $(Microstation)\Bentley.DgnPlatformNET.dll + False + + + False + $(Microstation)\Assemblies\Newtonsoft.Json.dll + + + $(Microstation)\ustation.dll + False + + + + + Designer + CommandTable.xml + + + + + + + + + + + + \ No newline at end of file diff --git a/MSAddinTest/MSAddinTest.nuspec b/MSAddinTest/MSAddinTest.nuspec new file mode 100644 index 0000000..1fef2dd --- /dev/null +++ b/MSAddinTest/MSAddinTest.nuspec @@ -0,0 +1,17 @@ + + + + MSAddinTest + 1.0.7 + MSAddinTest + galens + false + Apache-2.0 + + https://github.com/GalensGan/MSAddinTest/ + a framework for microstation addin or utils test, support hot reloading + + copyright 2024 + Microstation AddinTest Test + + \ No newline at end of file diff --git a/MSAddinTest/MainWindow.xaml b/MSAddinTest/MainWindow.xaml new file mode 100644 index 0000000..2bd6ed3 --- /dev/null +++ b/MSAddinTest/MainWindow.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + diff --git a/MSAddinTest/MainWindow.xaml.cs b/MSAddinTest/MainWindow.xaml.cs new file mode 100644 index 0000000..44fc258 --- /dev/null +++ b/MSAddinTest/MainWindow.xaml.cs @@ -0,0 +1,63 @@ +using MSAddinTest.Core; +using MSAddinTest.Core.Command; +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 MSAddinTest +{ + /// + /// MainWindow.xaml 的交互逻辑 + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + + // 初始化 + new PluginManager(null); + + TestBtn.Click += TestBtn_Click; + LoadPluginBtn.Click += LoadPluginBtn_Click; + UnloadPluginBtn.Click += UnloadPluginBtn_Click; + ReloadPluginBtn.Click += ReloadPluginBtn_Click; + } + + private void ReloadPluginBtn_Click(object sender, RoutedEventArgs e) + { + var cmd = new ReloadPluginCommand("TestPlugin"); + PluginManager.Manager.InvokeCommand(cmd); + } + + private void UnloadPluginBtn_Click(object sender, RoutedEventArgs e) + { + // 卸载插件 + var cmd = new UnloadPluginCommand("TestPlugin"); + PluginManager.Manager.InvokeCommand(cmd); + } + + private void LoadPluginBtn_Click(object sender, RoutedEventArgs e) + { + var cmd = new LoadPluginCommand(); + PluginManager.Manager.InvokeCommand(cmd); + } + + private void TestBtn_Click(object sender, RoutedEventArgs e) + { + var cmd = new RunPluginCommand("test", new Plugin.PluginArg()); + PluginManager.Manager.InvokeCommand(cmd); + } + } +} diff --git a/MSAddinTest/Properties/AssemblyInfo.cs b/MSAddinTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..625e333 --- /dev/null +++ b/MSAddinTest/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("MSAddinTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MSAddinTest")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(false)] + +//若要开始生成可本地化的应用程序,请设置 +//.csproj 文件中的 CultureYouAreCodingWith +//例如,如果您在源文件中使用的是美国英语, +//使用的是美国英语,请将 设置为 en-US。 然后取消 +//对以下 NeutralResourceLanguage 特性的注释。 更新 +//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。 + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //主题特定资源词典所处位置 + //(未在页面中找到资源时使用, + //或应用程序资源字典中找到时使用) + ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置 + //(未在页面中找到资源时使用, + //、应用程序或任何主题专用资源字典中找到时使用) +)] + + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 +//通过使用 "*",如下所示: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.1.0")] +[assembly: AssemblyFileVersion("1.1.0")] diff --git a/MSAddinTest/Properties/Resources.Designer.cs b/MSAddinTest/Properties/Resources.Designer.cs new file mode 100644 index 0000000..80c2b01 --- /dev/null +++ b/MSAddinTest/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace MSAddinTest.Properties { + using System; + + + /// + /// 一个强类型的资源类,用于查找本地化的字符串等。 + /// + // 此类是由 StronglyTypedResourceBuilder + // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 + // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen + // (以 /str 作为命令选项),或重新生成 VS 项目。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// 返回此类使用的缓存的 ResourceManager 实例。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MSAddinTest.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 重写当前线程的 CurrentUICulture 属性,对 + /// 使用此强类型资源类的所有资源查找执行重写。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/MSAddinTest/Properties/Resources.resx b/MSAddinTest/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/MSAddinTest/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/MSAddinTest/Properties/Settings.Designer.cs b/MSAddinTest/Properties/Settings.Designer.cs new file mode 100644 index 0000000..24c29b3 --- /dev/null +++ b/MSAddinTest/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace MSAddinTest.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.8.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/MSAddinTest/Properties/Settings.settings b/MSAddinTest/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/MSAddinTest/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/MSAddinTest/Startup/Commands.xml b/MSAddinTest/Startup/Commands.xml new file mode 100644 index 0000000..db104e1 --- /dev/null +++ b/MSAddinTest/Startup/Commands.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MSAddinTest/Startup/KeyinFuncs.cs b/MSAddinTest/Startup/KeyinFuncs.cs new file mode 100644 index 0000000..9ff638a --- /dev/null +++ b/MSAddinTest/Startup/KeyinFuncs.cs @@ -0,0 +1,76 @@ +using MSAddinTest.Core; +using MSAddinTest.Core.Command; + +namespace MSAddinTest.Startup +{ + internal class KeyinFuncs + { + /// + /// 加载插件 + /// + /// + public static void LoadPlugin(string unparsed) + { + var cmd = new LoadPluginCommand(); + PluginManager.Manager.InvokeCommand(cmd); + } + + /// + /// 卸载插件 + /// + /// + public static void UnloadPlugin(string unparsed) + { + var cmd = new UnloadPluginCommand(unparsed); + PluginManager.Manager.InvokeCommand(cmd); + } + + /// + /// 重载插件 + /// + /// + public static void ReloadPlugin(string unparsed) + { + var cmd = new ReloadPluginCommand(unparsed); + PluginManager.Manager.InvokeCommand(cmd); + } + + /// + /// 测试插件 + /// + public static void TestPlugin(string unparsed) + { + var cmd = new RunPluginCommand(unparsed); + PluginManager.Manager.InvokeCommand(cmd); + } + + /// + /// 设置参数 + /// + public static void SetPluginSettings(string unparsed) + { + var cmd = new UpdateSettingsCommand(unparsed); + PluginManager.Manager.InvokeCommand(cmd); + } + + /// + /// 安装插件,安装后,启动时会自动加载 + /// + /// + public static void Install(string unparsed) + { + var cmd = new InstallCommand(); + PluginManager.Manager.InvokeCommand(cmd); + } + + /// + /// 卸载插件 + /// + /// + public static void Uninstall(string unparsed) + { + var cmd = new UninstallCommand(); + PluginManager.Manager.InvokeCommand(cmd); + } + } +} diff --git a/MSAddinTest/Startup/MSAddin.cs b/MSAddinTest/Startup/MSAddin.cs new file mode 100644 index 0000000..bb8919d --- /dev/null +++ b/MSAddinTest/Startup/MSAddin.cs @@ -0,0 +1,39 @@ +using Bentley.MstnPlatformNET; +using MSAddinTest.Core.Command; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace MSAddinTest.Startup +{ + /// + /// Microstation Addin 入口 + /// + [AddIn(MdlTaskID = "MSAddinTest")] + public class MSAddin : AddIn + { + public static AddIn Instance { get; private set; } + + public MSAddin(IntPtr mdlDescriptor) : base(mdlDescriptor) + { + Instance = this; + } + + protected override int Run(string[] commandLine) + { + // 初始化插件管理器 + new Core.PluginManager(this); + + MessageCenter.Instance.ShowInfoMessage("MSAddintTest 加载成功!","",false); + + // 加载自启动插件 + var cmd = new LoadPluginsWhenStartupCommand(); + Core.PluginManager.Manager.InvokeCommand(cmd); + + return 0; + } + } +} diff --git a/MSAddinTest/Utils/Ex_Json.cs b/MSAddinTest/Utils/Ex_Json.cs new file mode 100644 index 0000000..065107f --- /dev/null +++ b/MSAddinTest/Utils/Ex_Json.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Utils +{ + /// + /// json 扩展方法 + /// + internal static class Ex_Json + { + /// + /// 通过json表达式获取值,如果不存在,则返回默认值 + /// + /// + /// + /// + /// + /// + public static T SelectTokenOrDefault(this JToken jToken, string path, T defaultValue) + { + var value = jToken.SelectToken(path, false); + if (value == null) return defaultValue; + + return value.ToObject(); + } + } +} diff --git a/MSAddinTest/Utils/FileHelper.cs b/MSAddinTest/Utils/FileHelper.cs new file mode 100644 index 0000000..890f319 --- /dev/null +++ b/MSAddinTest/Utils/FileHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace MSAddinTest.Utils +{ + internal class FileHelper + { + /// + /// 获取文件的 HASH256 值 + /// + /// + /// + public static string GetFileHash(string filePath) + { + if (!File.Exists(filePath)) return ""; + + var hash = SHA512.Create(); + var stream = new FileStream(filePath, FileMode.Open); + byte[] bytes = hash.ComputeHash(stream); + stream.Close(); + return BitConverter.ToString(bytes); + } + } +} diff --git a/MSUtils/MSUtils.csproj b/MSUtils/MSUtils.csproj new file mode 100644 index 0000000..55a031a --- /dev/null +++ b/MSUtils/MSUtils.csproj @@ -0,0 +1,66 @@ + + + + + Debug + AnyCPU + {E687EF99-6F1E-4255-A31F-F235257A9391} + Library + Properties + MSUtils + MSUtils + v4.6.2 + 512 + false + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + 7.3 + prompt + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + 7.3 + prompt + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MSUtils/Properties/AssemblyInfo.cs b/MSUtils/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d9f1bf9 --- /dev/null +++ b/MSUtils/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("MSUtils")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MSUtils")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("e687ef99-6f1e-4255-a31f-f235257a9391")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 +//通过使用 "*",如下所示: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.*")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MSUtils/TestClass.cs b/MSUtils/TestClass.cs new file mode 100644 index 0000000..84a0d8f --- /dev/null +++ b/MSUtils/TestClass.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MSUtils +{ + public class TestClass + { + /// + /// 测试异常 + /// + /// + public static void NullException() + { + throw new NullReferenceException("test exception in reference"); + } + } +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..731f394 --- /dev/null +++ b/Readme.md @@ -0,0 +1,353 @@ +# MSAddinTest 使用文档 + +## 简介 + +本插件名为:MSAddinTest,可以帮助开发者在不关闭 Microstation 的情况下,可以实现代码热重载与快速调试。 + +对代码的入侵小,使用时只需在 keyin 前加上 `MSTest test` 即可调用现有的 keyin 命令。 + +The name of this plugin is `MSAddinTest`,it's an efficient plugin for addin developing on Miscrostation platform which allows you to hot-reload addin plugin。 + +it is so easy to use,when you want to invoke a keyin function,just put the string `MSTest test` before your addin keyin,aha,just it~ + +## 功能特点 + +1. 支持热重载 + + 在进行 Microstation 调试过程中,仅可在方法内部进行修改,无法增加方法或者类等结构型操作,如果想要进行深度修改,只能关闭 Microstation,修改 Addin 库,编译,重新打开 Microstation,再开始调试。而启动 Microstation 相对费时,会浪费掉很多时间。 + + 本插件主要解决这个痛点,提供了对库的热重载功能,可以在不关闭 Microstation 的情况下,重新修改编译加载 DLL 库。 + +2. 支持自动启动 + + 支持一键配置在启动时加载。 + +3. 支持自动加载 DLL + + 支持配置启动时自动加载测试 DLL,可以指定自动加载某些库。 + +4. 支持断点调试 + + 支持测试 DLL 的断点调试。 + +5. 搭建测试简单,代码入侵小,与正常开发无异 + + 测试 keyin 时,只需要 `MSTest test keyin` 即可执行测试,其它详见安装与使用。 + +## 为什么不使用VS的原生热重载? + +vs 热重载对于以下修改不支持: + +- 添加方法、字段、构造函数等 +- 动态对象的增改 +- [More ...](https://learn.microsoft.com/zh-cn/visualstudio/debugger/supported-code-changes-csharp?view=vs-2022#unsupported-changes-to-code) + +而 MSAddinTest 没有上述限制。 + +## MSAddinTest 安装 + +1. 拷贝 `MSAddinTest.dll` 到 Microstaion 中的 `mdlapp` 目录里 +2. 启动 Microstation +3. 加载 `MSAddinTest.dll` +4. 输入 `MSTest install` 开启自动启动。可以输入 `MSTest uninstall` 关闭自动启动。 + +> 支持基于 Microstation 的所有 Bentley 产品 + +## MSAddinTest 使用 + +### Debug 流程 + +**调试:** + +1. [标记调用入口](#标记调用入口) +2. 编译代码 +3. 启动 Microstation + +1. 输入 keyin `MSTest load` 加载待测试 dll +2. VS 中附加进程到 Micostation 开启调试(快捷键:Ctrl+Alt+P) +3. 输入 `MSTest test keyin` 调用待测试的 keyin 命令 + +**重新修改:** + +1. 关闭调试 + +2. 编辑代码 + +3. 重新编译 + +4. 在 Microstation 中使用 `MsTest reload DllName` 来重新加载库文件 + + 这一步可以通过设置实现自动重载([转到 MSTest set](#mstest-set))来跳过。 + +5. 在 vs 中将 Microstation 重新附加到进程(快捷键:Shift+Alt+P) + +6. 输入 `MSTest test keyin` 进行调试 + +### 标记调用入口 + +对于 Addin、静态方法、实例方法,标记调用入口的方式略有不同,下面分别进行介绍。 + +#### Addin 库 + +想要支持 Addin 库的调用,须将自定义的 `Addin` 类继承抽象类 `TestAddin`。 + +示例如下: + +``` csharp +/// +/// 测试 addin 调试 +/// 1. 继承抽象类 TestAddin +/// 2. 在 Init 中获取传递的 addin 供当前库使用 +/// +[AddIn(MdlTaskID = "TestAddinPlugin")] +internal class PluginAddin +// 通过 #if DEBUG 针对不同的编译场景编写不同的代码 +#if DEBUG + // debug 时,继承 TestAddin,实现热重载 + : TestAddin +{ + // 可以在此处重写初始化方法,一般不进行重写 + // 该方法为 TestAddin 特有 + public override void Init(AddIn addin) + { + // 重写时,一定要调用父类中的方法 + base.Init(addin); + } +#else + // 非 debug 模型式,直接继承 AddIn + : AddIn + { +#endif + + public static AddIn Instance { get; private set; } + public PluginAddin(IntPtr mdlDescriptor) : base(mdlDescriptor) + { + // 在此处可以保存 this 用于窗体的加载 + // this 是 TestAddin 实例,可以转换成 Addin 是因为 TestAddin 进行了隐式转换 + Instance = this; + } + + // 在这个方法中释放资源 + public override void Unloaded(UnloadedEventArgs eventArgs) + { + // 当插件重载时,可以在此处卸载上一次加载的事件 + } + + // Run 方法自动调用 + protected override int Run(string[] commandLine) + { + return 0; + } +} +``` + +**使用方法:** + +通过 `MSTest test + keyin + arg` 的方式调用 keyin 对应的静态方法 + +**其它:** + +通过上述配置后,插件会自动读取命令表,从而根据命令表查找到 Keyin 对应的静态方法。 + +一般 keyin 会有多个单词,如果觉得通过 MSTest test + keyin 的方式太长,可以在静态方法中添加 `MSTestAttribute` 特性来添加 keyin 别名。 + +如下所示: + +``` csharp +// 对于下列 keyin 对应的静态方法, 不仅可以通过 MSTest test keyin 来调用,还可以通过 MSTest test element 来调用 +[MSTest("element",Description ="这是keyin别名")] +public static void TestElement(string unparsed) +{ + MessageBox.Show("我是 keyin,我被调用了"); +} +``` + +#### 静态方法 + +除了 keyin 对应的静态方法外,想要调用其它静态方法,须满足以下条件: + +1. 类继承接口 `ITestStaticMethod` +2. 静态方法添加特性 `MSTestAttribute` +3. 静态方法有且仅有一个 string 参数 + +示例如下: + +``` csharp +/// +/// 测试静态方法 +/// 1. 类继承接口 ITestStaticMethod +/// 2. 静态方法添加特性 MSTest +/// 3. 静态方法有且仅有一个IMSTestArg参数 +/// +public class TestStaticMethodExecutor : ITestStaticMethod +{ + [MSTest("static")] + public static object Execute(string arg) + { + MessageBox.Show("IStaticMethodPlugin 被调用了!"); + return true; + } +} + +// 通过 MSTest test static 来调用,static 指的是 [MSTest("static")] 中的 static +``` + +**使用方法:** + +通过 `MSTest test name` 调用。 + +> 静态方法必须添加 MSTestAttribute 才能被调用 + +#### 实例类 + +本插件也支持对实例的调试,须满足以下条件: + +1. 继承接口 `ITestClass` +2. 类上添加特性 `MSTestAttribute` + +示例如下: + +``` csharp +/// +/// 测试类执行器 +/// 1. 继承接口 ITestClass +/// 2. 类添加特性 MSTest +/// +[MSTest("class", Description = "测试 IClassPlugin 插件")] +internal class TestClassExecutor : ITestClass +{ + // 实现接口 ITestClass + // 该接口为实例初始化后的调用入口 + public void Execute(string arg) + { + MessageBox.Show("IClassPlugin 被调用了!"); + } +} + +// 通过 MSTest test class 调用 +``` + +**使用方法:** + +通过 `MSTest test name` 调用。 + +#### 混合标记 + +可以将上述三种方式混合使用,可以在一个类上同时实现 `Addin`、`静态方法 `和 `实例方法` 三种测试方式。 + +其中 Addin 和 静态方法 的调用名称一样时,保留 Addin 测试入口。 + +## 内置 Keyin 介绍 + +### keyin 汇总 + +| keyin 名称 | 作用 | +| ------------------------ | ------------------------------------------------------------ | +| MSTest install | 在用户配置中添加 MSAddinTest 自启动配置。安装后,每次打开软件都会自动加载 MSAddinTest。 | +| MSTest uninstall | 取消自启动。 | +| MSTest load | 加载待测试的 Dll。只有加载了的 Dll,才能调用其中的方法。如果已经加载过,则等效于 reload。 | +| MSTest unload + dllName | 卸载已经加载的 dll | +| MSTest reload + dllName | 重新加载目标 dll。当修改编译后,需要调用该 keyin 进行重新加载。 | +| MSTest test + keyin/name | 调用方法。 | +| MSTest set | 设置参数 | + +### MSTest set + +该命令目前支持以下两个参数的设置: + +1. 目标 Dll 在启动时就加载 + + 格式:`dllName.autoLoad=true` + + 多个配置用逗号分隔。 + +2. 目标 Dll 的自动热重载 + + 格式:`dllName.autoReload=true` + + 当修改编译后,MSTestAddin 会自动重载目标 dll,不需要手动调用 `MSTest reload` + +也可以同时设置: + +```csharp +MSTest set dllName.autoLoad=true,dllName.autoReload=true +``` + +如果后一个参数的 dll 名称与前一个是一样,则可以省略,如下: + +``` csharp +MSTest set dllName.autoLoad=true,autoReload=true +``` + +> 该配置的文件路径为: +> +> "_USTN_HOMEROOT" 变量位置下的`MSAddinTest\config.json` + +## 引用 DLL 更新 + +假设有一个 addin.dll,它引用了一个开发中的功能库 utils.dll,当我们修改 utils.dll 时,也希望插件可以热加载,及时反馈到调试上。 + +要使引用的 Dll 变动触发热重载,需要给引用的程序集添加 AssemblyVersion 自增版本号(当然,也可以每次修改后手动修改该版本号)。 + +自增版本号修改方法如下: + +1. 在 `Properties.AssemblyInfo.cs` 修改如下内容: + + ``` csharp + // 将下面内容 + [assembly: AssemblyVersion("1.0.0.0")] + // 改成 + [assembly: AssemblyVersion("1.0.0.*")] + ``` + +2. 打开项目配置文件 `XXX.csproj`,修改如下内容: + + ``` xml + // 将这个配置改成 false + false + ``` + +> **特别注意:** +> +> 当被引用 DLL 用于正式环境时,须将其程序集集版本号改成特定版本号,防止其它引用该 DLL 的其它库出现错误。 + +## 实现原理 + +通过向默认域中加载不同版本的程序集来实现 DLL 版本的重载。为了可以重新编译已加载的 DLL,需要保证加载的 DLL 在被加载后不被锁定,本程序采用从内存的加载方式实现了这个需求。 + +该实现是一种伪热加载的实现方法,每次加载的 Dll 都会驻留在内存中,卸载某个程序集时,只是丢弃了其引用,并没有从内存里释放。 + +可能有人要问了,为什么采用这种方式呢? + +该程序的前一个版本采用了子程序域的思想,虽然可以完美解决 Dll 的加载与卸载,但是无法跨程序域复用一些单例对象,比如 `Session.Instance`,所以最终采用了这种方式。 + +如果感兴趣,可以切换到 `Addin` 分支进行阅读。 + +当然,后期如果解决了上述所提到的问题,也会考虑重构成程序域的方式,毕竟那才是正统。 + +## 使用需知 + +1. DLL 一旦加载后,就不能卸载,所以每次加载都会驻留在内存中 +2. 通过 `AppDomain.CurrentDomain.GetAssemblies()` 获取到的程序集是按加载顺序排列的 + 如果在程序中涉及到序列化和反序列化操作时,序列化时不要保存程序集的版本信息,反序列化时取最新的程序集来获取相应的 Type。 +3. Tool 工具内抛出的异常无法被 Catch 到,如果不处理,会触发 MS 异常捕获,超过两次后会闪退 + +## UI + +本插件预留了 UI 接口,但短期内不会实现。欢迎 PR。 + +## 开发环境 + +1. VisualStudio 2022 +2. Microstation CE +3. 环境变量 `Microstation`,值指向 Microstation 安装目录,路径末没有 `\` 号 + +## 赞助与支持 + +如果觉得插件不错,可以请作者喝一杯咖啡哟! + +
+wechat +ailipay +
+ diff --git a/TestAddinPlugin/App.config b/TestAddinPlugin/App.config new file mode 100644 index 0000000..b50c74f --- /dev/null +++ b/TestAddinPlugin/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TestAddinPlugin/Properties/AssemblyInfo.cs b/TestAddinPlugin/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6d069a4 --- /dev/null +++ b/TestAddinPlugin/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("TestAddinPlugin")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestAddinPlugin")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("013a0d0a-5c3f-4510-acc0-ccb8c86cc4fc")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 +//通过使用 "*",如下所示: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TestAddinPlugin/TestAddin/Commands.xml b/TestAddinPlugin/TestAddin/Commands.xml new file mode 100644 index 0000000..917819e --- /dev/null +++ b/TestAddinPlugin/TestAddin/Commands.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/TestAddinPlugin/TestAddin/PluginAddin.cs b/TestAddinPlugin/TestAddin/PluginAddin.cs new file mode 100644 index 0000000..cfb0259 --- /dev/null +++ b/TestAddinPlugin/TestAddin/PluginAddin.cs @@ -0,0 +1,36 @@ +using Bentley.MstnPlatformNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MSAddinTest.MSTestInterface; + +namespace TestAddinPlugin.TestAddin +{ + /// + /// 测试 addin 调试 + /// 1. 继承抽象类 MSTest_Addin + /// 2. 在 Init 中获取传递的 addin 供当前库使用 + /// + [AddIn(MdlTaskID = "TestAddinPlugin")] + internal class PluginAddin : MSTest_Addin + { + public static AddIn Instance { get; private set; } + public PluginAddin(IntPtr mdlDescriptor) : base(mdlDescriptor) + { + + } + + public override void Init(AddIn addin) + { + Instance = addin; + Run(new string[] { }); + } + + protected override int Run(string[] commandLine) + { + return 0; + } + } +} diff --git a/TestAddinPlugin/TestAddin/PluginKeyinFuncs.cs b/TestAddinPlugin/TestAddin/PluginKeyinFuncs.cs new file mode 100644 index 0000000..43d8ff2 --- /dev/null +++ b/TestAddinPlugin/TestAddin/PluginKeyinFuncs.cs @@ -0,0 +1,24 @@ +using MSAddinTest.MSTestInterface; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace TestAddinPlugin.TestAddin +{ + internal class PluginKeyinFuncs + { + [MSTest("element",Description ="这是keyin别名")] + public static void TestElement(string unparsed) + { + MessageBox.Show("我是 keyin,我被调用了"); + } + + public static void TestAddin(string unparsed) + { + MessageBox.Show("我是纯 keyin,且我没有被 MSTest 标记,我被调用了"); + } + } +} diff --git a/TestAddinPlugin/TestAddinPlugin.csproj b/TestAddinPlugin/TestAddinPlugin.csproj new file mode 100644 index 0000000..e538b48 --- /dev/null +++ b/TestAddinPlugin/TestAddinPlugin.csproj @@ -0,0 +1,58 @@ + + + net48 + Library + true + false + true + AnyCPU;x64 + true + C:\Program Files\Bentley\MicroStation 2024\MicroStation\Mdlapps + C:\Program Files\Bentley\MicroStation 2024\MicroStation + + + x64 + C:\Program Files\Bentley\MicroStation 2024\MicroStation\Mdlapps + + + + bin\x64\Debug\ + + + bin\x64\Release\ + + + + False + $(Microstation)\Bentley.DgnPlatformNET.dll + + + False + $(Microstation)\Bentley.GeometryNET.dll + + + False + $(Microstation)\Bentley.GeometryNET.Structs.dll + + + $(Microstation)\ustation.dll + False + + + + + Designer + CommandTable.xml + + + + + + + + + + + \ No newline at end of file diff --git a/TestAddinPlugin/TestClassExecutor.cs b/TestAddinPlugin/TestClassExecutor.cs new file mode 100644 index 0000000..7b0481b --- /dev/null +++ b/TestAddinPlugin/TestClassExecutor.cs @@ -0,0 +1,22 @@ +using System.Windows.Forms; + +namespace TestAddinPlugin +{ + /// + /// 测试类执行器 + /// 1. 继承接口 IMSTest_Class + /// 2. 类添加特性 MSTest + /// + [MSTest("class", Description = "测试 IClassPlugin 插件,通过 mstest test class 来调用")] + public class TestClassExecutor : IMSTest_Class + { + /// + /// 该接口为实例初始化后的调用入口 + /// + /// + public void Execute(string arg) + { + MessageBox.Show("IClassPlugin 被调用了!"); + } + } +} diff --git a/TestAddinPlugin/TestStaticMethodExecutor.cs b/TestAddinPlugin/TestStaticMethodExecutor.cs new file mode 100644 index 0000000..40caa95 --- /dev/null +++ b/TestAddinPlugin/TestStaticMethodExecutor.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Windows.Forms; + +using Bentley.DgnPlatformNET.Elements; +using Bentley.GeometryNET; +using Bentley.MstnPlatformNET; + +using MSAddinTest.Interfaces; + +using MSUtils; + +namespace TestAddinPlugin +{ + /// + /// 测试静态方法 + /// 1. 类继承接口 IMSTest_StaticMethod + /// 2. 静态方法添加特性 MSTest + /// 3. 静态方法有且仅有一个IMSTestArg参数 + /// + public class TestStaticMethodExecutor : IMSTest_StaticMethod + { + [MSTest("static", Description = "通过 mstest test static 来调用")] + public static object Execute(string arg) + { + MessageBox.Show("IStaticMethodPlugin 被调用了4!"); + return true; + } + + + [MSTest("element")] + public static object NewElement(string arg) + { + // 绘制一个元素 + CurvePrimitive line = CurvePrimitive.CreateLineString(new List() + { + new DPoint3d(0,0,0), + new DPoint3d(100000,0,0), + }); + var instance = Session.Instance; + if (instance == null) return false; + + var dgnm = instance.GetActiveDgnModel(); + Element et = DraftingElementSchema.ToElement(dgnm, line, null); + if (et != null) et.AddToModel(); + + MessageBox.Show("绘制元素成功!"); + return true; + } + + [MSTest("exception")] + public static object TestReference(string arg) + { + TestClass.NullException(); + return false; + } + } +}