适用于 Windows 窗体应用程序的可插入 UI 组件



我们的应用程序包含许多模块,这些模块可以在安装和运行后动态推送到我们的应用程序。所有这些模块可能需要在 UI 上显示一些内容。 所以我认为我们可以构建一个可以从 DLL(或任何其他类型的程序集)加载 UI 组件的 UI exe。假设模块 1 和模块 2 在机器上处于活动状态,我们将在 UI 的左帧显示"模块 1"和"模块 2"。如果用户单击"module1",右侧框架将打开模块 1 的屏幕,该模块是从与模块 1 一起向下推的另一个程序集(例如 DLL)加载的。

只是想知道这种可插入的 UI 体系结构在 Windows 窗体中是否可行。我在互联网上做了一些搜索,我没有找到任何有用的信息。

是的,这是可能的,我已经自己做到了。

执行此操作的最佳方法是在程序外部创建第二个 DLL。在该 DLL 中,您定义插件将实现的接口。然后,使主 EXE 中的窗体加载目录中的所有 DLL,并查看它是否包含实现该接口的任何类。在您的插件 DLL 中,您还引用相同的 DLL 并让您的模块实现该接口。您希望所有插件通用的任何功能都需要放入IMyPlugin因为这是您将在UI中将所有内容转换为的界面,因此只有这些功能可见。

//In a 2nd project that compiles as a DLL
public interface IMyPlugin
{
    Control GetControl();
}
///////////////////
//In your main project
private List<IMyPlugin> pluginsList;   
private void MainForm_Load(object sender, EventArgs e)
{ 
    foreach(string pluginPath in Directory.EnumerateFiles(Application.StartupPath + @"Plugins", "*.dll"))
    {
        try
        {
            //load the assembly
            Assembly pluginAssembly = Assembly.LoadFrom(pluginPath);
            //Find all types defined in the assembly.
            Type[] types = pluginAssembly.GetTypes();
            //Filter the types to only ones that implment IMyPlugin
            var plugins = types.Where(x => typeof(IMyPlugin).IsAssignableFrom(x));
            //Filter the plugins to only ones that are createable by Activator.CreateInstance
            var constructablePlugins = plugins.Where(x => !x.ContainsGenericParameters && x.GetConstructor(Type.EmptyTypes) != null);
            foreach (var pluginType in constructablePlugins)
            {
                //instantiate the object
                IMyPlugin plugin = (IMyPlugin)Activator.CreateInstance(pluginType);
                pluginsList.Add(plugin);
            }
        }
        catch (BadImageFormatException ex)
        {
            //ignore this exception -- probably a runtime DLL required by one of the plugins..
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.ToString(), "MainForm.MainForm_Load()");
        }
    }
    //Suspend the layout for the update
    this.SuspendLayout();
    this.someFlowLayoutPanelToStoreMyPlugins.SuspendLayout();
    foreach(IMyPlugin plugin in pluginsList)
    {
        this.someFlowLayoutPanelToStoreMyPlugins.Controls.Add(plugin.GetControl());
    }
    //resume the layout
    this.someFlowLayoutPanelToStoreMyPlugins.ResumeLayout(false);
    this.someFlowLayoutPanelToStoreMyPlugins.PerformLayout();
    this.ResumeLayout();
}

//////////////////////

// In your plugin DLL.
public class Plugin : UserControl, IMyPlugin
{
    public Plugin()
    {
        //The code in the main form requires there be a public 
        //  no parameter constructor (either explicitly or implicitly),
        //  UserControls usually have one anyway for InitializeComponent.
        InitializeComponent();
    }
    public Control GetControl()
    {
        return this;
    }
    // The rest of your code.
}

注意:这段代码只是复制和粘贴了我的很多代码,我不知道它是否会按原样完美运行,但这会让你接近你需要去的地方。

这是很有可能的,而且它不是特定于WinForms的。您需要一种方法来要求模块在某个时候提供其 UI。例如:您的模块将实现一个接口,并且它将具有返回 Panel 的方法。调用此函数时,可以在 UI 上对它执行任何操作。

例如:您的右侧面板可以是可以托管面板的东西。

您的界面可能如下所示

interface IModule
{
    ...
    Panel GetUI();
    ...
}

因此,当用户单击左窗格中的模块时,您将运行类似这样的内容。

var selectedModule = GetSelectedModule() 
// this method will do the Reflection to load your assemblies, 
// go through the Types, filter every type that implement IModule and load them.
// then get an instance of the module. (Let me know if you want help on Reflection)
if (!GetConfiguration().IsModuleEnables(selectedModule))
    return; // module not enabled. Ignore click ???
rightPane.Children.Clear();
rightPane.Children.Add(selectedModule.GetUI());
// might want to dock the module as 'Fill' as well.

我认为您最好在应用程序中保持模块的"处于活动状态"状态,而不是询问模块是否已启用(恶意模块可以始终返回 true。

希望这有帮助。

更新:为了保证安全,我建议您将模块加载到单独的应用程序域中。尽管在它们之间传递 UI 对象时可能会遇到问题。

我没有尝试过这个,所以我不能保证它会起作用,但从我所看到的,它应该可以。

如果在构建 exe 时已知模块列表,则只需更新组件 dll 即可更新任何组件的功能。 如果包含某种方法,可以从 dll 中切换给定组件是否处于活动状态,则可以使用稍后为其添加功能的虚拟组件"填充"dll。 例如:

using ComponentLibrary;
class Program
{
  static void Main()
  {
    //...
    if (Module1.IsActive()) listModules.Add(Library.Module1);
    if (Module2.IsActive()) listModules.Add(Library.Module2);
  }
  listModules_click()
  {
     // var m = clicked-on-module
     if (m is Module1)
     {
       component = new Module1();
     }
     else if (m is Module2)
     {
       component = new Module2();
     }
  }
}
namespace ComponentLibrary
{
  abstact class Module : Component
  {
     public abstract bool IsActive();
  }
  public class Module1 : Module
  {
     public bool IsActive() { return true; }
  }
  public class Module2 : Module
  {
     public bool IsActive() { return false; }
  }  
}

稍后,您编写 Module2 代码,并将其结果替换为 true IsActive() 。 但是签名不会更改,因此您无需重新编译 exe。 在此之前,没有办法真正拉出模块2。

最新更新