Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB10

Develop a Plugin Extension for Your VisualBasic Application

4.50/5 (14 votes)
29 Dec 2013CPOL4 min read 31.3K   1.4K  
A simple and easy way to develop an application plugin

Introduction: Why We Want a Plugin?

  1. Makes your application extendable

    Sometimes, you just want to let other people to be able to extend your program to meet the Practice application requirements without modifying the source code and compile again, so that a program plugin is a convenient way.

  2. Makes your application coding more cool

    As you can see, almost every opensource program or widely spread programs support plugin extension. If you want your program to be more useful and friendly, then you should allow your user to develop a plugin to meet their practice requirements.

Background: Dynamic Load Assembly Module Using Reflection

This article is about how to develop a plugin for my VisualBasic program in a very simple way of my own. And the basically technology is the reflection operation in .NET.

Some very useful functions in the reflection operation are:

  1. Load an application assembly from a file (DLL or EXE file which is written in .NET):
    VB.NET
    Reflection.Assembly.LoadFile(AssemblyPath) 
  2. GetType: A useful keyword in VisualBasic to read the meta data of a type:
    VB.NET
    Dim Type As System.Type = GetType(TypeName)
  3. GetMethods, GetMembers, GetProperties, etc.

    Those functions which start with Get can let your code know some information about what is contained in the target Class Object.

  4. GetCustomAttributes

    A very useful function in the reflection to get some proceeding target for your code with the custom attribute as a flag to point out which member is our target.

  5. LINQ: A useful query statement in the VisualBasic

    LINQ statement is a SQL like statement in VisualBasic, and The LINQ To Object is the most used operation in our program.

    VB.NET
    Dim LQuery = From <Element> In <Collection> Where <Boolean Statement> Select <Element>

Using the Code

1. Load the Assembly Module File

Just one easy step to load an assembly module from a specific file using reflection:

VB.NET
Dim Assembly As Reflection.Assembly = Reflection.Assembly.LoadFile(AssemblyPath)

But please make sure that this function requires an absolute path string value.
An EXE module is the same object as the DLL extension module in the VisualBasic. The difference between the DLL and the EXE module is that there always exists a Main entry in the EXE module for executing the assembly. So you can use this function trying to load any .NET assembly module.

2. Get Module Entry

In this step, we will trying to find out a module that contains the plugin commands and I call this module object as PluginEntry. In my opinion, a module entry is the same as the entry point of an EXE assembly.
Create a custom attribute to point out the plugin entry module:

VB.NET
''' <summary>
''' Module PlugInsMain.(目标模块,在本模块之中包含有一系列插件命令信息,本对象定义了插件在菜单之上的根菜单项目)
''' </summary>
''' <remarks></remarks>
<AttributeUsage(AttributeTargets.Class, allowmultiple:=False, inherited:=True)>
Public Class PlugInEntry : Inherits Attribute
    ''' <summary>
    ''' The name for this root menu.(菜单的根部节点的名称)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Name As String
    ''' <summary> 
    ''' The icon resource name for this root menu.(菜单对象的图标名称)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Icon As String = ""
    Public Overrides Function ToString() As String
        Return String.Format("PlugInEntry: {0}", Name)
    End Function
End Class

This custom attribute class object contains two properties to describe the menu root item in the form for this plugin assembly.

This plugin entry attribute describes the menu item Text property as “PlugIn Test Command” and an icon resource name for loading the icon image from the resource manager.
Here is an example plugin entry definition:

VB.NET
<PlugInEntry(name:="PlugIn Test Command", Icon:="Firefox")>
Module MainEntry
……
End Module 

Image 1

So how to find out this entry module from the target loaded assembly module? Here is how, using a LINQ and reflection operation parsing the Meta data:

VB.NET
Dim EntryType As System.Type = GetType(PlugInEntry), _
PluginCommandType = GetType(PlugInCommand), IconLoaderEntry = GetType(Icon)
Dim FindModule = From [Module] In Assembly.DefinedTypes
                 Let attributes = [Module].GetCustomAttributes(EntryType, False)
                 Where attributes.Count = 1
                 Select New KeyValuePair(Of PlugInEntry, _
                 Reflection.TypeInfo)(DirectCast(attributes(0), PlugInEntry), [Module]) '
Dim MainModule = FindModule.First  'Get the plugin entry module.(获取插件主模块)

Target plugin entry module must contain a PluginEntry custom attribute!

3. Get Command Entry

Now we can find out the module which contains the plugin commands, then we must load these plugin commands, and create a menu item for each plugin command.
Here, we will use another custom attribute to help us find out the plugin command in the plugin entry module:

