C#Winforms插件式架构示例
介绍
在这里,我们有一个用于C#.NET 4.5 Winforms平台的插件体系结构项目的示例实现。该项目是在Visual Studio 2013中创建的,但将在Visual Studio 2012中打开并运行,没有任何问题。
背景
我前段时间听说过插件架构,并认为这是一个好主意。因此,我决定创建一个示例C#Winforms插件项目,但要针对现实情况进行实际设计。
考虑到这一点,我决定创建一个具有用户控件和单个下拉菜单的插件系统,这两个系统都包含在一个监督类中。该项目的设计方式是,宿主项目不知道插件类的操作,仅从测试类或外部类库(DLL)加载插件。
插件的设计方式是,下拉菜单可以将键入的事件发送到用户控件,以告诉用户已选择了哪个下拉菜单项。
使用代码
这是该解决方案及其所有项目的图像:
该解决方案包含五个项目,两个Winforms宿主项目和三个类库。主项目和Winforms宿主项目是Winforms.Plugins.Host从本地测试类或类库文件(DLL)加载插件的位置。
还有一个名为的测试宿主项目Winforms.Plugins.DemoPlugin.TestHarness,该项目用于在将插件类库加载到宿主项目之前对其进行测试。
这三个类库由两个演示插件库和一个共享类库组成。我认为它们的名称表示哪个是哪个。
宿主项目(winforms.plugins.host)的唯一功能是加载插件,并使用插件中的用户控件填充Tab控件,然后使用每个单独的下拉菜单或菜单项填充Menu Strip控件。
宿主项目由一个主窗体,一个继承的用户控件和一个本地测试插件类组成,如下所示:
还有一个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.
}
}
}
主机应用程序一旦建立testMode和pluginFilePath成员变量,便会使用其Form.Text属性将模式通知用户。可以看出,“测试模式”是从该App.config appSettings部分派生而来的。
注意:pluginFilePath成员再次使用相对路径来确保可移植性。
启动应用程序时,它看起来像这样:
可以看到有三个按钮,第一个是“将插件实例化到容器上”。在实时模式下,它使用反射来读取找到的程序集中的类。如果它们从类库中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)的许可。


浙公网安备 33010602011771号