Plugin architecture with GUI

Viewed 9921

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?

12 Answers

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

.

The way I see it, this question is best solved as two separate problems.

  1. Enable each plugin to specify a configuration dialog (or something similar, perhaps in the form of tabs, as you suggested).
  2. Provide a centralized configuration file/database and an API for the plugins to store their configuration information.

Allowing each plugin to specify a configuration dialog

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();
}

Providing a central configuration database

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.

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.

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
}

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.

Related