Reputation: 211
I'm developing an application that makes heavy use of plugins. The app is in C# and I'm thinking about building the configuration GUI in WPF. I got the plugin architecture down in terms of how to manage the actual plugins themselves. However, each plugin has its own configuration, and that's where I'm looking for help.
The plugin architecture is simple -- there's an interface that plugins implement and I just load all the plugins inside a directory. However, where do the plugins get their configuration from? I'd like to have some generic way of handling this, so that each plugin isn't responsible for reading its own configuration file. Also, I'd like the GUI to expand with each plugin -- that is, each plugin installed should add a tab to the GUI with the specific configuration options for that plugin. Then upon save, the configuration file(s) would be saved.
What's my best way of going about this?
Upvotes: 21
Views: 10414
Reputation: 405
MEF would be the best choice for creating extensible applications (plugin architecture).
Another way for smaller applications would be using reflection we can have the pluggable design. This article explains pretty well.
Upvotes: 1
Reputation: 1901
I'll answer for the question about the GUI having a tab for each plugin. Basically, I have attached a behavior class for my WPF control tab and then iterated for each plugin, which has a viewmodel wrapper with an IsEnabled property and a method Icon() declared inside its interface.
the behavior class
class TabControlBehavior : Behavior<TabControl>
{
public static readonly DependencyProperty PluginsProperty =
DependencyProperty.RegisterAttached("Plugins", typeof(IEnumerable<PluginVM>), typeof(TabControlBehavior));
public IEnumerable<PluginVM> Plugins
{
get { return (IEnumerable<PluginVM>)GetValue(PluginsProperty); }
set { SetValue(PluginsProperty, value); }
}
protected override void OnAttached()
{
TabControl tabctrl = this.AssociatedObject;
foreach (PluginVM item in Plugins)
{
if (item.IsEnabled)
{
byte[] icon = item.Plugin.Icon();
BitmapImage image = new BitmapImage();
if (icon != null && icon.Length > 0)
{
image = (BitmapImage)new Yemp.Converter.DataToImageConverter().Convert(icon, typeof(BitmapImage), null, CultureInfo.CurrentCulture);
}
Image imageControl = new Image();
imageControl.Source = image;
imageControl.Width = 32;
imageControl.Height = 32;
TabItem t = new TabItem();
t.Header = imageControl;
t.Content = item.Plugin.View;
tabctrl.Items.Add(t);
}
}
}
protected override void OnDetaching()
{
}
}
the plugin interface
public interface IPlugin
{
string Name { get; }
string AuthorName { get; }
byte[] Icon();
object View { get; }
}
and the PluginVM class
public class PluginVM : ObservableObjectExt
{
public PluginVM(IPlugin plugin)
{
this.Plugin = plugin;
this.IsEnabled = true;
}
public IPlugin Plugin { get; private set; }
private bool isEnabled;
public bool IsEnabled
{
get { return isEnabled; }
set {
isEnabled = value;
RaisePropertyChanged(() => IsEnabled);
}
}
}
The tabcontrol will display only enabled plugins with its custom icon returned by Icon() method.
The code of the Icon() method within the plugin implementation should look like this:
public byte[] Icon()
{
var assembly = System.Reflection.Assembly.GetExecutingAssembly();
byte[] buffer = null;
using (var stream = assembly.GetManifestResourceStream("MyPlugin.icon.png"))
{
buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
}
return buffer;
}
Upvotes: 0
Reputation: 6353
The problem with not wanting each plugin to read its own configuration file is that you have to have one central file that knows what configuration settings to include; this goes against the concept of a plugin, where I give you a plugin and you know nothing about it.
That's not the same, of course, as a central config file with information about what plugins to load.
Contrary to what others have said, there are ways to have DLLs read config setting from their own config file, but if you specifically don't want to do that I won't get into them.
I'm surprised nobody's mentioned the registry. It's the family secret everyone wants to keep locked in the basement, but that's exactly what it was meant for: A centralized location for storing application settings that anyone (i.e. any plugin) can access.
However. If you want the main application to access and store the plugin configurations, the best way (IMO) is to have an interface to query the plugin's properties (and to pass them back). If you keep it generic, you can reuse it for other apps.
interface IExposedProperties {
Dictionary<string,string> GetProperties();
void SetProperties(Dictionary<string,string> Properties);
}
The main app would query each plugin for a set of properties, then add/update settings to its config file, as such:
private void SavePluginSettings(List<IExposedProperties> PluginProperties) {
// Feel free to get fancy with LINQ here
foreach (IExposedProperties pluginProperties in PluginProperties) {
foreach (KeyValuePair<string,string> Property in pluginProperties.GetProperties()) {
// Use Property.Key to write to the config file, and Property.Value
// as the value to write.
//
// Note that you will need to avoid Key conflict... you can prepend
// the plugin name to the key to avoid this
}
}
}
I haven't tested that yet, just threw it down quick in a few free moments; I'll check it later to make sure there aren't any dumb mistakes. But you get the idea.
As for expanding the GUI, other suggestions have pretty much nailed it... the choice is whether you want to pass the app's control to the plugin, or query the plugin for a tab page and add it.
Typically you'd see the app pass an object to the plugin... caveat being that it must be an object over which the plugin is allowed complete control. I.E. you wouldn't pass the tab control itself, but you could have the app create the tab page and pass it to the plugin, saying basically 'do what you want with it'.
The 'return null if no tab' argument carries some weight here, but I prefer to have the app handle the creation and cleanup of all objects it uses. Can't give you a good reason why... one of those arbitrary style preferences, maybe. I wouldn't criticize either approach.
interface IPluginDisplay {
bool BuildTab(TabPage PluginTab); // returns false if couldn't create
} // tab or no data
interface IPluginDisplay {
TabPage GetTab(); // creates a tab and returns it
}
HTH,
James
P.S. Thought I would mention the mother of all plugin apps: Windows Explorer
You name it, explorer's got it. Any app can add property pages to any file type, the right-click menus dynamically query DLLs for entries... if you want perspective on how flexible plugins can be, read up on Shell Programming. Some excellent articles at Code Project... Part V talks about adding tabs to the property page dialog for files.
http://www.codeproject.com/Articles/830/The-Complete-Idiot-s-Guide-to-Writing-Shell-Extens
.
Upvotes: 4
Reputation: 8474
What about creating a custom configuration section for each class that implements the shared interface?
Something like:
<configuration>
<configSections>
<add name="myCustomSection" type="MySharedAssembly.MyCustomSectionHandler, MySharedAssembly"/>
</configSections>
<myCustomSection>
<add name="Implementation1"
type="Assembly1.Implementation1, Assembly1"/>
settings="allowRead=false;allowWrite=true;runas=NT AUTHORITY\NETWORK SERVICE"/>
<add name="Implementation2"
type="Assembly1.Implementation2, Assembly1"/>
settings="allowRead=false;allowWrite=false;runas=NT AUTHORITY\LOCAL SERVICE"/>
</myCustomSection>
....
Have an Initialize(IDictionary<string,string> keyValuePairs)
method, to which you pass the configuration section name into it. It can load its associated section then using
var section = ConfigurationManager.GetSection("/myCustomSection") as MyCustomSection
foreach( var addElement in section.Names )
{
var newType = (IMyInterface)Activator.CreateInstance(addElement.Type);
var values = (from element in addElement.Value.Split(';')
let split = element.Split('=')
select new KeyValuePair<string,string>(split[0], split[1]))
.ToDictionary(k => k.Key, v => v.Value);
newType.Initialize( values );
//... do other stuff
}
Upvotes: 2
Reputation: 44374
The way I see it, this question is best solved as two separate problems.
The basic approach here is to have each plugin define a property that will return a Panel
for the host to display in a tab.
interface IPlugin
{
...
System.Windows.Controls.Panel ConfigurationPanel { get; }
}
The host could create a tab for every loaded plugin, and then place each plugin's configuration panel in the correct tab.
There are many possibilities here. For example, a plugin could signal that it does not have any configuration options by returning null
. The host would of course need to check this and decide whether or not to create a tab.
Another similar approach is to define a method instead of a property. In this case, the method would be called every time the configuration dialog is shown, and would return a new Panel instance. This would probably be slower, and not a good idea unless the property approach causes issues with adding/removing an element to the visual tree multiple times. I have not had those issues, so you should be good with a property.
It may not be necessary to provide this flexibility. You could, as has been mentioned, offer an API for plugins to build their configuration dialog from fields such as text boxes, check boxes, combo boxes, etc. This would probably end up being a more complicated approach than a simple Panel
property.
You would probably also need a callback to the plugin when the configuration is closed, telling it to save its configuration data, which I will get to soon. Here is an example:
interface IPlugin
{
...
// Inside this method you would get the data from the Panel,
// or, even better, an object bound to the Panel.
// Then you would save it with something like in the next section.
void SaveConfiguration();
}
This could be solved by an API something like the following:
static class Configuration
{
static void Store(string key, object data);
static object Retrieve(string key);
}
The host would then control reading and writing the configuration to disk.
Example usage from the plugin :
Configuration.Store("Maximum", 10);
int max = (int)Configuration.Retrieve("Maximum");
This would enable the configuration data to act as a dictionary. You would likely be able to make use of serialization to store arbitrary objects in a file. Alternatively, you could restrict configuration to storing strings or something similar. It would depend on what type of configuration needs to be stored.
The simplest approach has some security issues. Plugins would be able to read and modify other plugins' data. However, if the host prefixes keys by a plugin ID or something similar, you would be able to effectively divide the configuration into each plugin's separate container.
Upvotes: 3
Reputation: 52133
Define an interface for configurable plugins:
public interface IConfigurable
{
public void LoadConfig(string configFile);
public void ShowConfig();
// Form or whatever, allows you to integrate it into another control
public Form GetConfigWindow();
}
The just invoke the IConfigurable interface for configurable plugins.
If you want you can make the interface work the other way, making the main application provide a container (a frame or a dock for example) to the plugin for it too feel, but I would recommend the other way around.
public interface IConfigurable
{
void LoadConfig(string configFile);
void ShowConfig(DockPanel configurationPanel);
}
Finally you can do it the hard way, by defining exactly what the plugin can offer as configuration option.
public interface IMainConfigInterop
{
void AddConfigurationCheckBox(ConfigurationText text);
void AddConfigurationRadioButton(ConfigurationText text);
void AddConfigurationSpinEdit(Confguration text, int minValue, int maxValue);
}
public interface IConfigurable
{
void LoadConfig(string configFile);
void PrepareConfigWindow(IMainConfigInterop configInterop);
}
of course this option is the more restrictive and more secure one, since you can limit perfectly how the plugin is able to interact with the configuration window.
Upvotes: 8
Reputation: 5424
I typically follow the approach of adding an IConfigurable (or whatever) type of interface to my plugin. However, I don't think I'd let it show it's own GUI. Rather, allow it to expose a collection of settings that you render appropriately in your own GUI for configuring plugins. The plugin should be able to load with default settings. It doesn't need to function properly before it has been configured, however. Essentially, you end up loading plugins where some plugins have plugins for the settings manager. Does that make sense? Your settings manager can persist the settings for all plugins (typically to program data or user data) and can inject those values on application startup.
A "setting" interface would need to contain the information necessary to render it. This will include the category, title, description, help ID, widget, widget parameters, data ranges, etc. I've commonly used some form of delimited title to separate down into subcategories. I also find it helpful to have a Valid property and "applied value vs. unapplied value" support for allowing the cancellation of a form where all settings changed are discarded. In my current product I have settings of type Real, Bool, Color, Directory, File, List, Real, and String. They all inherit from the same base class. I've also got a rendering engine that ties each to a matching control and binds up the various parameters on each.
Upvotes: 2
Reputation: 738
The way I did my implementation to handle exactly the same problem (since dll-files cannot read their config files) was that I defined two interfaces one for the plugin IPlugin
and one for the host IHost
. The plugin was created and initialized with a reference to the host (implementing the IHost
interface).
interface IHost {
string[] ReadConfiguration(string fileName);
}
interface IPlugin {
void Initialize(IHost host);
}
class MyPlugin : IPlugin {
public void Initialize(IHost host) {
host.ReadConfiguration("myplygin.config");
}
}
IHost
can then supply any common functions that you need between all your plugins (as reading and writing configuration), and the plugins are also more aware of your host application, without being locked to a specific host application. You could for example write a Web Service application and one Forms application that uses the same plugin API.
Another way would be to initialize the plugin with the configuration information read by the host application, however, I had the need for the plugin to be able to read/write it's configuration, and perform other actions, on demand.
Upvotes: 7
Reputation: 6138
Microsoft promote developers to use this approach - Composite Application Guidance for WPF and Silverlight
Upvotes: 2
Reputation: 40863
Take a look at Composite WPF. Using something like this should allow you to have each plug in define the tab(s). The shell application would then only be responsible for loading each module to that modules respective view.
Upvotes: 4
Reputation: 5904
Mono.Addins is a very good library for implementing plugins of any kind (even ones which can update themselves from a website). MEF can also accomplish this, but it's a bit lower level. But MEF is going to be part of .NET 4.
Upvotes: 1
Reputation: 81537
i suggest taking a look at how the Castle Windsor DI/IoC framework handles the extra requirements you're after, and perhaps consider using it.
But in general, your plugins should support an interface, property or constructor by which you can inject a configuration object from a common source such as your app.config file.
Upvotes: 1