Lesson blog: Creating a plugin accepting application

Last week we had a lesson describing a simple sample of how you could create an application that accepts plugins, using an interface approach. This is my take on it;

First of all you need to decide quite exactly what the plugin developer should be possible to do. For this example I have created a simple Win Forms application that shows a list of contacts and allows the user to edit the contacts.

The Main Window of the application:

image

When the user double clicks a contact a simple editing form is opened:

image

When the user clicks Save this form is closed and any changes are reflected in the main window. But this form is quite dull and simple. Wouldn't it be cool if you could let some other developer attach a plugin to the application that will change the layout and possibly the behavior of this form? That's what we're going to do.

I wont go into the details of how the above application is built, if you want to follow along download this starting point here.

Some considerations has already been made when it comes to the plugin part, so lets have a look at the solution as it looks now;

image

PluginHost is the Windows Application and PluginLib is a Class Library that is referenced by PluginHost. PluginLib contains the Contact business class that has one string property for each peace of data on each contact (i.e. Firstname, Lastname etc). This is a separate project because the resulting dll is what will be shared both by the Host application and any Plugin "applications".

The host application is made up of two Form classes, MainWindow and DefaultContactEditor. Since the editor window might be replaced by a plugin some efforts has been made to keep as much logic as possible in the MainWindow class. Let's have a look att some of the code in the DefaultContactEditor class;

public Contact ContactToEdit { get; set; }
public event EventHandler ContactSaved;

public void LoadContact(Contact c)
{
    tbFirstname.Text = c.FirstName;
    tbLastname.Text = c.LastName;
    tbEmail.Text = c.Email;
    tbMSN.Text = c.MSN;
    tbHomepage.Text = c.Homepage;
    ContactToEdit = c;
}

private void SaveEditedContact()
{
    ContactToEdit.FirstName = tbFirstname.Text;
    ContactToEdit.LastName = tbLastname.Text;
    ContactToEdit.Email = tbEmail.Text;
    ContactToEdit.MSN = tbMSN.Text;
    ContactToEdit.Homepage = tbHomepage.Text;
    if (ContactSaved != null)
    {
        ContactSaved(this, EventArgs.Empty);
    }
}

The LoadContact method is called from the MainWindow when the edit window is opened, and it fills up the windows textboxes with values from the given Contact object and assigns this object to the class member ContactToEdit. It is placed in a separate method instead of in the constructor, because later on an interface will require this method (ideally the interface should require a constructor receiving a contact object but as far as I know that is not possible).

The SaveEditedContact is called when the user clicks the save button in the edit window. This method ends by triggering the ContactSaved event that the MainWindow is listening to.

Now, let's have a look at some of the code in the MainWindow class;

private void lvContacts_MouseDoubleClick(object sender, MouseEventArgs e)
{
    if (lvContacts.SelectedItems.Count > 0)
    {
        Contact selectedContact = (Contact)lvContacts.SelectedItems[0].Tag;
        DefaultContactEditor editor = new DefaultContactEditor();
        editor.LoadContact(selectedContact);
        editor.ContactSaved += new EventHandler(editor_ContactSaved);
        editor.Show();
    }
}

private void editor_ContactSaved(object sender, EventArgs e)
{
    ((Form)sender).Close();
    DisplayContacts();
}

The first method here is an event handler for the double click event for the ListView control that lists the contacts in the main window. After assuring that one item is selected I get a reference to the selected Contact object and creates a new instance of the edit window. Here you can see the call to LoadContact. Also, you can see that the ContactSaved event is attached to the editor_ContactSaved method. This method will ensure that the edit window is closed when the contact is saved and it calls a method - DisplayContacts - that will redraw the ListView control reflecting any changes that might has been made.

Now to the plugin fixing. First, let's create an interface from some of the members in the DefaultContactEditor class. I call this interface IContactEditor and put it in the PluginLib project;

public interface IContactEditor
{
    Contact ContactToEdit { get; set; }
    event EventHandler ContactSaved;

    void LoadContact(Contact c);
}

After that I make sure that the DefaultContactEditor implements this interface, by adding it to the inheritance list;

