Tuesday, 24 June 2008

Dynamic assembly loading (.NET framework 2.0)


In the .NET framework 3.5, you have a lot of functionality for writing your custom plug-ins of add-ins for your own application. In the .NET framework 2.0 you have not. I was searching a time on the Internet to find out how to do this. Finally I got something, but the description was not completely clear to me, so I figured out how to deal with this. In this post, I wrote in detail how to get your (host) application plug-in enabled. The sample enclosed is written on Visual Studio 2008

The idea is that you have your host application, you plug-in dll and in between an interface. I started with a simple host application, called ThePluginDemo.exe.This application has one form with a menu and a status bar on it. The plugin we will create, add a new menu option called "My Plugins" with a menu item under it called "Plugin Demo". Ok, let's start.

1. Pre-configuration of host application

On the "My Project"details, the root namespace is edited to PluginDemo.Presentation and the Output Path is changed to ..\Output\.

Note: All the projects in this solution will have the same output path, so you don't have to copy/paste the new dll's and stuff when you did a rebuild action.

2. Creating the interface

1. Add a new project to the solution:

An Empty Project was selected, the .NET Framework 2.0 option was selected in the drop down box on the top right corner of the window, and the project was called PluginInterfaces

2. On the "My Project" details , the Root namespace is changed to PluginDemo.Interfaces, the application type is changed to Class Library and also the Output Path is changed to ..\Output\.

3. A new VB-class file was added and the default code was replaced to:

Imports System
Imports System.Windows.Forms

Public Interface IPlugin

  Sub AddMenuItems(ByVal target As MenuStrip)

End Interface

The sub AddMenuItems is the procedure that will be called later on to add the new menu item of the plugin.

Don't forget to add a reference to system.windows.forms.

4. On the project ThePluginDemo (= host application), add a reference to PluginInterfaces.

3. Create the Plug-in project

1. A new, empty project (on .NET framework 2.0) was added to the solution with the name MyPlugin, Root namespace PluginDemo.Plugins, Output Path ..\Output\.

2. References to PluginInterfaces and system.windows.forms were added.

3. A new VB-class file was added, called MyFirstPlugin. The code of the file was edited so it looks like:

Imports PluginDemo.PluginInterfaces
Imports System.Windows.Forms

Public Class MyFirstPlugin

   Implements IPlugin

   Public Sub AddMenuItems( _
       ByVal target As MenuStrip) _
       Implements _

   End Sub

End Class

A procedure was added which will be called when pressing on the menu item created by this plugin:

Public Sub miPluginDemo_Clicked( _
   ByVal sender As Object, _
   ByVal e As EventArgs)

   MessageBox.Show("Plugin Demo menu item clicked!")

End Sub

Note: the parameters sender and e are needed, otherwise the method does not have a signature compatible with delegate 'Delegate Sub EventHandler(...)'.

4. The following code was added to this class and is responsible for adding new menu options:

Private mainMenu As MenuStrip

Public Sub AddMenuItems( _
   ByVal target As MenuStrip) _
   Implements PluginInterfaces.IPlugin.AddMenuItems

   ' Store target menu strip in a local variable
   mainMenu = target
   ' Check if the report plugin menu item already exists
   If Not mainMenu.Items.ContainsKey("miMyPluginDemo") Then
       ' Create the report plugin menu item
       Dim reportMenu As New ToolStripMenuItem("My Plugins")
       reportMenu.Name = "miMyPluginDemo"
   End If
   ' Create a ToolStripMenuItem as submenu item with an event
   Dim mi As New ToolStripMenuItem
   mi.Name = "miPluginDemo"
   mi.Text = "Plugin Demo"

   ' Attach a handler that fires a procedure
   AddHandler (mi.Click), AddressOf miPluginDemo_Clicked
   ' Add the toolstripmenuitem to the mainmenu/report
End Sub

' Procedure to add a ToolStripMenuItem to the mainmenu/report
' location.
Private Sub AddMenuItemToReportMenu( _
   ByVal mi As ToolStripMenuItem)

   Dim tsmi As ToolStripMenuItem
   tsmi = CType(mainMenu.Items.Item("miMyPluginDemo") _
       , ToolStripMenuItem)
End Sub

4. Create a plug-in XML file

To load the plugin dynamically, a XML file will be created that lists the available plugins. In this example, just one plugin dll.

1. A new XML file was added to the MyPlugin project and is called PluginList.xml

2. The content was edited, so it looks like:


ID: a sequence number and should be unique when you have more than one plugin you want to use.

Type: the namespace of the MyPlugin project followed by the class name that contains the plugin functionality you want to use.

File: the name of the file created by building the MyPlugin project (without extension).

3. The file properties of this XML file were edited to:

Build action = None; Copy to Output Directory = Copy Always

5. Load plugin in host application

Finally, we edit the host application so it will load the plugin dynamically.

1. Imports to the PluginInterface and to system.reflection were added:

Imports PluginDemo.PluginInterfaces
Imports System.Reflection

2. A new dataset object was added with filename Plugins.xsd.

A DataTable was added and named PluginTool (see also the tag in the xml file)

Three columns were added:

  * Name: ID; DataType: System.Int32; Unique: True

  * Name: Type; DataType: System.String

  * Name: File; DataType: System.String

3. On the mainform, two private variable were declared to hold the content of the XML document as created in step 4.

Private pluginXml As Plugins
Private pluginList As ArrayList

4. Finally, a procedure was added to load the plugin(s). The call to this procedure was placed in the form onload event.

Private Sub LoadPlugins()
   ' Create a new instance of the plugins
   pluginXml = New Plugins

       ' Try to load the XML file with the
       ' available(plugin(s)) information

       ' Create a new instance of an ArrayList
       Me.pluginList = New ArrayList

       ' Step through all available PluginTools,
       ' read from the XML file and add them to
       ' the(ArrayList)
       Dim r As Plugins.PluginToolRow

       For Each r In pluginXml.PluginTool
           Dim a As Assembly = Assembly.Load(r.File)
           Dim t As Type = a.GetType(r.Type)
           Dim p As IPlugin = _
               CType(Activator.CreateInstance(t) _
               , IPlugin)

       For Each o As Object In Me.pluginList
           Dim p As IPlugin = DirectCast(o, IPlugin)

       ' Show statusbar message when plugin(s)
       ' is (are) (succesfully) loaded
       tsStatus1.Text = "Plugin(s) loaded."

   Catch ex As Exception
       ' Show statusbar message when loading
       ' plugin(s) failed
       tsStatus1.Text = "Plugin(s) not loaded."
   End Try

End Sub

5. This is it! We are done. Running the application shows the plugin loaded.

The complete solution can you download here: ThePluginDemo.zip.

Good luck!

No comments: