WPF Visual Plugins

WPF is a modern GUI rendering system that utilizes DirectX and provides the ability to create rich user interfaces with 2D/3D rendering, vector graphics, runtime animation, and much more.

This article will describe how to create a new WPF visual plugin and talk about what features it provides and how they can be used.

Getting Started

Before we start to create our first plugin, let's look at a whole Hydra plugin project.

A Hydra plugin project consists of three parts:

  • Plugin Module - A DLL project that acts as a container for the module controller and can hold a set of plugins.
  • Module Controller - The entry point of the module. The module controller supplies the host with information about stored plugins and provides methods that allow the host to instantiate and work with plugins.
  • Plugins - Special classes stored inside the plugin module.

Now we can start to create and set up a Hydra plugin project and add a visual WPF plugin to it.

Working with Wizards

First we need to create a new module, to do this select File -> New -> Project in the Visual Studio menu, choose the language you need (we provide templates for C#, VB.NET and Oxygene for .NET), then, in the RemObjects Hydra category, choose Plugin Module (WPF):

In this dialog you also need to enter a project name and destination folder.

After you press OK, the wizard will create a new plugin module and add a module controller to the project. I will describe how to set up a module controller later in this article.

Now that we have a plugin module, we can add a plugin to it: Select Project -> Add New Item from the VS menu and then select the Visual Plugin (WPF) item:

Select a name for the new plugin and press Add. And we are done, the wizard will add a new visual plugin to the project.

The plugin module can contain as many plugins as you want, and you can also have visual and non-visual plugins in the same project.

Review the resulting project

Now let's review the resulting project. It consists of three parts:

Plugin Module

The plugin module is a Class Library project that will produce an assembly file that can be used by any of the supported host platforms.

The wizard automatically adds a reference to the RemObject.Hydra.dll assembly that contains the Hydra core classes. If you intend to use this plugin module with a Delphi (VCL or FireMonkey) host, you will need this file to be present in the same folder as the plugin, so you may want to tune your project so it will automatically copy the core dll into the destination folder. To do so select Properties of the RemObjects.Hydra.dll:

On the Properties page, set CopyLocal to true:

In the AssemblyInfo.cs, set the following assembly attribute:

[assembly: ComVisible(true)]

Please do not change this attribute, as it allows the host application to access the interfaces exposed by the plugin.

Module Controller

The module controller is a class that acts as an entry point for the plugin module. The host will instantiate this class on module load to get information about plugins.

The wizard generates the following code for the module controller:

  [ModuleController]
  public partial class TheModuleController : ModuleController
  {
    public TheModuleController()
    {
      InitializeComponent();
    }
  }

The module controller is inherited from the ModuleController class that itself is inherited from System.ComponentModule.Component. By default this controller holds two image lists that can be shared with the host application.

The ModuleController also allows you to set additional meta-data, for example:

  [ModuleController(Name = "Controller", Description = "My module controller", UserData = "Data")]
  public partial class TheModuleController : ModuleController
  [..]

Visual Plugin

The visual plugin is the control that you will use to embed into the host application. It is inherited from the RemObjects.Hydra.WPF.VisualPlugin class that allows it to be used in a cross platform environment. This control is inherited from the @@@, so it provides the same design surface as a regular UserControl does.

The code that was generated by the wizard looks like this:

  [Plugin, VisualPlugin]
  public partial class Plugin1 : VisualPlugin
  {
    public Plugin1()
    {
      InitializeComponent();
    }
  }

Please do not remove the two attributes, as they are used by the host application to indicate that the class is indeed a plugin, and they are also used to read plugin meta-data.

As in the module controller, you can set the meta-data manually:

  [Plugin(Name = "SamplePlugin", Description = "This is sample plugin", UserData = "Data"), VisualPlugin]
  public partial class Plugin1 : VisualPlugin
  [..]

Using the plugin

By now you will have a complete project that, without any additional work, can be loaded by all supported host platforms.

Both module controller and visual plugin provide a set of internal methods that are used by the Hydra framework to perform specific tasks, however, they also provide some useful methods/properties which can be used in your own application.

Below we will describe the most common way to use the module controller and plugin, but first, let's talk about custom interfaces. One of the major parts of the Hydra framework are custom interfaces. Custom interfaces are user-defined interfaces that can be used in the cross-platform environment (they can, for example, be shared between a FireMonkey host and a .NET plugin) to receive access to data or to call host or plugin methods. When we talk about things like "accessing host methods" we refer to the custom interfaces, but since this is a large topic, we can't cover it in this article, so if you feel like you need to use them, please take a look at these two articles:

Before we start to describe the visual plugin itself, let's look at how you can use the module controller.

How to use Module Controller

While the main purpose of the module controller is to provide information about plugins to the host, it can also be useful with some specific tasks.

The host will instantiate the module controller only once on module load, so it can be used to initialize some global data or allow the host to gain access to global methods via custom interfaces.

By default the module controller holds two ImageList controls, assigned to the SmallImages and LargeImages properties. These image lists will be passed to the host application, so you can use them to store shared images.

ModuleController also has an event called HostChanged. This is an important event if you need to be able to access host methods or data. Since the controller is initialized on module load, it doesn't have access to the Host property at the time when its constructor is executed. When the host assigns its reference to the module controller, the HostChanged event is fired and you can safely get access to the host. One thing that you need to consider is that this event can be called with a null host reference (when the host unloads a module) so you need to check this before accessing host members.

The last thing that is available to the module controller is the Host property; this property holds the reference to the host object, and can be used to access host methods or data.

How to use the Visual Plugin

WPF visual plugins are derived from the @@@ and provide exactly the same design surface. So you can use them in the same way as your regular user controls. The biggest difference is, of course, that you can load and embed these plugins into any supported host application.

Like module controller, the VisualPlugin provides access to the Host property and HostChanged events, so everything that was said above for the module controller also applies to the visual plugin.

One important topic when working with WPF plugins is tab key navigation; unlike other supported plugins it requires some additional work. Unfortunately, WPF doesn't provides access to their internal methods to deal with tab order. Currently, it only allows to work with TabIndex to determine the proper tab order. But most controls that support TabIndex will set it to the default value that doesn't allow us to properly detect the tab order of a control. So in order to add proper tab support you will need to manually @@@adjust controls TabIndex property@@@.

The other important thing is how WPF plugins work with custom interfaces. By default WPF blocks COM interop in their @@@ and this limits you to only having access to host methods and properties, but you won't be able to expose plugin functionality to a host. So in order to deal with this problem, we have a special wrapper class, but this will require some work, for example, we need our plugin to implement the following interface:

[Guid("8032a51c-5961-41f5-9582-c77d98ea4d93")]
public interface ICustomPlugin: IHYCrossPlatformInterface
{
  string UserData { get; set; }
}

To do so, first we need to implement this interface in a plugin:

[Plugin, VisualPlugin, NeedsManagedWrapper(typeof(CustomPluginWrapper))]
public partial class Plugin : VisualPlugin, ICustomPlugin
{
 [..]
    public string UserData
    {
      get { return MyCustomData; }
      set { MyCustomData = value; }
    }
}

Please note that we not only implemented the interface member, but also added a new attribute called NeedsManagedWrapper that points to a special wrapper class. The host will access this wrapper instead of directly accessing plugin members, so let's take a look at how this wrapper if defined:

  public class CustomPluginWrapper: VisualPluginWrapper, ICustomPlugin
  {
    private new Plugin PluginInstance { get { return base.PluginInstance as Plugin; } }

    public string UserData
    {
      get { return PluginInstance.UserData; }
      set { PluginInstance.UserData= value; }
    }
  }

As you can see, the only thing it does is access the actual implementation of the property in the plugin, and this allows us to overcome WPF limitations.