新文章 网摘 文章 随笔 日记

C#Winforms插件式架构示例

百特
您必须具有确认的电子邮件地址才能投票
2014年1月31日CPOL
C#.NET 4.5 Winforms插件体系结构示例

介绍

在这里,我们有一个用于C#.NET 4.5 Winforms平台的插件体系结构项目的示例实现。该项目是在Visual Studio 2013中创建的,但将在Visual Studio 2012中打开并运行,没有任何问题。

背景

我前段时间听说过插件架构,并认为这是一个好主意。因此,我决定创建一个示例C#Winforms插件项目,但要针对现实情况进行实际设计。

考虑到这一点,我决定创建一个具有用户控件和单个下拉菜单的插件系统,这两个系统都包含在一个监督类中。该项目的设计方式是,宿主项目不知道插件类的操作,仅从测试类或外部类库(DLL)加载插件。

插件的设计方式是,下拉菜单可以将键入的事件发送到用户控件,以告诉用户已选择了哪个下拉菜单项。

使用代码

这是该解决方案及其所有项目的图像:

图片1

该解决方案包含五个项目,两个Winforms宿主项目和三个类库。主项目和Winforms宿主项目是Winforms.Plugins.Host从本地测试类或类库文件(DLL)加载插件的位置。

还有一个名为的测试宿主项目Winforms.Plugins.DemoPlugin.TestHarness,该项目用于在将插件类库加载到宿主项目之前对其进行测试。

这三个类库由两个演示插件库和一个共享类库组成。我认为它们的名称表示哪个是哪个。

宿主项目(winforms.plugins.host)的唯一功能是加载插件,并使用插件中的用户控件填充Tab控件,然后使用每个单独的下拉菜单或菜单项填充Menu Strip控件。

宿主项目由一个主窗体,一个继承的用户控件和一个本地测试插件类组成,如下所示:

图片2

还有一个PluginsToConsume文件夹,其中应在启动之前放置包含插件的类库(DLL)。

=================

注意

演示插件类库将它们的程序集PluginsToConsume作为构建后步骤shell脚本命令复制到主机项目中文件夹中。一个示例如下所示:

copy $(ProjectDir)\bin\Debug\*.* $(SolutionDir)Winforms.Plugins.Host\PluginsToConsume /y

可以看出,已经使用相对路径来确保解决方案的可移植性。

=================

InheritedUserControl将要解释的,因为它的一部分的插件类的结构。

主机表单的文件背后代码如下所示:

using System;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using Microsoft.Practices.Unity;
using Winforms.Plugins.Shared;
 
namespace Winforms.Plugins.Host
{
    public partial class HostForm : Form
    {
        IUnityContainer container = null;
        private String pluginFilePath = String.Empty;
        private Boolean testMode = false;
 
        public HostForm()
        {
            InitializeComponent();
        }
 
        private void HostForm_Load(object sender, EventArgs e)
        {
            pluginFilePath = Directory.GetParent
            (System.IO.Directory.GetCurrentDirectory()).Parent.FullName + @"\PluginsToConsume\";
            testMode = Boolean.Parse(ConfigurationManager.AppSettings["TestMode"]);
 
            hostTabControl.Visible = false;
 
            if (testMode)
                this.Text = "Test Mode";
            else
                this.Text = "Live Mode - Plugins Extracted From Assemblies";
        }
 
        private void btnLoadPlugins_Click(object sender, EventArgs e)
        {
            LoadPluginsFromContainer();
        }
 
        private void LoadPluginsFromContainer()
        {
            if (container != null)
            {
                hostTabControl.TabPages.Clear();
                menuStripHost.Items.Clear();
 
                var loadedPlugins = container.ResolveAll<IPlugin>();
 
                if (loadedPlugins.Count() > 0)
                    hostTabControl.Visible = true;
 
                foreach (var loadedPlugin in loadedPlugins)
                {
                    menuStripHost.Items.Add(loadedPlugin.PluginControls().MenuStripItemContainer);
 
                    TabPage tabPage = new TabPage(loadedPlugin.Name());
                    tabPage.Controls.Add(loadedPlugin.PluginControls().UserControlContainer);
                    hostTabControl.TabPages.Add(tabPage);
                }
            }
        }
 
