C# Winforms 插件架构示例
C# .NET 4.5 Winforms 插件架构示例
引言
这里有一个 C# .NET 4.5 Winforms 平台的插件架构项目的示例实现。该项目是在 Visual Studio 2013 中创建的,但也可以在 Visual Studio 2012 中打开并运行,没有任何问题。
背景
我之前听说过插件架构,觉得是个好主意。于是我决定创建一个示例 C# Winforms 插件项目,但采用适合真实场景的实际设计。
考虑到这一点,我决定创建一个插件系统,其中包含一个用户控件和一个单独的下拉菜单,两者都包含在一个监管类中。该项目的设计方式使得宿主项目不知道插件类的操作,并且只从测试类或外部类库(DLL)加载插件。
插件的设计方式使得下拉菜单可以将一个类型化事件发送到用户控件,告知它哪个下拉项已被选中。
Using the Code
这是解决方案及其所有项目的图像

该解决方案包含五个项目,两个 Winforms 宿主项目和三个类库。主要的 Winforms 宿主项目是 `Winforms.Plugins.Host`,插件将在此处加载,无论是从本地测试类还是从类库文件(DLL)加载。
还有一个名为 `Winforms.Plugins.DemoPlugin.TestHarness` 的测试宿主项目,用于在插件类库加载到宿主项目之前对其进行测试。
三个类库包括两个演示插件库和一个共享类库。我认为它们的名称足以表明它们各自的功能。
宿主项目(`winforms.plugins.host`)的唯一功能是加载插件,用插件的用户控件填充选项卡控件,然后用每个单独的下拉菜单或菜单项填充菜单栏控件。
宿主项目包含一个主窗体、一个继承的用户控件和一个本地测试插件类,如下所示

还有一个 `PluginsToConsume` 文件夹,包含插件的类库(DLL)应在启动前放置在此处。
=================
注意
演示插件类库通过一个 Post-Build 步骤的 shell 脚本命令将它们的程序集复制到宿主项目的 `PluginsToConsume` 文件夹中。下面是一个示例
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` 成员使用了相对路径以确保可移植性。
当应用程序启动时,它看起来像这样

如可见,有三个按钮,第一个是“将插件实例化到容器”。在 Live 模式下,这会使用反射读取它找到的程序集中的类。如果它们继承自 `IPlugin`(一个在 `Winforms.Plugins.Shared` 类库中找到的接口),那么它会尝试将它们作为插件来使用。
接下来应该按下“将插件加载到窗体”按钮,然后将派生的任何插件数据加载到宿主窗体中。还有一个“清空容器”按钮,用于删除本地缓存的已存储插件数据。
注意:插件部分的完整解释将稍后包含。
为了存储插件数据,宿主窗体使用了一个 Microsoft Unity 依赖注入容器。我认为,鉴于插件的理念与依赖注入中使用的 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
在此步骤之后,应按下“将插件加载到窗体”按钮,然后执行以下代码
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 容器不为空,则清除宿主选项卡控件和菜单栏控件的项目,然后用加载的插件填充它们。
对于宿主应用程序遇到的每个插件类,它会创建一个新的选项卡页,并为每个插件创建一个下拉菜单或菜单项。如下所示

上面的截图是在测试模式下完成的,Live 模式下的插件加载后看起来是这样的

为了将数据加载到第一个 Live 插件中,应从下拉菜单中选择“Live Plugin 1 -> Load Data”。
如可见,在 `DataGridView` 控件中加载了模拟数据。数据模拟是通过使用 `NuGet` 包安装程序提供的 `NBuilder` 数据模拟扩展实现的。
数据模拟通过以下源代码块实现
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` 控件。还有一个第二个 Live 插件,它从另一个程序集中加载图像作为第二个简单示例。
插件类
插件类必须继承自 `Winforms.Plugins.Shared` 中的一个名为 `IPlugin` 的接口类。
接口的定义如下所示
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`,用于在下拉菜单和插件类中的用户控件之间传递。第二个是一个扩展方法,用于线程安全地使用事件。
最后
我希望这个例子能以某种方式对编程社区有用,这也是该项目的预期目的。
示例项目实际上并没有多少错误处理,但由于这是为了概念验证而设计的,所以没有觉得有必要。
兴趣点
唯一真正值得关注的一点是继承用户控件的使用。这是我以前没有用过的,当我意识到我需要的项目结构时,我很惊喜地发现继承用户控件作为一种控件模板。
历史
- 版本 1.0