noah
noah

Reputation: 211

Plugin architecture with GUI

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

Answers (12)

Kiran Thokal
Kiran Thokal

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

Fabrizio Stellato
Fabrizio Stellato

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

James King
James King

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

Dominic Zukiewicz
Dominic Zukiewicz

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

Kendall Frey
Kendall Frey

Reputation: 44374

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.

Upvotes: 3

Jorge C&#243;rdoba
Jorge C&#243;rdoba

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

Brannon
Brannon

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

Kristoffer L
Kristoffer L

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

Vasyl Boroviak
Vasyl Boroviak

Reputation: 6138

Microsoft promote developers to use this approach - Composite Application Guidance for WPF and Silverlight

Upvotes: 2

Mark Coleman
Mark Coleman

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

joemoe
joemoe

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

Paul Sasik
Paul Sasik

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

Related Questions