Menu

Help-Patterns-Plugins

Using the Two-Layered GUI Toolkit for Plugins

As the name says, the Two-Layered GUI Toolkit offers two layers for describing GUI elements:

  • The description layer that only needs to know the Two-Layered GUI Toolkit.
  • The binding layer that needs to know the Two-Layered GUI Toolkit and an actual GUI toolkit.

By taking advantage of this, you can build plugin-enabled applications with plugins that contribute to the GUI without being linked to a particular GUI toolkit. The plugins just need to reference the Two-Layered GUI Toolkit, and your application can use whatever GUI toolkit (that has a respective binding) is convenient. You can even change the GUI toolkit later on without breaking any plugins!

Here's how:

The Plugin Interface

While there are various plugin definition, description and detection mechanisms in use, most of them have in common that there is (at least) one common interface for all plugins. Please refer to sources that know way better than this tutorial about how to properly implement plugin discovery and loading.

For our modest purposes, we will use the following tiny plugin interface with two methods:

using System;

using TwoLayeredGUI;

namespace Plugins
{
    public interface IPlugin
    {
        void DisplaySomething(IGUIProvider gui);

        bool AskSomething(IGUIProvider gui);
    }
}

As you see, both methods expect something called IGUIProvider. This is an interface that provides access to all the dialog boxes encapsulated by the Two-Layered GUI Toolkit. The concrete implementation that invokes a binding to a particular GUI toolkit will be supplied by the host application, so the plugins will only work with the IGUIProvider interface.

Two Exemplary Plugins

Our first plugin is very simple: It just shows an info message box and a question message box:

using System;

using TwoLayeredGUI;

namespace Plugins
{
    public class SomePlugin : IPlugin
    {
        public void DisplaySomething(IGUIProvider gui)
        {
            bool rememberChoice;
            gui.MessageBox.Show(new MessageBoxSettings("I am a plugin!"), out rememberChoice);
        }

        public bool AskSomething(IGUIProvider gui)
        {
            bool rememberChoice;
            return gui.MessageBox.Show(new MessageBoxSettings("Would you say 'yes' or 'no'?", ButtonDef.YesButton, ButtonDef.NoButton), out rememberChoice).Kind == DefaultButtonKind.Yes;
        }
    }
}

The MessageBox member of IGUIProvider is used to display the message boxes. Another fact that gets evident here is that the configuration of the message boxes is done with settings objects, in this case MessageBoxSettings. Check out the API reference to learn more about the plethora of available options.

In order to emphasize that plugins can really do different things, as long as they implement the common plugin interface, we will define a second plugin class. Our second plugin is a bit more complicated:

using System;
using System.IO;

using TwoLayeredGUI;
using TwoLayeredGUI.ValueInput;

namespace Plugins
{
    public class SomeOtherPlugin : IPlugin
    {
        public void DisplaySomething(IGUIProvider gui)
        {
            InputBoxSettings settings = new InputBoxSettings();
            settings.Title = "Save Sum";
            settings.TopDesc = "Enter two values. Their sum will be saved to a file.";
            settings.Values.Add(new Int32Value(new AccessString("First value:", 'F')));
            settings.Values.Add(new Int32Value(new AccessString("Second value:", 'S')));

            object[] values;
            if (gui.InputBox.Show(settings, out values)) {
                int sum = (int)values[0] + (int)values[1];
                using (var writer = File.CreateText("sum.txt")) {
                    writer.WriteLine(sum);
                }
            }
        }

        public bool AskSomething(IGUIProvider gui)
        {
            InputBoxSettings settings = new InputBoxSettings();
            settings.Title = "Your Decision";
            settings.Values.Add(new BooleanValue(new AccessString("Yes")));

            object[] values;
            if (gui.InputBox.Show(settings, out values)) {
                return (bool)values[0];
            } else {
                return false;
            }
        }
    }
}

In this plugin, the DisplaySomething method displays an input box, a dialog box that lets users input one or more values. Therefore, the InputBox object is retrieved from the GUI provider, and an InputBoxSettings instance is created. Note how labels with mnemonic characters are created by use of the AccessString class.

The AskSomething method uses another input box; this time only showing a boolean input control (a checkbox in most GUI toolkit bindings).

The Windows Forms Host Application

In order to run the plugins, some host application is required. The host application knows the IPlugin interface, but doesn't know the particular plugin classes. Instead, the plugin classes are loaded at runtime, for example by using reflection.

We will use Windows Forms for our host application. It has a menu item (of type System.Windows.Forms.ToolStripMenuItem) named miCommands that will serve as a menu header for items that invoke the DisplaySomething method of a plugin, and another menu item named miQuestions for items that invoke AskSomething. Assuming that the available plugins have already been loaded into some IEnumerable<IPlugin> loadedPlugins, the menus can then be populated as follows:

using TwoLayeredGUI.WinForms;
using TLG = TwoLayeredGUI.WinForms;

using Plugins;

// ...

var gui = new GUIProvider();

