E = m c²
Extensible Message Checker

Writing Plugins

Plugin Types

There are 5 basic types of plugins, each serving a different purpose in the message cycle. To write custom plugins, you should simply inherit from the appropriate base class and optionally add some attributes to specify additional metadata.

The following table lists the different types of plugins and the base class they should inherit from (all in the JelleDruyts.Emc.Common namespace of the JelleDruyts.Emc.Common.dll assembly). They all inherit from the EmcPlugin base class, which already provides a number of services common to all plugins.

TypeBase ClassDescription
TriggersTriggerPluginTrigger a new message cycle.
SourcesSourcePluginCheck for new messages.
FiltersFilterPluginFilter out unwanted messages.
PublishersPublisherPluginPublish new messages to a destination.
UI PublishersThreadSafePublisherPluginPublish new messages on the UI thread automatically (for Windows Forms).
Hosted PluginsHostedPluginDoes not participate in the message cycle but has settings and commands.

Plugin Attributes

The following table lists the attributes that can optionally be applied to plugin types.

AttributeDescription
EmcPluginAttributeDefines a display name, author, description and url for a plugin.
EmcPluginIconAttributeDefines the icon to be displayed when representing a plugin.
EmcPluginSettingsTypeAttributeDefines the type of the settings object for a plugin.

Plugin Services

The plugin model is explicitly designed to be as simple as possible, so the following services are provided out-of-the-box, with little to no code required from the individual plugins:

  • Initialization & Shutdown: plugins are managed by the host, which will initialize and dispose the plugin instances.
  • Configuration: you only need to provide a type of a settings object; the runtime will allow the user to configure the settings, which are then automatically persisted to file, and provided to the plugin at initialization.
  • Encryption: certain settings can automatically be encrypted by simply providing a custom attribute. This helps protect sensitive data, such as passwords. Encryption can be done in different ways, configurable by the user (e.g. using a password, using the Windows built-in encryption, ...).
  • Text Templating: some plugins can benefit from a text templating engine that replaces certain parts of strings with dynamic data, e.g. $(Sender) can get replaced by the sender of an E = m c² message. See below for more information.

A Simple Plugin

A very simple plugin is shown below, in this case a trigger plugin that will cause a message cycle when it is activated.

using System;
using System.Collections.Generic;
using System.Text;

using JelleDruyts.Emc.Common;

namespace JelleDruyts.Emc.Plugins.Triggers.Startup
{
    [EmcPlugin(Name = "Startup", Description = "This plugin will trigger when the runtime is started up.", Author = "Jelle Druyts", Url = "http://jelle.druyts.net")]
    [EmcPluginIcon(typeof(StartupTrigger))]
    public class StartupTrigger : TriggerPlugin
    {
        public override void Activate()
        {
            // Signal a trigger once as the plugin is activated.
            OnTriggered(EventArgs.Empty);
        }
    }
}

Note that:

  • A reference to JelleDruyts.Emc.Common.dll is needed to be able to import the JelleDruyts.Emc.Common namespace and all its types.
  • The EmcPlugin attribute is used to specify the name, description, author and url. These are shown in the E = m c² configuration window.
  • The EmcPluginIcon attribute is used to specify an icon for the plugin. The given type is used to look up an embedded resource in the assembly defining the type. A resource with the same name as the type is searched for, but optionally you can specify the actual name of the resource if it doesn't match the type name.
  • The EmcPluginSettingsType attribute isn't used since this plugin needs no settings.
  • The plugin class inherits from the TriggerPlugin base class.
  • The plugin class overrides the Activate method, which is called when the runtime is started and all plugins are initialized.
  • The plugin class calls the OnTriggered method, which raises an event to the runtime to start a new message cycle.
  • This is the actual source code for the built-in StartupTrigger plugin. It really is that simple.

Each type of plugin has different members that can be overridden, so check the documentation of the specific base class for all the details.

Plugin Basics

The EmcPlugin Class

All plugin types ultimately inherit from the EmcPlugin base class, which already provides a number of services. The following table lists the most important members of the EmcPlugin class and their purpose.

MemberDescription
public string Name { get; }Allows the plugin to retrieve its display name, as chosen by the user.
public string Settings { get; }Allows the plugin to retrieve its settings, as configured by the user.
public virtual bool Initialize()Allows the plugin to initialize itself.
public virtual PluginCommand[] GetCommands()Allows the plugin to define commands that can be executed through the host (see below).
public virtual string GetStatus()Allows the plugin to report its status to the host.
public virtual void Dispose(bool disposing)Allows the plugin to clean up when it is being shut down.
protected void Trace(string message)Allows the plugin to trace messages through the host, optionally with a specific log level.
InvokeRequired, Invoke, BeginInvoke, EndInvokeAllows the plugin to synchronize with the UI thread in a Windows Forms host.

The EmcMessage Class

Most plugins will also handle E = m c² messages, which are the basis for all communication between plugins. The following table lists the most important members of the EmcMessage class and their purpose.