public partial class DefaultContactEditor : Form, IContactEditor

Nothing more is needed here since the interface members already are implemented in DefaultContactEditor.

The idea now is to change the code that opens the editor window so that it uses a plugin if available and if not it will use the DefaultContactEditor. Let's start with creating a helper class in the PluginHost application;

public static class PluginUtility
{
    private const string PLUGIN_PATH = @"Plugins\";

    public static IContactEditor GetEditorWindow()
    {
        IContactEditor result = null;

        try
        {
            foreach (string file in Directory.GetFileSystemEntries(PLUGIN_PATH, "*.dll"))
            {
                Assembly dllAssembly = Assembly.LoadFrom(file);

                foreach (Type t in dllAssembly.GetTypes())
                {
                    //Find class that inherits from Form and implements IContactEditor
                    if (t.IsPublic && !t.IsAbstract &&
                        t.IsSubclassOf(typeof(Form)) &&
                        t.GetInterface("IContactEditor") != null)
                    {
                        result = (IContactEditor)dllAssembly.CreateInstance(t.FullName);
                        break; //Exit foreach when plugin is found
                    }
                }

                if (result != null)
                    break; //Exit foreach when plugin is found
            }
        }
        catch { }

        //If result still is null, return default contact editor
        if (result == null)
        {
            result = new DefaultContactEditor();
        }

        return result;
    }
}

image First we declare a constant containing the relative path to the folder where the plugin has to be placed. This folder has to be created in the output directory of the application, i.e. bin/Debug.

To keep the example simple this application will only accept one plugin. That one plugin, if existing, will be found in the GetEditorWindow method that returns an IContactEditor. In the first foreach we loop all dll-files within the Plugins directory. Foreach dll we loop the types that exists in the dll and using an if statement we look for classes that are public, not abstract, that inherits from Form and implements IContactEditor. If such a type is found we call the CreateInstance method of the Assembly object, which will create an instance of the found type. This instance is cast to an IContactEditor and assigned to the result variable. After that we make sure we exit both foreach loops. If no plugin is found, the last if will be true and the result variable will be assigned an instance of the DefaultContactEditor class.

Now, the double click event of the Main Windows ListView could be updated as follows;

private void lvContacts_MouseDoubleClick(object sender, MouseEventArgs e)
{
    if (lvContacts.SelectedItems.Count > 0)
    {
        Contact selectedContact = (Contact)lvContacts.SelectedItems[0].Tag;
        //OLD: DefaultContactEditor editor = new DefaultContactEditor();
        IContactEditor editor = PluginUtility.GetEditorWindow();
        editor.LoadContact(selectedContact);
        editor.ContactSaved += new EventHandler(editor_ContactSaved);
        //OLD: editor.Show();
        ((Form)editor).Show();
    }
}

Now lets build a plugin by adding another class library to the solution, I call it EditorPlugin. To speed things up I add a copy of the DefaultContactEditor class to this class library (make sure you change the namespace and the class name to EditorPlugin in the code view and in the designer.cs file!). Since this class inherits from Form I also need to reference System.Drawing and System.Windows.Forms which isn't included from the beginning in a Class Library (the compiler will inform you about this). We also need access to the Contact class and the IContactEditor interface; a reference to the PluginLib will solve this. After that I changed the design of the form as follows;

image

All that is altered here is a minor layout change, putting together the first and lastname textboxes on one single row. Now, lets build this class library and put the resulting EditorPlugin.dll in the Plugins directory of the PluginHost application.

Now when you run the application this restyled window will be used for editing. Download the complete solution here.

Try for yourself;

  • Add a checkbox by the MSN field with the caption "Same as email", setting the MSN textbox to the same value of the email textbox when the checkbox is checked.
  • What would you have to inform plugin developers about so that they will be able to develop a plugin for this application?
  • Add a dialog in the application that let's the user add new contacts to the list. Make it possible for plugin developers to replace this dialog.

1 comment :

  1. Thanks for sharing use interview questions on .Net technology. While preparing for my job interview, your article helped me a lot to sharpen my skills and do well in my interview. One of trainer from leading dot net training in Chennai suggests me about your site.

    ReplyDelete