        private void btnEmptyContainer_Click(object sender, EventArgs e)
        {
            container = new UnityContainer();
            hostTabControl.Visible = false;
 
            hostTabControl.TabPages.Clear();
            menuStripHost.Items.Clear();
 
        }
 
        private void btnLoadContainer_Click(object sender, EventArgs e)
        {
            container = new UnityContainer();
            hostTabControl.Visible = false;
 
            hostTabControl.TabPages.Clear();
            menuStripHost.Items.Clear();
 
            if (testMode)
            {
                container.RegisterInstance<IPlugin>
                ("Plugin 1", new TestPlugin("Test Plugin 1"));
                container.RegisterInstance<IPlugin>
                ("Plugin 2", new TestPlugin("Test Plugin 2"));
            }
            else
            {
                string[] files = Directory.GetFiles(pluginFilePath, "*.dll");
 
                Int32 pluginCount = 1;
 
                foreach (String file in files)
                {
                    Assembly assembly = Assembly.LoadFrom(file);
 
                    foreach (Type T in assembly.GetTypes())
                    {
                        foreach (Type iface in T.GetInterfaces())
                        {
                            if (iface == typeof(IPlugin))
                            {
                                IPlugin pluginInstance = (IPlugin)Activator.CreateInstance
                                (T, new [] {"Live Plugin " + pluginCount++});
                                container.RegisterInstance<IPlugin>
                                (pluginInstance.Name(), pluginInstance);
                            }
                        }
                    }
                }
            }
            // At this point the unity container has all the plugin data loaded onto it. 
        }
    }
}

主机应用程序一旦建立testModepluginFilePath成员变量,便会使用其Form.Text属性将模式通知用户可以看出,“测试模式”是从该App.config appSettings部分派生而来的

注意pluginFilePath成员再次使用相对路径来确保可移植性。

启动应用程序时,它看起来像这样:

图片3

可以看到有三个按钮,第一个是“将插件实例化到容器上”。在实时模式下,它使用反射来读取找到的程序集中的类。如果它们从类库中IPlugin的接口继承Winforms.Plugins.Shared,则它将尝试将它们作为插件使用。

接下来,应按下“将插件加载到表单”按钮,然后将派生的所有插件数据加载到主机表单中。还有一个“空容器”按钮,可删除本地缓存的已存储插件数据。

注意:稍后将包含该解决方案的插件部分的完整说明。

为了存储插件数据,主机表单使用Microsoft Unity Dependency Injection容器。我以为该插件的想法类似于依赖注入中使用的IOC(控制反转)原理,所以使用DI容器存储插件数据似乎是一个好主意,即使DI容器只是用作事件之间的数据存储,这是一种有效的方法。

插件通过以下方式加载到容器中:

private void btnLoadContainer_Click(object sender, EventArgs e)
{
    container = new UnityContainer();
    hostTabControl.Visible = false;

    hostTabControl.TabPages.Clear();
    menuStripHost.Items.Clear();

    if (testMode)
    {
        container.RegisterInstance<IPlugin>
        ("Plugin 1", new TestPlugin("Test Plugin 1"));
        container.RegisterInstance<IPlugin>
        ("Plugin 2", new TestPlugin("Test Plugin 2"));
    }
    else
    {
        string[] files = Directory.GetFiles(pluginFilePath, "*.dll");

        Int32 pluginCount = 1;

        foreach (String file in files)
        {
            Assembly assembly = Assembly.LoadFrom(file);

            foreach (Type T in assembly.GetTypes())
            {
                foreach (Type iface in T.GetInterfaces())
                {
                    if (iface == typeof(IPlugin))
                    {
                        IPlugin pluginInstance = (IPlugin)Activator.CreateInstance
                        (T, new [] {"Live Plugin " + pluginCount++});
                        container.RegisterInstance<IPlugin>
                        (pluginInstance.Name(), pluginInstance);
                    }
                }
            }
        }
    }
    // At this point the unity container has all the plugin data loaded onto it.
}