VB.NET
''' <summary>
''' Function Main(Target As Form) As Object.(应用于目标模块中的一个函数的自定义属性,相对应于菜单中的一个项目)
''' </summary>
''' <remarks></remarks>
<AttributeUsage(AttributeTargets.Method, allowmultiple:=False, inherited:=True)>
Public Class PlugInCommand : Inherits Attribute
    Public Property Name As String
    ''' <summary>
    ''' The menu path for this plugin command.(这个插件命令的菜单路径)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Path As String = "\"
    ''' <summary>
    ''' The icon resource name.(图标资源名称,当本属性值为空的时候,对应的菜单项没有图标)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Icon As String = ""
    Dim Method As Reflection.MethodInfo
    Public Overrides Function ToString() As String
        If String.IsNullOrEmpty(Path) OrElse String.Equals("\", Path) Then
            Return String.Format("Name:={0}; Path:\\Root", Name)
        Else
            Return String.Format("Name:={0}; Path:\\{1}", Name, Path)
        End If
    End Function
    Public Function Invoke(Target As System.Windows.Forms.Form) As Object
        Return PlugInEntry.Invoke({Target}, Method)
    End Function
    Friend Function Initialize(Method As Reflection.MethodInfo) As PlugInCommand
        Me.Method = Method
        Return Me
    End Function
End Class

The load method is the same as we find out the plugin entry module.

VB.NET
Dim LQuery = From Method In MainModule.Value.GetMethods
             Let attributes = Method.GetCustomAttributes(PluginCommandType, False)
             Where attributes.Count = 1
             Let command = DirectCast(attributes(0), PlugInCommand).Initialize(Method)
             Select command Order By command.Path Descending  'Load the available
                                                            'plugin commands.(加载插件模块中可用的命令)

Now from comparing the picture and example command definition, you can find out how to use this custom attribute.

VB.NET
<PlugInEntry(name:="PlugIn Test Command", Icon:="FireFox")>
Module MainEntry
    <PlugIn.PlugInCommand(name:="Test Command1", path:="\Folder1\A")> _
     Public Function Command1(Form As System.Windows.Forms.Form) As String
        MsgBox("Test Command 1 " & vbCrLf & String.Format("Target form title is ""{0}""", Form.Text))
        Return 1
    End Function
    <PlugIn.PlugInCommand(name:="Open Terminal", path:="\Item2")> _
     Public Function TestCommand2() As Integer
        Process.Start("cmd")
        Return 1
    End Function
    <PlugIn.PlugInCommand(name:="Open File", path:="\Folder1\", icon:="FireFox")> _
     Public Function TestCommand3() As Integer
        Process.Start(My.Application.Info.DirectoryPath & "./test2.vbs")
        Return 1
    End Function 

Image 2

4. Dynamically Create the Menu Item for Each Plugin Command

Creating a menu strip item is easy coding, you can learn to create the menu item from the form designer auto generated code, here I write a Recursive function to create the menu item for each plugin command:

VB.NET
''' <summary>
''' Recursive function for create the menu item for each plugin command.(递归的添加菜单项)
''' </summary>
''' <param name="MenuRoot"></param>
''' <param name="Path"></param>
''' <param name="Name"></param>
''' <param name="p"></param>
''' <returns></returns>
''' <remarks></remarks>
Private Shared Function AddCommand(MenuRoot As System.Windows.Forms.ToolStripMenuItem, _
   Path As String(), Name As String, p As Integer) As System.Windows.Forms.ToolStripMenuItem
    Dim NewItem As System.Func(Of String, ToolStripMenuItem) = _
                             Function(sName As String) As ToolStripMenuItem
                             Dim MenuItem = New System.Windows.Forms.ToolStripMenuItem()
                                            MenuItem.Text = sName
                                            MenuRoot.DropDownItems.Add(MenuItem)
                                            Return MenuItem
                                         End Function
    If p = Path.Count Then
        Return NewItem(Name)
    Else
        Dim LQuery = From menuItem As ToolStripMenuItem In MenuRoot.DropDownItems _
                     Where String.Equals(menuItem.Text, Path(p)) Select menuItem '
        Dim Items = LQuery.ToArray
        Dim Item As ToolStripMenuItem
        If Items.Count = 0 Then Item = NewItem(Path(p)) Else Item = Items.First
        Return AddCommand(Item, Path, Name, p + 1)
    End If
End Function

The menu icon entry: and at last I define an icon loading attribute for specific function to load the menu icon from the plugin assembly DLL resource manager:

VB.NET
''' <summary>
''' Function Icon(Name As String) As System.Drawing.Image.
''' (本自定义属性指明了目标模块中的一个用于获取图标资源的方法)
''' </summary>
''' <remarks></remarks>
<AttributeUsage(AttributeTargets.Method, allowmultiple:=False, inherited:=True)>
Public Class Icon : Inherits Attribute
End Class 

The icon image resource loading interface is expressed as:

VB.NET
<Icon()> Public Function Icon(Name As String) As System.Drawing.Image
        Dim Objedc = My.Resources.ResourceManager.GetObject(Name)
        Return DirectCast(Objedc, System.Drawing.Image)
End Function 

Which loads the image resource from the resource manager using a specific resource name string.

Using AddHandler and lamda expression to associate the control event to a specific procedure function.
AddHandler Item.Click, Sub() Command.Invoke(Target) '关联命令

5. Passing the Argument to the Target Method

It is a problem to pass the parameter to the target method as we are not sure about the number of parameters that will appear in the target function, so that we will not get an unexpected exception.

VB.NET
''' <summary>
'''
''' </summary>
''' <param name="Parameters">Method calling parameters object array.</param>
''' <param name="Method">Target method reflection information.</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function Invoke(Parameters As Object(), Method As Reflection.MethodInfo) As Object
    Dim NumberOfParameters = Method.GetParameters().Length
    Dim CallParameters() As Object
    If Parameters.Length < NumberOfParameters Then
        CallParameters = New Object(NumberOfParameters - 1) {}
        Parameters.CopyTo(CallParameters, 0)
    ElseIf Parameters.Length > NumberOfParameters Then
        CallParameters = New Object(NumberOfParameters - 1) {}
        Call Array.ConstrainedCopy(Parameters, 0, CallParameters, 0, NumberOfParameters)
    Else
        CallParameters = Parameters
    End If
    Return Method.Invoke(Nothing, CallParameters)
End Function

Here, I post the full plugin loading function which contains the loading steps I described above:

VB.NET
    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="Menu"></param>
    ''' <param name="AssemblyPath">Target dll assembly file.(目标程序集模块的文件名)</param>
    ''' <returns>返回成功加载的命令的数目</returns>
    ''' <remarks></remarks>
    Public Shared Function LoadPlugIn(Menu As MenuStrip, AssemblyPath As String) As Integer
        If Not FileIO.FileSystem.FileExists(AssemblyPath) Then 'When the filesystem object 
                     'can not find the assembly file, then this loading operation was abort.
            Return 0
        Else
            AssemblyPath = IO.Path.GetFullPath(AssemblyPath) 'Assembly.LoadFile required 
                                                             'full path of a program assembly file.
        End If
        Dim Assembly As Reflection.Assembly = Reflection.Assembly.LoadFile(AssemblyPath)
        Dim EntryType As System.Type = GetType(PlugInEntry), _
           PluginCommandType = GetType(PlugInCommand), IconLoaderEntry = GetType(Icon)
        Dim FindModule = From [Module] In Assembly.DefinedTypes
                         Let attributes = [Module].GetCustomAttributes(EntryType, False)
                         Where attributes.Count = 1
                         Select New KeyValuePair(Of PlugInEntry, _
                         Reflection.TypeInfo)(DirectCast(attributes(0), PlugInEntry), [Module]) '
        Dim MainModule = FindModule.First  'Get the plugin entry module.(获取插件主模块)
        Dim LQuery = From Method In MainModule.Value.GetMethods
                     Let attributes = Method.GetCustomAttributes(PluginCommandType, False)
                     Where attributes.Count = 1
                     Let command = DirectCast(attributes(0), PlugInCommand).Initialize(Method)
                     Select command Order By command.Path Descending  'Load the available 
                                                         'plugin commands.(加载插件模块中可用的命令)
        Dim Icon = From Method In MainModule.Value.GetMethods Where 1 = _
            Method.GetCustomAttributes(IconLoaderEntry, False).Count Select Method '菜单图标加载函数
        Dim IconLoader As Reflection.MethodInfo = Nothing
        If Icon.Count > 0 Then
            IconLoader = Icon.First
        End If
        Dim MenuEntry = New System.Windows.Forms.ToolStripMenuItem()   '生成入口点,并加载于UI之上
        MenuEntry.Text = MainModule.Key.Name
        If Not IconLoader Is Nothing Then MenuEntry.Image = Invoke({MainModule.Key.Icon}, IconLoader)
        Menu.Items.Add(MenuEntry)
        Dim Commands = LQuery.ToArray
        Dim Target As System.Windows.Forms.Form = Menu.FindForm
        For Each Command As PlugInCommand In Commands   '生成子菜单命令
            Dim Item As ToolStripMenuItem = AddCommand(MenuEntry, _
               (From s As String In Command.Path.Split("\") _
                Where Not String.IsNullOrEmpty(s) Select s).ToArray, Command.Name, p:=0)
            If Not IconLoader Is Nothing Then Item.Image = Invoke({Command.Icon}, IconLoader)
            AddHandler Item.Click, Sub() Command.Invoke(Target)      '关联命令
        Next
        Return Commands.Count
End Function

6. Testing

There are two test plugin example in my upload project file, you can look into the two example to know how to using this plugin example library

In the testing form, we have nothing but a menu control on it, and then in the load event, we load the plugin assembly from a specific file:

VB.NET
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Call PlugIn.PlugInEntry.LoadPlugIn(Me.MenuStrip1, "./plugins/TestPlugIn.dll")
    Call PlugIn.PlugInEntry.LoadPlugIn(Me.MenuStrip1, "./plugins/TestPlugIn2.dll")
End Sub

Then let the the plugin module procedure some data and display on the target form.

Image 3

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)