65.9K
CodeProject 正在变化。 阅读更多。
Home

为您的 VisualBasic 应用程序开发插件扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (14投票s)

2013年12月30日

CPOL

4分钟阅读

viewsIcon

31985

downloadIcon

1453

一种开发应用程序插件的简单易行的方法。

简介:为什么我们需要插件?

  1. 使您的应用程序可扩展

    有时,您只是想让其他人能够扩展您的程序以满足实际应用需求,而无需修改源代码并重新编译,因此程序插件是一种便捷的方式。

  2. 让您的应用程序编码更酷

    正如您所见,几乎所有开源程序或广泛传播的程序都支持插件扩展。如果您希望您的程序更有用和友好,那么您应该允许您的用户开发插件来满足他们的实际需求。

背景:使用反射动态加载程序集模块

本文将介绍如何以一种非常简单的方式为我的 VisualBasic 程序开发插件。其基本技术是 .NET 中的反射操作。

反射操作中有一些非常有用的函数,包括

  1. 从文件(用 .NET 编写的 DLL 或 EXE 文件)加载应用程序程序集
    Reflection.Assembly.LoadFile(AssemblyPath) 
  2. GetType:VisualBasic 中一个有用的关键字,用于读取类型的元数据
    Dim Type As System.Type = GetType(TypeName)
  3. GetMethods、GetMembers、GetProperties 等。

    那些以 Get 开头的函数可以让您的代码了解目标类对象包含的内容。

  4. GetCustomAttributes

    反射中一个非常有用的函数,用于获取代码的一些目标,并使用自定义属性作为标志来指出哪个成员是我们的目标。

  5. LINQ:VisualBasic 中一个有用的查询语句

    LINQ 语句是 VisualBasic 中的类似 SQL 的语句,而 LINQ To Object 是我们程序中最常用的操作。

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

Using the Code

1. 加载程序集模块文件

只需一个简单的步骤,即可使用反射从特定文件加载程序集模块

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

但请确保此功能需要一个绝对路径字符串值。
EXE 模块与 VisualBasic 中的 DLL 扩展模块是相同的对象。DLL 和 EXE 模块之间的区别在于,EXE 模块总是存在一个 Main 入口点用于执行程序集。因此,您可以使用此函数尝试加载任何 .NET 程序集模块。

2. 获取模块入口

在此步骤中,我们将尝试找到一个包含插件命令的模块,我称之为 PluginEntry。在我看来,模块入口与 EXE 程序集的入口点相同。
创建一个自定义属性来指向插件入口模块

''' <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

这个自定义属性类对象包含两个属性,用于描述该插件程序集在表单中的菜单根项。

此插件入口属性将菜单项的 Text 属性描述为“PlugIn Test Command”,并提供一个图标资源名称,用于从资源管理器加载图标图像。
这是一个示例插件入口定义

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

那么如何从目标加载的程序集模块中找到这个入口模块呢?方法是使用 LINQ 和反射操作来解析元数据。

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.(获取插件主模块)

目标插件入口模块必须包含一个 PluginEntry 自定义属性!

3. 获取命令入口

现在我们可以找到包含插件命令的模块,然后我们必须加载这些插件命令,并为每个插件命令创建一个菜单项。
在这里,我们将使用另一个自定义属性来帮助我们找到插件入口模块中的插件命令

''' <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

load 方法与我们查找插件入口模块的方法相同。

   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.(加载插件模块中可用的命令)

现在,通过比较图片和示例命令定义,您可以找出如何使用此自定义属性。

<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. 为每个插件命令动态创建菜单项

创建菜单项是一项简单的编码任务,您可以从表单设计器的自动生成代码中学习如何创建菜单项。我在这里编写了一个递归函数来为每个插件命令创建菜单项。

''' <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

菜单图标条目:最后,我定义了一个图标加载属性,用于从插件程序集 DLL 资源管理器加载菜单图标。

''' <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 

图标图像资源加载接口表示为

<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 

使用指定的资源名称字符串从资源管理器加载图像资源。

使用 AddHandler 和 lambda 表达式将控件事件与特定的过程函数关联起来。
AddHandler Item.Click, Sub() Command.Invoke(Target) '关联命令

5. 将参数传递给目标方法

将参数传递给目标方法是一个问题,因为我们不确定目标方法中会出现多少参数,这样我们就不会得到意外的异常。

    ''' <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 

在这里,我将包含上述加载步骤的完整插件加载函数发布出来。

    ''' <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. 测试

我的上传项目文件中包含两个测试插件示例,您可以查看这两个示例来了解如何使用此插件示例库。

在测试表单中,我们只有菜单控件,然后在 load 事件中,我们从特定文件加载插件程序集。

    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 

然后让插件模块处理一些数据并在目标表单上显示。

© . All rights reserved.