RegisterInstance<T> 方法用于针对容器注册插件类实例。

完成此步骤后,应按下“将插件加载到表单”按钮,然后执行以下代码:

private void btnLoadPlugins_Click(object sender, EventArgs e)
{
    LoadPluginsFromContainer();
}

private void LoadPluginsFromContainer()
{
    if (container != null)
    {
        hostTabControl.TabPages.Clear();
        menuStripHost.Items.Clear();

        var loadedPlugins = container.ResolveAll<IPlugin>();

        if (loadedPlugins.Count() > 0)
            hostTabControl.Visible = true;

        foreach (var loadedPlugin in loadedPlugins)
        {
            menuStripHost.Items.Add(loadedPlugin.PluginControls().MenuStripItemContainer);

            TabPage tabPage = new TabPage(loadedPlugin.Name());
            tabPage.Controls.Add(loadedPlugin.PluginControls().UserControlContainer);
            hostTabControl.TabPages.Add(tabPage);
        }
    }
}

如果DI容器不为空,则清除宿主的Tab控件和Menu Strip控件的项目,然后使用已加载的插件填充它们。

对于每个插件类,主机应用程序都会遇到一个新的“标签页”,并为每个插件下拉菜单或“菜单项”。如下所示:

形成

上面的捕获是在测试模式下完成的,实时模式插件在加载时如下所示:

形成

为了将数据加载到第一个实时插件中,应从下拉菜单中选择“实时插件1->加载数据”。

可以看出,已将模拟数据加载到DataGridView控件中。数据模拟是通过使用软件包安装程序NBuilder 提供数据模拟扩展来实现的NuGet 

数据模拟是通过以下源代码块实现的:

public class MockData
{
    public static DataTable GenerateDataTable<T>(int rows)
    {
        var datatable = new DataTable(typeof(T).Name);

        typeof(T).GetProperties().ToList().ForEach(x => datatable.Columns.Add(x.Name));

        Builder<T>.CreateListOfSize(rows).Build().ToList()
            .ForEach(x => datatable.LoadDataRow(x.GetType().GetProperties()
                .Select(y => y.GetValue(x, null)).ToArray(), true));

        return datatable;
    }
}

返回的DataTable内容以DataGridView 标准方式绑定到控件。还有第二个实时插件,该插件可加载图像,作为从不同程序集派生的第二个简单示例。

插件类

插件类必须继承于中IPlugin 找到的接口类Winforms.Plugins.Shared

接口的定义如下所示:

public interface IPlugin
{
    String Name();
    ControlTemplate PluginControls();
}

其中Winforms.Plugins.DemoPlugin包含一个名为的示例插件,其定义如下:

public class DataGridViewPlugin : IPlugin
{
    private ControlTemplate controlTemplate;
    private String name = String.Empty;

    public DataGridViewPlugin(String name)
    {
        this.name = name;
        controlTemplate = new ControlTemplate(this.Name(),
                                                new List<string>() { "Load Data" },
                                                new DataGridViewUserControl());
    }

    public String Name()
    {
        return this.name;
    }

    public ControlTemplate PluginControls()
    {
        return controlTemplate;
    }
}

除了该Name属性外,还有一个ControlTemplate 控件模板类,如下所示:

public class ControlTemplate
{
    public UserControlWithCallBack UserControlContainer;
    public ToolStripMenuItem MenuStripItemContainer;

    public ControlTemplate(String name, List<String>
    dropDownMenuItemNames, UserControlWithCallBack pluginUserControl)
    {
        UserControlContainer = new UserControlWithCallBack();

        UserControlContainer = pluginUserControl;

        ToolStripMenuItem topLevelMenuStripItem = new ToolStripMenuItem(name);

        foreach (String dropDownMenuItemName in dropDownMenuItemNames)
        {
            ToolStripMenuItem dropDownMenuStripItem = new ToolStripMenuItem(dropDownMenuItemName);
            dropDownMenuStripItem.Click += new EventHandler(MenuItemClickHandler);

            topLevelMenuStripItem.DropDownItems.Add(dropDownMenuStripItem);
        }

        MenuStripItemContainer = topLevelMenuStripItem;
    }

    private void MenuItemClickHandler(object sender, EventArgs e)
    {
        ToolStripMenuItem receivedMenuItem = (ToolStripMenuItem)sender;
        UserControlContainer.ReceiveData(receivedMenuItem.Text);
    }
}

ControlTemplate 类有接受一个名称的构造,为下拉菜单文本项列表,用户控件从继承UserControlWithCallBack类。这是插件类中使用的用户控件的基类,如下所示:

public partial class UserControlWithCallBack : UserControl
{
    public event EventHandler<EventArgs<String>> CallBack;

    public UserControlWithCallBack()
    {
        InitializeComponent();
    }

    public void ReceiveData(String callBackData)
    {
        CallBack.SafeInvoke(this, new EventArgs<string>(callBackData));
    }
}

然后,继承的用户控件必须订阅基类CallBack事件,如下所示:

public partial class DataGridViewUserControl : UserControlWithCallBack
{
    public DataGridViewUserControl()
    {
        InitializeComponent();
        base.CallBack += DataGridViewUserControl_CallBack;
    }

    void DataGridViewUserControl_CallBack(object sender, EventArgs<string> e)
    {
        if (e.Value == "Load Data")
        {
            DataTable testData = MockData.GenerateDataTable<Person>(50);
            dataGridViewTest.DataSource = testData;

            lblDescription.Visible = true;
            dataGridViewTest.Visible = true;
        }
    }
}

这允许用户控件的下拉菜单将数据传递给继承的用户控件。

扩展方法

public 使用了两种方法,如下所示:

namespace System
{
    public class EventArgs<T> : EventArgs
    {
        public EventArgs(T value)
        {
            _value = value;
        }

        private T _value;

        public T Value
        {
            get { return _value; }
        }
    }

    public static class Extensions
    {
        public static void SafeInvoke<T>
        (this EventHandler<T> eventToRaise, object sender, T e) where T : EventArgs
        {
            EventHandler<T> handler = eventToRaise;
            if (handler != null)
            {
                handler(sender, e);
            }
        }
    }
} 

第一种方法提供EventArgs在下拉菜单和插件类中的用户控件之间使用的typed 第二种是扩展方法,允许线程安全使用事件。

最后

我希望该示例在某种程度上被证明对编程社区有用,这是该项目的预期目的。

该示例项目实际上没有任何错误处理,但是由于这是作为概念证明而设计的,因此没有必要。

兴趣点

 

我能想到的唯一真正的兴趣点就是使用继承的用户控件。这是我以前从未使用过的东西,当我意识到需要的项目结构时,惊喜地发现继承的用户控件作为控件模板。

历史

  • 版本1.0

执照

本文以及所有相关的源代码和文件均已获得The Code Project Open License(CPOL)的许可。

分享

关于作者

百特
软件开发人员
 
英国 英国
自2000年以来,我一直是使用Microsoft技术的软件开发人员。

技能包括C / C ++,C#,VB.NET,MS SQL Server,ASP.NET Webforms / MVC,AngularJS,NodeJS,Sharepoint开发,WPF,WCF,Winforms等。

 

https://www.codeproject.com/articles/714926/csharp-winforms-plug-ins-architecture-example
posted @ 2020-04-21 09:53  岭南春  阅读(226)  评论(0)    收藏  举报