Develop a Plugin Extension for Your VisualBasic Application






4.50/5 (14 votes)
A simple and easy way to develop an application plugin
Introduction: Why We Want a Plugin?
- 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.
- 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:
- Load an application assembly from a file (DLL or EXE file which is written in .NET):
Reflection.Assembly.LoadFile(AssemblyPath)
GetType
: A useful keyword in VisualBasic to read the meta data of a type:Dim Type As System.Type = GetType(TypeName)
- 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. 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.
- 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.
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 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:
''' <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:
<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 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:
''' <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.
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.
<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. 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:
''' <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:
''' <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:
<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.
''' <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:
''' <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.