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:
<AttributeUsage(AttributeTargets.Class, allowmultiple:=False, inherited:=True)>
Public Class PlugInEntry : Inherits Attribute
Public Property Name As String
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
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:
<AttributeUsage(AttributeTargets.Method, allowmultiple:=False, inherited:=True)>
Public Class PlugInCommand : Inherits Attribute
Public Property Name As String
Public Property Path As String = "\"
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
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:
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:
<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.
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:
Public Shared Function LoadPlugIn(Menu As MenuStrip, AssemblyPath As String) As Integer
If Not FileIO.FileSystem.FileExists(AssemblyPath) Then
Return 0
Else
AssemblyPath = IO.Path.GetFullPath(AssemblyPath)
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
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
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()
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.