UPDATE: 2018-05-17
This article was originally written 8 years ago. I now recommend using MEF (Managed Extensibility Framework) for these kinds of projects. However, some people may still find rolling their own more useful. As such, I have copied the code to GitHub for anyone who still finds it useful and may wish to fork the code.
Introduction
This article will demonstrate how to create a basic plug-in application for WinForms using the PluginFramework
. This is not intended to be a perfect solution, but it is a mighty good start in that direction, if I must say so myself! It should help you get started on the right track. I have been meaning to write this for quite some time, but never had the time; in fact, I still don't have much time, so you'll have to forgive me if the article is somewhat lacking in elaboration!
Interface
First of all, we need to define a common interface for loading plug-ins. All interfaces are defined in the PluginFramework.Interfaces
project. This way, both the host and the plug-ins can reference a separate project (we wouldn't want the interfaces in the host application, as this would mean each plug-in built would need to reference the plug-in host!).
IPlugin
About as simple as it gets... this is the interface that will be used to make sure all the code can play nice.
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
: Name of the plug-in Description
: Obvious, right? Group
: This allows you to group related plug-ins together (think of a MenuStrip
and menu items). SubGroup
: Not too hard to figure out what this is, is it? Configuration
: This allows you to pass configuration details to and from your plug-in. For example, the host could supply the configuration on plug-in load, and when disposing the project, the latest configuration can be passed back to the host for saving to disk for later use. Icon
: URI to an icon file, which can be used on a TreeView
or MenuStrip
control, for example.
IFormPlugin
public enum ShowAs
{
Normal,
Dialog
}
public interface IFormPlugin: IPlugin
{
Form Content { get; }
ShowAs ShowAs { get; }
}
Now we get to specifics...
Content
: A form control, which is to be loaded as a plug-in
IUserControlPlugin
public interface IUserControlPlugin: IPlugin
{
UserControl Content { get; }
}
Content
: A user control, which is to be loaded as a plug-in
Attributes
This will be used when trying to load assembly files. Marking the assembly with the correct attributes means you are sure to not try to load a rogue DLL. It also helps you locate the control to load from the assembly.
[AttributeUsage(AttributeTargets.Assembly)]
public class MainContentAttribute : Attribute
{
public string Content { get; set; }
public MainContentAttribute(string mainContent)
{
this.Content = mainContent;
}
}
When creating plug-ins, simply add something like this to your AssemblyInfo file:
[assembly: MainContent("DemoUserControlPlugin.UserControl1")]
where UserControl1
inherits IUserControlPlugin
.
You could also add other attributes to check for plug-in versions, etc., but I will leave that up to you.
Utilities
Configuration File
The configuration file class allows you to easily load and save plug-in configuration, and even lets you specify which plug-ins to load on startup.
Below is an example configuration file:
<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>
The items <UCConfig />
and <ThisFormConfig />
relate to the configuration property in the IPlugin
interface.
PluginHelper
Now this is where the real work gets done, and you'll be surprised at just how simple it is.
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; }
}
public static PluginInfo AddPlugin(string file)
{
Assembly assembly = Assembly.LoadFile(file);
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;
}
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;
}
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;
}
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;
}
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; }
}
Yes, it needs some work. If I had the time, I'd clean it up, but this is intended to give you a basic working framework for building a plug-in app. You are welcome to customize as you see fit. In any case, it works well enough.
Helper Controls
Just to be really helpful, here are some controls that will auto-load an IPlugin
:
PluginMenuStrip
Simply add one of these to your form, call the AddPlugin
method from your code-behind... and voila; you now have a new menu item that once clicked will activate your plug-in!
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
Pretty much the same code as with the PluginMenuStrip
. This control will load a plug-in via the AddPlugin
method. However, as there is no standard way to show a UserControl
, you will have to write that code yourself from the plug-in host (your app). You can get the currently selected IPlugin
from the current TreeNode
's Tag
property.
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);
}
}
}
An Example
An example 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
}
And a demo plug-in host:
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);
}
}
And there you have it! Yes, I may get less points for not writing more words, but you can't complain that it isn't an easy-to-use framework! ;-) Enjoy! And if you have any improvements, I would be more than happy to hear them.
History
v1.1 - 2010 09 14
Due to various requests, I have updated the code with a newer version that addresses some bugs; yes, those icons are fixed (I never did quite get why something so off scope of the article was so important to some, but hey...). There was also an issue when loading the host the first time around; it would sometimes crash when closing. This is now resolved.
As an added bonus, there is now an ISettingsPlugin
interface so that the user can change settings as well as a SettingsForm
that will load the settings plugins for you (You could always make your plugin tabbed, but I think that just gets messy). To create a Settings plugin, do the same as with a regular plugin:
- Create your user control.
- Implement
ISettingsPlugin
. - Add Content Attribute to Assembly using the
SettingsContentAttribute
instead of the MainContentAttribute
. - Away you go.
- When you're done, test it with the Host.exe by clicking on the Plugin Settings menu item.