MemberDescription
public virtual string Source { get; }Gets the source of this message, e.g. the name of the plugin that created the message.
public virtual string Sender { get; }Gets the sender of this message.
public virtual string[] Receivers { get; }Gets the receiver(s) of this message.
public virtual string ReceiverList { get; }Gets the receiver(s) of this message concatenated with a semicolon separator.
public virtual string Subject { get; }Gets the subject of this message.
public virtual string Content { get; }Gets the contents of this message.
public virtual DateTime Received { get; }Gets the date and time this message was received.
public string GetContentAsHtml()Gets the message content as an HTML fragment. If the message is already HTML formatted, its <body> is extracted and returned. If the message is plain text, it is HTML encoded and new lines are replaced with line breaks.
public string GetContentAsText()Gets the message content as a text fragment.

Note that the class and most of its members are virtual, so it is possible to subclass it in order to propagate more information between plugins.

Plugin Settings

Specifying And Using Settings

Most plugins will typically require settings that need to be configured by the user and persisted by the runtime. This is possible by providing a single type that contains all settings for that plugin, and specifying this type in the EmcPluginSettingsType attribute.

A simple plugin that uses settings is shown below, in this case a publisher plugin that will show the number of new messages in a message box.

[EmcPlugin(Name = "MessageBox", Description = "This plugin will show a simple message box.")]
[EmcPluginSettingsType(typeof(PluginSettings))]
[EmcPluginIcon(typeof(MessageBoxPublisher))]
public class MessageBoxPublisher : ThreadSafePublisherPlugin
{
    public override void PublishCore(EmcMessage[] messages)
    {
        PluginSettings settings = (PluginSettings)this.Settings;
        MessageBox.Show(string.Format("There are {0} new messages.", messages.Length), settings.Title);
    }
}

Note that:

  • The EmcPluginSettingsType attribute is used to indicate the type of the settings object, i.e. the PluginSettings class.
  • The plugin class inherits from ThreadSafePublisherPlugin, instead of the default PublisherPlugin base class, since it is a Windows Forms plugin that will show a message box. This must be done on the UI thread, so inheriting from the base class and overriding the PublishCore method ensures that it is always called on the correct thread.
  • The Settings property of the EmcPlugin base class is of type System.Object, so it must be cast to the specific plugin settings type, in this case the PluginSettings class.

Defining Settings

There is one important rule for settings objects, and that is that they must be serializable for the runtime to be able to persist them. This means they should at least declare the [Serializable] attribute, or implement the System.Runtime.Serialization.ISerializable interface.

In its most basic form, the PluginSettings class could look as follows:

[Serializable]
public class PluginSettings
{
    private string title = "New messages...";

    public string Title
    {
        get { return this.title; }
        set { this.title = value; }
    }
}

However, to make it easier for the user to configure the settings, settings objects can support all design-time enhancements that are available in the .NET Framework, such as default values, descriptions, categories, type editors, type converters, ... A more user-friendly settings object would look as follows:

[Serializable]
public class PluginSettings
{
    private string title = "New messages...";

    [Description("The title of the message box.")]
    [DefaultValue("New messages...")]
    [Category("Options")]
    public string Title
    {
        get { return this.title; }
        set { this.title = value; }
    }
}

With these extra attributes, the Title setting will be placed in the "Options" category, the user will be shown a description of the setting, and it becomes possible to reset the setting to its default value.

Commonly Used Attributes

The following table lists a number of commonly used attributes in settings classes with their purpose.

