插件框架
构建桌面插件应用程序的基本框架。
更新:2018-05-17
本文最初写于8年前。我现在建议为此类项目使用 MEF(托管可扩展性框架)。但是,有些人可能会发现自己动手构建更实用。因此,我已将代码复制到 GitHub,供仍认为它有用并可能希望分支代码的任何人使用。
引言
本文将演示如何使用 PluginFramework
创建一个基本的 WinForms 插件应用程序。这并非旨在成为一个完美的解决方案,但它是一个朝着这个方向迈出的非常好的开端,如果我必须这么说!它应该能帮助您走上正确的轨道。我早就想写这个了,但一直没时间;事实上,我现在仍然没多少时间,所以如果文章在阐述方面有所欠缺,您可得原谅我!
接口
首先,我们需要为加载插件定义一个公共接口。所有接口都定义在 PluginFramework.Interfaces
项目中。这样,主机和插件都可以引用一个单独的项目(我们不希望接口位于主应用程序中,因为这样一来,每个构建的插件都必须引用插件主机!)。
IPlugin
这简直是最简单的了……这个接口将用于确保所有代码都能很好地协同工作。
public interface IPlugin
{
string Title { get; }
string Description { get; }
string Group { get; }
string SubGroup { get; }
XElement Configuration { get; set; }
string Icon { get; }
void Dispose();
}
Title
:插件的名称Description
:显而易见,不是吗?Group
:这允许您将相关的插件组合在一起(可以想象成MenuStrip
和菜单项)。SubGroup
:这不难理解,对吧?Configuration
:这允许您与插件之间传递配置详细信息。例如,主机可以在加载插件时提供配置,并在项目卸载时,将最新的配置传回主机以保存到磁盘供以后使用。Icon
:图标文件的 URI,例如,可用于TreeView
或MenuStrip
控件。
IFormPlugin
public enum ShowAs
{
Normal,
Dialog
}
public interface IFormPlugin: IPlugin
{
Form Content { get; }
ShowAs ShowAs { get; }
}
现在我们来看具体细节……
Content
:一个窗体控件,将被加载为插件
IUserControlPlugin
public interface IUserControlPlugin: IPlugin
{
UserControl Content { get; }
}
Content
:一个用户控件,将被加载为插件
属性
这将在尝试加载程序集文件时使用。为程序集添加正确的属性意味着您可以确信不会尝试加载恶意 DLL。它还可以帮助您从程序集中定位要加载的控件。
[AttributeUsage(AttributeTargets.Assembly)]
public class MainContentAttribute : Attribute
{
public string Content { get; set; }
public MainContentAttribute(string mainContent)
{
this.Content = mainContent;
}
}
创建插件时,只需在您的 AssemblyInfo 文件中添加类似以下内容:
[assembly: MainContent("DemoUserControlPlugin.UserControl1")]
其中 UserControl1
继承自 IUserControlPlugin
。
您还可以添加其他属性来检查插件版本等,但我将把这些留给您。
实用程序
配置文件
配置文件类允许您轻松加载和保存插件配置,甚至允许您指定在启动时加载哪些插件。
以下是一个示例配置文件
<ConfigurationFile>
<Startup>
<Plugin Title="DemoFormPlugin"
AssemblyPath="D:\My Documents\Visual Studio 2008\Projects\
PluginFramework\Demo\bin\Debug\
Plugins\DemoFormPlugin.dll" />
<Plugin Title="UserControlTest"
AssemblyPath="D:\My Documents\Visual Studio 2008\Projects\
PluginFramework\Demo\bin\Debug\Plugins\
DemoUserControlPlugin.dll" />
</Startup>
<PluginConfiguration>
<Plugin Title="DemoFormPlugin">
<Configuration>
<ThisFormConfig />
</Configuration>
</Plugin>
<Plugin Title="UserControlTest">
<Configuration>
<UCConfig />
</Configuration>
</Plugin>
</PluginConfiguration>
</ConfigurationFile>
<UCConfig />
和 <ThisFormConfig />
这两项与 IPlugin
接口中的 Configuration
属性相关。
PluginHelper
现在,这才是真正的工作所在,您会惊讶于它的简单。
public static class PluginHelper
{
private static string pluginsDirectory = Path.GetDirectoryName(
Assembly.GetExecutingAssembly().GetName().CodeBase).Substring(6);
public static string PluginsDirectory
{
get { return pluginsDirectory; }
set { pluginsDirectory = value; }
}
/// <summary>
/// Returns a new plugin and the assembly location.
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
public static PluginInfo AddPlugin(string file)
{
Assembly assembly = Assembly.LoadFile(file);
//PluginVersionAttribute version =
// (PluginVersionAttribute)Attribute.GetCustomAttribute(assembly,
// typeof(PluginVersionAttribute));
//if (version != null && version.VersonNumber == "1.0.0")
//{
MainContentAttribute contentAttribute =
(MainContentAttribute)Attribute.GetCustomAttribute(
assembly,typeof(MainContentAttribute));
IPlugin plugin =
(IPlugin)assembly.CreateInstance(contentAttribute.Content, true);
PluginInfo pluginInfo = new PluginInfo();
pluginInfo.AssemblyPath = file;
pluginInfo.Plugin = plugin;
return pluginInfo;
//}
//else
//{
// return null;
// //txtDescription.Text = "You tried to load an unsupported assembly";
//}
}
/// <summary>
/// Creates a new instance of the plugin inside the specified assembly file
/// </summary>
/// <typeparam name="T">Form / UserControl</typeparam>
/// <param name="assemblyFile">The assembly file to load</param>
/// <returns></returns>
public static T CreateNewInstance<T>(string assemblyFile)
{
Assembly assembly = Assembly.LoadFile(assemblyFile);
MainContentAttribute contentAttribute =
(MainContentAttribute)Attribute.GetCustomAttribute(assembly,
typeof(MainContentAttribute));
T item = (T)assembly.CreateInstance(contentAttribute.Content, true);
return item;
}
/// <summary>
/// <para>Looks for plugins in the directory
/// specified by the PluginsDirectory</para>
/// <para>property</para>
/// </summary>
/// <returns>an IDictionary with plugin Title
/// as the Key and Assembly path as the Value</returns>
public static IDictionary<string, string> FindPlugins()
{
Dictionary<string, string> plugins =
new Dictionary<string, string>();
PluginInfo pluginInfo;
foreach (string file in Directory.GetFiles(PluginsDirectory))
{
FileInfo fileInfo = new FileInfo(file);
if (fileInfo.Extension.Equals(".dll"))
{
try
{
pluginInfo = AddPlugin(file);
plugins.Add(pluginInfo.Plugin.Title, file);
}
catch
{
}
}
}
return plugins;
}
/// <summary>
/// Gets all plug-ins from the PluginDirectory
/// </summary>
/// <returns></returns>
public static IDictionary<string, PluginInfo> GetPlugins()
{
Dictionary<string, PluginInfo> plugins =
new Dictionary<string, PluginInfo>();
PluginInfo pluginInfo;
foreach (string file in Directory.GetFiles(PluginsDirectory))
{
FileInfo fileInfo = new FileInfo(file);
if (fileInfo.Extension.Equals(".dll"))
{
try
{
pluginInfo = AddPlugin(file);
plugins.Add(pluginInfo.Plugin.Title, pluginInfo);
}
catch
{
}
}
}
return plugins;
}
/// <summary>
/// Gets the specified plugins
/// </summary>
/// <param name="pluginsToLoad">List of assembly paths</param>
/// <returns></returns>
public static IDictionary<string, PluginInfo>
GetPlugins(IEnumerable<string> pluginsToLoad)
{
Dictionary<string, PluginInfo> plugins =
new Dictionary<string, PluginInfo>();
PluginInfo pluginInfo;
foreach (string file in pluginsToLoad)
{
FileInfo fileInfo = new FileInfo(file);
if (fileInfo.Extension.Equals(".dll"))
{
try
{
pluginInfo = AddPlugin(file);
plugins.Add(pluginInfo.Plugin.Title, pluginInfo);
}
catch
{
}
}
}
return plugins;
}
}
public class PluginInfo
{
public IPlugin Plugin { get; set; }
public string AssemblyPath { get; set; }
}
是的,它还需要一些工作。如果我有时间,我会清理它,但这是为了给您提供一个基本的、可工作的插件应用程序构建框架。您可以根据需要自定义。无论如何,它工作得足够好。
辅助控件
为了真正提供帮助,这里有一些将自动加载 IPlugin
的控件
PluginMenuStrip
只需将其中一个添加到您的窗体中,从代码隐藏中调用 AddPlugin
方法……瞧;您现在有了一个新的菜单项,一旦点击它就会激活您的插件!
public class PluginMenuStrip : MenuStrip
{
public void AddPlugin(PluginInfo pluginInfo)
{
ToolStripMenuItem pluginItem =
new ToolStripMenuItem(pluginInfo.Plugin.Title);
pluginItem.Tag = pluginInfo;
if (!string.IsNullOrEmpty(pluginInfo.Plugin.Icon))
{
pluginItem.Image = Image.FromFile(pluginInfo.Plugin.Icon);
}
if (pluginInfo.Plugin is IFormPlugin)
{
pluginItem.Click += new EventHandler(pluginItem_Click);
}
if (!string.IsNullOrEmpty(pluginInfo.Plugin.SubGroup))
{
ToolStripMenuItem subGroup =
new ToolStripMenuItem(pluginInfo.Plugin.SubGroup);
subGroup.DropDownItems.Add(pluginItem);
if (!string.IsNullOrEmpty(pluginInfo.Plugin.Group))
{
ToolStripMenuItem group =
new ToolStripMenuItem(pluginInfo.Plugin.Group);
group.DropDownItems.Add(subGroup);
this.Items.Add(group);
}
else
{
this.Items.Add(subGroup);
}
}
else
{
this.Items.Add(pluginItem);
}
}
void pluginItem_Click(object sender, EventArgs e)
{
ToolStripMenuItem menuItem = sender as ToolStripMenuItem;
PluginInfo pluginInfo = menuItem.Tag as PluginInfo;
IFormPlugin plugin = pluginInfo.Plugin as IFormPlugin;
Form form = plugin.Content;
if (form.IsDisposed)
{
form =
PluginHelper.CreateNewInstance<Form>(pluginInfo.AssemblyPath);
}
if (plugin.ShowAs == ShowAs.Dialog)
{
form.ShowDialog();
}
else
{
form.Show();
}
}
}
PluginTreeView
与 PluginMenuStrip
的代码几乎相同。此控件将通过 AddPlugin
方法加载插件。但是,由于没有标准方法来显示 UserControl
,您必须在插件主机(您的应用程序)中自己编写该代码。您可以从当前 TreeNode
的 Tag
属性中获取当前选定的 IPlugin
。
public class PluginTreeView: TreeView
{
ImageList imageList = new ImageList();
protected override void OnCreateControl()
{
base.OnCreateControl();
this.ImageList = imageList;
imageList.Images.Add(Resources.Tree);
}
public void AddPlugin(PluginInfo pluginInfo)
{
TreeNode pluginItem = new TreeNode(pluginInfo.Plugin.Title);
pluginItem.Tag = pluginInfo;
if (!string.IsNullOrEmpty(pluginInfo.Plugin.Icon))
{
imageList.Images.Add(new Icon(pluginInfo.Plugin.Icon));
pluginItem.ImageIndex = imageList.Images.Count - 1;
pluginItem.SelectedImageIndex = imageList.Images.Count - 1;
}
if (!string.IsNullOrEmpty(pluginInfo.Plugin.SubGroup))
{
TreeNode subGroup = new TreeNode(pluginInfo.Plugin.SubGroup);
subGroup.Nodes.Add(pluginItem);
if (!string.IsNullOrEmpty(pluginInfo.Plugin.Group))
{
TreeNode group = new TreeNode(pluginInfo.Plugin.Group);
group.Nodes.Add(subGroup);
this.Nodes.Add(group);
}
else
{
this.Nodes.Add(subGroup);
}
}
else
{
this.Nodes.Add(pluginItem);
}
}
}
一个示例
一个示例 IUserControlPlugin
public partial class UserControl1 : UserControl, IUserControlPlugin
{
public UserControl1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("You clicked button 1!");
}
private void button2_Click(object sender, EventArgs e)
{
MessageBox.Show("You clicked button 2!");
}
#region IUserControlPlugin Members
public UserControl Content
{
get { return this; }
}
#endregion
#region IPlugin Members
public string Title
{
get { return "UserControlTest"; }
}
public string Description
{
get { return "Info about this user control plugin"; }
}
public string Group
{
get { return "UCGroup"; }
}
public string SubGroup
{
get { return "UCSubGroup"; }
}
private XElement configuration = new XElement("UCConfig");
public XElement Configuration
{
get { return configuration; }
set { configuration = value; }
}
public string Icon
{
get { return "C:\\Icons\\Globe.ico"; }
}
#endregion
}
以及一个演示插件主机
public partial class DemoForm : Form
{
private ConfigurationFile configFile = null;
private IDictionary<string, PluginInfo> plugins = null;
private IDictionary<string, string> startupPlugins = null;
public DemoForm()
{
InitializeComponent();
PluginHelper.PluginsDirectory =
Path.Combine(Application.StartupPath, "Plugins");
}
private void pluginTreeView_AfterSelect(object sender, TreeViewEventArgs e)
{
if (e.Node.Tag == null)
{ return; }
PluginInfo pluginInfo = e.Node.Tag as PluginInfo;
if (pluginInfo.Plugin is IUserControlPlugin)
{
UserControl control = ((IUserControlPlugin)pluginInfo.Plugin).Content;
splitContainer1.Panel2.Controls.Clear();
splitContainer1.Panel2.Controls.Add(control);
control.Dock = DockStyle.Fill;
}
else if (pluginInfo.Plugin is IFormPlugin)
{
IFormPlugin formPlugin = (IFormPlugin)pluginInfo.Plugin;
Form form = formPlugin.Content;
if (form.IsDisposed)
{
form = PluginHelper.CreateNewInstance<Form>(
pluginInfo.AssemblyPath);
}
if (formPlugin.ShowAs == ShowAs.Dialog)
{
form.ShowDialog();
}
else
{
form.Show();
}
}
}
private void LoadPlugins(IEnumerable<string> assemblyPaths)
{
plugins = PluginHelper.GetPlugins(assemblyPaths);
foreach (PluginInfo pluginInfo in plugins.Values)
{
if (pluginInfo.Plugin is IFormPlugin)
{
pluginMenuStrip.AddPlugin(pluginInfo);
pluginTreeView.AddPlugin(pluginInfo);
}
else if (pluginInfo.Plugin is IUserControlPlugin)
{
pluginTreeView.AddPlugin(pluginInfo);
}
}
}
private void mnuToolsOptions_Click(object sender, EventArgs e)
{
AvailablePluginsForm form = new AvailablePluginsForm();
if (form.ShowDialog() == DialogResult.OK)
{
startupPlugins = form.SelectedPlugins;
LoadPlugins(form.SelectedPlugins.Values);
}
}
private void DemoForm_Load(object sender, EventArgs e)
{
if (File.Exists(Settings.Default.PluginConfigFile))
{
configFile = ConfigurationFile.Load(Settings.Default.PluginConfigFile);
LoadPlugins((from x in configFile.Startup.Plugins
select x.AssemblyPath).ToList());
}
else
{
configFile = new ConfigurationFile();
configFile.Save(Settings.Default.PluginConfigFile);
}
}
protected override void OnClosing(CancelEventArgs e)
{
if (startupPlugins != null)
{
foreach (KeyValuePair<string, string> kv in startupPlugins)
{
if (!configFile.Startup.Plugins.Contains(kv.Key))
{
StartupPlugin plugin = new StartupPlugin();
plugin.Title = kv.Key;
plugin.AssemblyPath = kv.Value;
configFile.Startup.Plugins.Add(plugin);
}
}
}
foreach (KeyValuePair<string, PluginInfo> kv in plugins)
{
PluginConfig config = configFile.PluginConfiguration.Plugins[kv.Key];
if (config == null)
{
config = new PluginConfig();
config.Title = kv.Key;
configFile.PluginConfiguration.Plugins.Add(config);
}
config.Configuration = kv.Value.Plugin.Configuration;
}
configFile.Save(Settings.Default.PluginConfigFile);
base.OnClosing(e);
}
}
就这样!是的,我可能会因为写得字少而得分不高,但您不能抱怨它不是一个易于使用的框架! ;-) 尽情享用吧!如果您有任何改进意见,我将非常乐意倾听。
历史
v1.1 - 2010 09 14
根据各种请求,我对代码进行了更新,使用了一个新版本来解决一些错误;是的,那些图标已修复(我从来不完全明白为什么一个如此脱离文章主题的东西对一些人来说如此重要,但嘿……)。第一次加载主机时也存在一个问题;关闭时有时会崩溃。现在已解决。
作为额外奖励,现在有一个 ISettingsPlugin
接口,以便用户可以更改设置,还有一个 SettingsForm
可以为您加载设置插件(您始终可以将插件做成标签页形式,但我认为那样会很混乱)。要创建设置插件,请执行与常规插件相同的操作:
- 创建您的用户控件。
- 实现
ISettingsPlugin
。 - 使用
SettingsContentAttribute
而不是MainContentAttribute
向程序集添加 Content Attribute。 - 大功告成。
- 完成后,通过点击“插件设置”菜单项,使用 Host.exe 进行测试。