foreach (IPlugin plugin in loadedPlugins) {
    IPlugin currentPlugin = plugin;

    var miCommand = new ToolStripMenuItem(plugin.GetType().Name);
    miCommand.Click += delegate {
        currentPlugin.DisplaySomething(gui);
    };
    miCommands.DropDownItems.Add(miCommand);

    var miQuestion = new ToolStripMenuItem(plugin.GetType().Name);
    miQuestion.Click += delegate {
        if (currentPlugin.AskSomething(gui)) {
            TLG.MessageBox.Show("Question confirmed.");
        }
    };
    miQuestions.DropDownItems.Add(miQuestion);
}

This code shows how the host application supplies an implementation of the IGUIProvider interface that is bound to a particular GUI toolkit. In this case, it is a class named GUIProvider that is found in the TwoLayeredGUI.WinForms namespace. Unsurprisingly, it uses Windows Forms to display the dialog boxes.

One more thing to note is how the code in the host application directly invokes a message box from the Windows Forms binding of the Two-Layered GUI Toolkit in the line

TLG.MessageBox.Show("Question confirmed.");

Hence, in this program the Two-Layered GUI Toolkit it used both by plugins and the host application, and both via GUI providers and by direct calls to a GUI binding. The explicit (abbreviated) namespace prefix was inserted to differentiate the MessageBox class from the Two-Layered GUI Toolkit from the System.Windows.Forms.MessageBox class found in the BCL.

The Gtk# Host Application

At some point, someone may decide that the host application should be ported to a different GUI toolkit - in this case, Gtk#. Had the plugins directly used Windows Forms, this would break all of our plugins, but luckily, we have employed only the description layer of the Two-Layered GUI Toolkit in our plugin types.

The new Gtk# host application GUI is about the same as the Windows Forms one. The menus (of type Gtk.Menu) for invoking DisplaySomething and AskSomething are named mCommands and mQuestions, respectively.

using TwoLayeredGUI.Gtk;

using Plugins;

// ...

var gui = new GUIProvider();

foreach (IPlugin plugin in loadedPlugins) {
    IPlugin currentPlugin = plugin;

    var miCommand = new MenuItem(plugin.GetType().Name);
    miCommand.Activated += delegate {
        currentPlugin.DisplaySomething(gui);
    };
    mCommands.Add(miCommand);

    var miQuestion = new MenuItem(plugin.GetType().Name);
    miQuestion.Activated += delegate {
        if (currentPlugin.AskSomething(gui)) {
            MessageBox.Show("Question confirmed.");
        }
    };
    mQuestions.Add(miQuestion);
}

Now, most of the code that has changed is related to Windows Forms menu items versus Gtk# menu items. The only relevant change for the plugins is that now, an instance of TwoLayeredGUI.Gtk.GUIProvider is created, an IGUIProvider implementation that uses Gtk#. Likewise, the Gtk# MessageBox class from the Two-Layered GUI Toolkit is used.

The resulting application still works the same way; the plugins display the same dialog boxes, this time with Gtk#. To the plugins, the change of the GUI toolkit in the host application was completely invisible.

Restrictive Plugin Interfaces

In the code samples presented above, plugins have a high degree of freedom: They can display one or more arbitrary dialog boxes of their choice, or none at all. This is not always desired. Some applications may want plugins to contribute only pre-defined parts to a user interface rather than completely take over control. In such situations, rather than providing an IGUIProvider implementation, other objects from the description layer can be passed.

Here is an example plugin interface that does not use general GUI providers:

using System;

using TwoLayeredGUI;
using TwoLayeredGUI.ValueInput;

namespace Plugins
{
    public interface IPlugin
    {
        void ExecuteProcess(IMessageBoxProvider gui);

        MessageBoxSettings RunOperation();

        IEnumerable<InputValue> PrepareSettings();
    }
}

In this interface, ExecuteProcess can still be used in a pretty similar way to what we have seen above. However, the passed object is not the general GUI provider, but a GUI provider that is restricted to displaying message boxes. Ready-made implementations of that interface, such as TwoLayeredGUI.WinForms.MessageBoxProvider, are available.

The RunOperation method is different in that it does not expect to be passed a GUI provider at all. Instead, it just returns a settings object that needs to be processed by the host application:

MessageBoxSettings msgSettings = plugin.RunOperation();
if (msgSettings != null) {
    MessageBox.Show(msgSettings);
}

Lastly, the PrepareSettings method just returns some settings objects that are used in dialog box settings mostly configured by the host application. In this case, the method has a chance to add some additional input values to an input box:

InputBoxSettings settings = new InputBoxSettings();
// various adjustments to settings
settings.Values.AddRange(plugin.PrepareSettings());

object[] values;
if (InputBox.Show(settings, out values)) {
    // values are processed
}

The bottom line is: Plugins only need to know the description layer of the Two-Layered GUI Toolkit. The actual binding to a particular GUI toolkit can be hidden away in the host application.


Related

News: 2013/01/tutorial-on-plugins-with-the-two-layered-gui-toolkit
Wiki: Help-Tutorial-SettingsObjects-Intro
Wiki: Help-Tutorial-SettingsObjects
Wiki: Help

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.