AttributeNamespaceDescriptionExample
DescriptionSystem.ComponentModelProvides a description of the setting to the user.[Description("The title of the message box.")]
DefaultValueSystem.ComponentModelProvides a default value for the setting, so the user can easily reset the value.[DefaultValue("New messages...")]
CategorySystem.ComponentModelProvides a category for the setting, so it is shown in category groups.[Category("Options")]
BrowsableSystem.ComponentModelHides the setting from the user, which can be useful to persist additional data for the plugin to the settings file (e.g. runtime state).[Browsable(false)]
MultilineStringEditorSystem.ComponentModel.DesignProvides an editor for strings that can span multiple lines.[Editor(typeof(MultilineStringEditor), typeof(UITypeEditor))]
FolderNameEditorSystem.Windows.Forms.DesignProvides an editor so the user can browse for a folder.[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
FileNameEditorSystem.Windows.Forms.DesignProvides an editor so the user can browse for a file.[Editor(typeof(FileNameEditor), typeof(UITypeEditor))]
FlagEditorJelleDruyts.Tools.DesignProvides an editor so the user can choose combinations of combinable flag enumerations.[Editor(typeof(FlagEditor), typeof(UITypeEditor))]

Advanced Topics

Encrypting Settings

Certain settings contain sensitive information and should be encrypted. The host application contains options for the user to specify encryption mechanisms, and all the settings class itself should do is indicate to the host which fields should be encrypted. This can be done by applying the SerializeEncrypted attribute (from the JelleDruyts.Tools.Security namespace) to the field (not the property) storing the sensitive information.

To allow the user to securely enter the sensitive data (so that it doesn't get displayed on the screen), the MaskedDialogEditor and MaskedTypeConverter attributes (both from the JelleDruyts.Tools.Design namespace) can be applied. An example for a Password setting can look as follows:

[SerializeEncrypted]
private string password = string.Empty;

[Description("The password.")]
[DefaultValue("")]
[Editor(typeof(MaskedDialogEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(MaskedTypeConverter))]
[Category("General")]
public string Password
{
    get { return this.password; }
    set { this.password = value; }
}

Note that:

  • A reference to the JelleDruyts.Tools.dll assembly is required, as well as using statements for the JelleDruyts.Tools.Design and JelleDruyts.Tools.Security namespaces.
  • The private password field is decorated with the SerializeEncrypted attribute to make sure it is encrypted when persisted.
  • The MaskedDialogEditor is used to allow the user to enter the password in a separate dialog box with password boxes.
  • The MaskedTypeConverter is used to hide the sensitive data from the configuration screen and show it as a password.

Text Templating

Some plugins can benefit from a text templating engine that replaces certain parts of strings with dynamic data, e.g. $(Sender) can get replaced by the sender of an E = m c² message. The JelleDruyts.Tools library provides a basic text templating engine with design-time support that can be used easily from E = m c² plugins.

If we extend our message box plugin from above to support templates, it becomes possible for the user to define a template format that will be processed to include values from messages. For example, the template "[$(Sender)] $(Subject)" could be expanded into "[Jelle Druyts] Here's a new message for you!". The plugin implementation could then become as follows:

public override void PublishCore(EmcMessage[] messages)
{
    PluginSettings settings = (PluginSettings)this.Settings;
    TemplateProcessor messageProcessor = new TemplateProcessor(settings.Format);
    StringBuilder summary = new StringBuilder();
    foreach (EmcMessage message in messages)
    {
        IDictionary<string, object> messageTokenValues = EmcMessageTemplateHelper.GetTokenValues(messages.Length, message);
        string processedText = messageProcessor.Process(messageTokenValues);
        summary.AppendLine(processedText);
    }
    MessageBox.Show(summary.ToString(), settings.Title);
}

Note that:

  • A reference to the JelleDruyts.Tools.dll assembly is required, as well as a using statement for the JelleDruyts.Tools.Text namespace.
  • An additional property named Format is added to the settings object. This provides the template format of the messages.
  • A template processor is created for that template. This will allow individual tokens, e.g. $(Sender), to be replaced.
  • Each message is processed in a loop using the template, by calling the EmcMessageTemplateHelper to retrieve the token values for the current list of token values and then passing these on to the Process method.
  • There is some overhead to using this template processor, but the advantage is that the user has full support for these templates in the configuration screen.
  • It is better to create the TemplateProcessor instance when the plugin is initialized and keep the instance in a private field. This removes the overhead of parsing the template each time new messages come in.
  • This is very close to the actual source code for the built-in PopupPublisher plugin.

The new Format property of the PluginSettings class would then look as follows:

private string format = "[$(Sender)] $(Subject)";

[Description("The format of each message that is displayed.")]
[DefaultValue("[$(Sender)] $(Subject)")]
[Editor(typeof(TemplateEditor), typeof(UITypeEditor))]
[TemplateInfoProvider(typeof(EmcMessageTemplateHelper))]
[Category("Options")]
public string Format
{
    get { return this.format; }
    set { this.format = value; }
}

Note that:

  • A reference to the JelleDruyts.Tools.dll assembly is required, as well as a using statement for the JelleDruyts.Tools.Design namespace.
  • The property defines a TemplateEditor as a UI Type Editor. This provides design-time support for the user in the configuration screen.
  • The property defines a TemplateInfoProvider attribute, which points to the EmcMessageTemplateHelper (in the JelleDruyts.Emc.Common namespace). This is needed for the TemplateEditor to work.

Plugin Commands

Plugins can provide commands to the runtime, which are pieces of functionality that are exposed by the plugin. They are made available to the user by the host, e.g. in the Windows application through the menu in the system's notification area. To provide commands to the runtime, override the GetCommands method to return a number of PluginCommand instances.

The following table lists the most important members of the PluginCommand class and their purpose.

MemberDescription
public string Text { get; }Gets the text to display for the command.
public ExecuteCommandCallback ExecuteCommandCallback { get; }Gets the callback method to be called when the command is executed. This delegate is called when the user executes the command.
public QueryCommandStateCallback QueryCommandStateCallback { get; }Gets the callback method to be called when querying the command state. This delegate is called when the host shows the command to the user and needs to know its state (Enabled, Disabled or Unavailable).

A simple example of a plugin that supports a "Say Hello" command (that is always enabled and shows a message box when executed) looks as follows:

public override PluginCommand[] GetCommands()
{
    PluginCommand command = new PluginCommand("Say Hello", new ExecuteCommandCallback(SayHello), new QueryCommandStateCallback(CanSayHello));
    return new PluginCommand[] { command };
}

private void SayHello()
{
    MessageBox.Show("Hello");
}

private CommandState CanSayHello()
{
    return CommandState.Enabled;
}