Click here to Skip to main content
Click here to Skip to main content

Develop a Plugin extension for your VisualBasic application

, 29 Dec 2013
Rate this:
Please Sign up or sign in to vote.
A simple and easy way to develop a application plugin
Prize winner in Competition "Best VB.NET article of December 2013"

Download PlugIns-noexe.zip

Download PlugIns.zip

Introduction: Why we want a plugin?

1. Makes your application extendable

Sometimes you just want let other people able to extend you
program to meet the Practice application requirements without modify 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 are support plugin extension. if you want your program more useful and friendly, then you should allowing your user to developing 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 function in the reflection operation:

1. Load a application assembly from a file. (Dll or EXE file which is write in .NET)

Reflection.Assembly.LoadFile(AssemblyPath) 

2. GetType: A useful keyword in VisualBasic to read the meta data of a type

Dim Type As System.Type = GetType(TypeName)

3. GetMethods, GetMembers, GetProperties, etc

Those function which is start with Get, they can let your code know something information about what contains 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 use operation in our program.

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:
  Dim Assembly As Reflection.Assembly = Reflection.Assembly.LoadFile(AssemblyPath)
But please make sure that this function is required 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 execute 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 that 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:
''' <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 property to descript the menu root item in the form for this plugin assembly.

This plugin entry attribute descript the menu item Text property as “PlugIn Test Command” and a icon resource name for load the icon image from the resource manager.
Here is a example plugin entry definition:
<PlugInEntry(name:="PlugIn Test Command", Icon:="Firefox")>
Module MainEntry
……
End Module 


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:

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 contains a PluginEntry custom attribute!

3. Get Command Entry

Now we can find out the module which is contains the plugin commands, then we must load these plugin command, 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:
''' <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 as the same as we findout 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.(加载插件模块中可用的命令)
Now from compare the picture and example command definition, you can find out how to use this custom attribute.

<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 

4. Dynamic create the menu item for each plugin command

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

''' <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 a icon loading attribute for specific the function to load the menu icon from the plugin assembly DLL resource manager:
''' <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 express as
<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 is load the image resource from then 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 passing the parameter to the target method as we are not sure the number of parameter will be appears in the target function, so that we will not to get an unexpected exception
    ''' <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 descript above:
    ''' <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:
    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.

License

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

About the Author

Mr. Xie.G.Gang 谢桂纲
Student Guangxi University
China China
A student of Genetics major, doing Bioinformatics programming, Molecular Biology and Microbial Genetics research. Interesting in Data Mining of Bioinformatics data and wanna working in Google. Now he is working hard on his Laboratory Experiments for his first research article about the analysis of the Signal Transduction Network in the bacterial Xanthomonas campestris pathovar carnpestris 8004.
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralMy vote of 4 Pinprofessional i004-Jan-14 1:41 
GeneralRe: My vote of 4 PinmemberMr. Xie.G.Gang 谢桂纲16-Jan-14 20:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140709.1 | Last Updated 30 Dec 2013
Article Copyright 2013 by Mr. Xie.G.Gang 谢桂纲
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid