动态加载类
Billy Hollis
2002年10月14日
从 MSDN Code Center 下载 VBNETDynamicForms.exe 示例文件(英文)。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者进行理解。)
摘要:本月 Billy Hollis 对 Microsoft .NET 的扩展性进行了探讨,并说明了如何使用 System.Reflection 命名空间在 Visual Basic 应用程序中动态加载类。
理想情况下,编写软件系统之前我们就应该知道此软件系统需要完成哪些操作。但实际情况并非如此,因此我们的系统应该是可以调整的。
最能体现这种适应性的是动态集成新功能的能力。例如,在 Windows 窗体应用程序中,这种能力是指加载编译原始应用程序很久以后创建的窗体的能力。
这种操作在 Microsoft Visual Basic® 6.0 中几乎是不可能的,但是在 Visual Basic .NET 中却相当容易。利用 System.Reflection 命名空间的功能,程序只需知道程序集的位置即可请求动态加载该程序集。然后即可加载该程序集中的类并对类进行实例化。由于 Windows 窗体也是一种类,这就意味着可以动态地加载、显示和使用窗体,即使在编译原始程序时窗体并不存在。
典型的方案
假设我们有一个多文档界面 (MDI) 应用程序,需要具有将任意窗体加载为子窗体的能力。那么应用程序应该能够执行以下操作:
- 获取可以加载的窗体的列表,通常不在 .NET 配置文件中。
- 显示该列表并允许用户从中选择一个窗体。
- 实例化并显示用户选择的窗体。
让我们编写这样一个应用程序,看看如何完成动态加载。
步骤 1:创建项目和 MDI 窗体
启动一个新的 Windows 窗体项目。将其命名为 FormsOnTheFly。在新项目中包含的空窗体 Form1 中,将它的 IsMdiContainer 属性更改为 True。这样,该窗体即变成一个 MDI 父窗体。更改窗体的大小,使窗体的长和宽的尺寸大约为默认值的两倍。
将一个面板控件拖动到窗体上,然后设置它的 Dock 属性,使它靠接在窗体的顶部。更改面板的大小,使它的高度大约为 0.5 英寸。
将一个组合框拖动到面板上。将它命名为 cboForms,然后将它的 DropDownStyle 设置为 DropDownList。
最后,将一个按钮拖动到面板上。将它命名为 btnLoadForm,然后将它的 Text 属性设置为 Load Form。
此时,Form1 应如图 1 所示。
图 1:添加所有控件后处于设计模式下的 MDI 窗体
步骤 2:生成一个类,以存放可用窗体的数据
我们将获取可能从基于 XML 的配置文件之外加载的窗体的有关信息。但是,为了更便于在组合框中显示可用窗体并获取有关所选窗体的信息,还应建立一个对象集合。集合中的每个对象都将存放一个可用窗体的信息。对象应具有以下属性:
- Description:显示在组合框中的窗体说明。
- Location:窗体所在的 DLL 的文件名。
- Type:窗体的 .NET 类型的名称(例如,MyProject.Form1)。
这样的集合可以数据绑定到列表框中(有关详细信息,请参阅 Not Your Father's Databinding [英文])。要从列表框中返回对所选对象的引用,还需要一个属性,我们称之为 Reference。
要创建类,请选择 Project | Add Class(项目|添加类),然后将类命名为 DynamicClass.vb。在类中添加以下代码:
Public Class DynamicClass Dim msLocation As String Dim msType As String Dim msDescription As String Public Sub New(ByVal sLocation As String, _ ByVal sDescription As String, _ ByVal sType As String) Me.Location = sLocation Me.Description = sDescription Me.Type = sType End Sub Public Property Location() As String Get Return msLocation End Get Set(ByVal Value As String) msLocation = Value End Set End Property Public Property Type() As String Get Return msType End Get Set(ByVal Value As String) msType = Value End Set End Property Public Property Description() As String Get Return msDescription End Get Set(ByVal Value As String) msDescription = Value End Set End Property Public ReadOnly Property Reference() As Object Get Return Me End Get End Property End Class
步骤 3:创建配置文件以存放可用窗体
应用程序在运行时需要的某些信息可能在编译时无法提供,这些信息通常放置在配置文件中。在 Visual Basic 6.0 中,配置文件应该是 INI 文件或 Windows 注册表。而在 .NET 中,则使用基于 XML 的配置文件。
我们无法详细介绍配置文件,因为这个主题非常复杂。但是,您应该知道,Windows 窗体应用程序的配置文件与应用程序的 EXE 启动文件在同一个目录中。配置文件的名称与程序的 EXE 启动文件的名称相同,只不过在 EXE 文件名后添加了后缀 .config。这就是说,如果执行 MyApp.exe 程序可启动我的应用程序,则配置文件的名称一定是 MyApp.exe.config,而且配置文件必须与 MyApp.exe 位于同一个目录中。
以下是示例中要使用的配置文件:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="availableclasses" type="System.Configuration.NameValueSectionHandler" /> </configSections> <availableclasses> <add key="Placeholder – do not load" value="DLLPathnameGoesHere~NameOfTypeGoesHere"></add> </availableclasses> </configuration>
此处,<availableclasses>
标记是占位符信息,便于人们看清格式。稍后,我们还会再返回来,为创建的新窗体添加配置信息。
这实际上并不是存放窗体配置信息的理想方式,因为我们用符号分隔的方式在同一位置保存 DLL 位置和类型名称。但是,使用高级方法分别存放这些信息会要求相当多的注释和代码,所以我们暂且使用这种替代方法。
使用某些文本编辑器或 XML 编辑器(或 Microsoft Visual Studio®)创建上述配置文件,然后使用 FormsOnTheFly.exe.config 文件名将其保存在 FormsOnTheFly 项目的 \bin 目录下。因为 .NET 配置类使用区分大小写的 XML 标记,所以创建此文件时,请注意 XML 标记中字母的大小写。
步骤 4:将配置信息读入集合中
我们为窗体编写的代码将使用 System.Configuration 和 System.Reflection 命名空间中的类。请将以下两行代码置于 Form1 代码的最顶端,以便更方便地访问这些类:
Imports System.Configuration Imports System.Reflection
还需要一个模块级变量来存放配置信息集合。请将以下代码行紧挨着 Inherits System.Windows.Forms.Form
代码行放在其下方:
Dim colAvailableClasses As ArrayList
现在,可以编写核心代码了。在 Form1 的 Form Load 事件中放置以下代码,以便读取配置文件、创建存放信息的对象集合以及将集合数据绑定到组合框:
' 实例化配置信息集合。 colAvailableClasses = New ArrayList() ' 获取要从配置文件中加载的可用项。 Dim ClassConfigValues As Specialized.NameValueCollection ClassConfigValues = CType(ConfigurationSettings.GetConfig("availableclasses"), _ Specialized.NameValueCollection) Dim iIndex As Integer Dim sLocation As String Dim sDescription As String Dim sType As String Dim sValue As String ' 创建可绑定到组合框的可用项的 ' 集合。 For iIndex = 0 To ClassConfigValues.Count - 1 sDescription = ClassConfigValues.Keys(iIndex) sValue = ClassConfigValues.Item(sDescription) ' 经过简单的处理,从一个字段中 ' 获取位置和类型。 Dim iPos As Integer iPos = InStr(sValue, "~") sLocation = Microsoft.VisualBasic.Left(sValue, iPos - 1) sType = Microsoft.VisualBasic.Right(sValue, Len(sValue) - iPos) Dim objNewForm As New DynamicClass(sLocation, sDescription, sType) colAvailableClasses.Add(objNewForm) Next ' 现在,将集合绑定到组合框。 ' 显示说明,并返回对象的引用。 cboForms.DataSource = colAvailableClasses cboForms.DisplayMember = "Description" cboForms.ValueMember = "Reference"
步骤 5:插入逻辑以加载所选窗体
现在,在 btnLoadForm 的 click 事件中放置以下逻辑:
Dim objFormToLoad As DynamicClass objFormToLoad = cboForms.SelectedValue Dim asmAssemblyContainingForm As [Assembly] = _ [Assembly].LoadFrom(objFormToLoad.Location) Dim TypeToLoad As Type = asmAssemblyContainingForm.GetType(objFormToLoad.Type) Dim GenericInstance As Object GenericInstance = Activator.CreateInstance(TypeToLoad) Dim FormToShow As Form = CType(GenericInstance, Form) FormToShow.MdiParent = Me FormToShow.Show()
这是程序的核心部分。通过使用集合中一个对象的信息实例化代码并显示窗体。让我们逐行说明这段代码。
首先我们引用了其中包含要加载窗体的位置和类型的对象 (objFormToLoad)。它被设置为组合框的 SelectedValue 属性,在从数据绑定的组合框返回所选内容时使用。
DLL 的位置包含在对象的 Location 属性中。Assembly 类的 LoadForm 方法使用该属性创建对程序集的引用。(将 Assembly 类置于括号中是因为 Assembly 是 .NET 关键字。括号将通知编译器,其中的内容不是正在使用的关键字,而是类名。)
下面,我们需要引用正在加载的 .NET 类型(类)。可以使用程序集的 GetType 方法,通过传递存放类型名称(该类型名称将从存放配置数据的对象的 Type 属性中获取)的字符串进行引用。对类型的引用保存在 TypeToLoad 中。
Reflection 类和 Activator 类使用它们的 CreateInstance 方法创建类型的实例。(CreateInstance 与 Visual Basic 6.0 中的 CreateObject 类似。)但是,实例必须是类型对象,因为该类型要动态加载。
最后,新实例化的对象(实际上是一个窗体)必须转换为正确的类型才能启用前期绑定。我们知道它是一个窗体,所以可以使用 CType 函数将其转换为窗体。
最后,将新窗体设置为 MDI 父窗体的子窗体并对其进行显示。
如果您阅读了 Death of the Browser?(英文),您会觉得上面的代码很熟悉。其逻辑与应用程序的 Internet 部署中使用的逻辑十分相似。如果您认为上述说明不够详细,请参阅 Death of the Browser?(英文)。
注意:从 Death of the Browser?(英文)所示的 URL 处加载的程序集被复制到本地缓存中。从 UNC 加载的程序集(如本文中的程序集)仅在当前位置使用,不被复制到任何缓存中。
步骤 6:编译应用程序
现在,我们可以编译应用程序,但由于尚未创建任何窗体,所以不会显示任何窗体。可以编译并运行程序,确保它能够工作并确保组合框能够正确加载占位符项。如果单击 btnLoadForm,则会显示错误信息或教程,因为配置文件中的信息还未指向任何对象。
步骤 7:创建要显示的窗体
现在,开始创建名为 FirstForm 的新的 Windows 窗体应用程序。在出现的空白 Form1 上放置一些控件 - 控件类型不限。
然后在 Solution Explorer(解决方案资源管理器)中的 FirstForms 项目上单击鼠标右键,选择 Properties(属性)。在 Output Type(输出类型)组合框中选择 Class Library(类库)。如果未看到组合框,可以在 Solution Explorer(解决方案资源管理器)的 Solution(解决方案),而不是 Project(项目)上单击鼠标右键。
现在开始创建项目。即创建一个包含该窗体的 DLL。
创建一个名为 C:\NewForms 的目录。将 FirstForms.dll 从 FirstForms 的 \bin 目录复制到 C:\NewForms 中。
对名为 SecondForm 和 ThirdForm 的项目重复上述操作。在每个窗体中拖入不同的控件以便于区分。也可以将每个窗体的背景色更改为独特的颜色。
步骤 8:用新的窗体信息更新配置文件
现在,我们已经创建了一些新窗体,还需要在配置文件中引用它们。请用以下代码行替换 FormsOnTheFly.exe.config 中的占位符信息:
<add key="First Form" value="C:\NewForms\FirstForm.dll~FirstForm.Form1"></add> <add key="Second Form" value="C:\NewForms\SecondForm.dll~SecondForm.Form1"></add> <add key="Third Form" value="C:\NewForms\ThirdForm.dll~ThirdForm.Form1"></add>
如果要更改窗体的位置或名称,则需要对以上代码行进行相应修改。
步骤 9:运行 FormsOnTheFly.exe 并加载窗体
现在执行 FormsOnTheFly.exe(不加载 Visual Studio)。选择组合框中的一个窗体,然后单击 btnLoadForm。如果正确完成了所有步骤,则可以看到子窗体加载到 MDI 窗口中(即使编译 MDI 应用程序时子窗体并不存在)。
图 2 显示了动态加载窗体后的 MDI 窗体。
图 2:动态加载窗体后的 MDI 窗体
此时,您可以根据需要创建任意多个新窗体并将其加载到 MDI 应用程序中。要使其可用,请将其编译为类库,然后在配置文件中添加对它们的引用。
动态加载其他类型的类
此技术除了可用于窗体外,还可用于其他类型的类。但是,情况相对较复杂。在上面的示例中,之所以能够对 System.Windows.Forms.Form 类接口进行前期绑定是因为我们知道它是一个窗体。因此可以使用窗体的方法(例如,Show 方法)。但对于我们自己创建的类,可以依据什么接口进行绑定呢?
答案是创建我们自己的接口。实际上,这是一个重要的实现接口的典型示例。您可能在 Visual Basic 6.0 中使用过接口。接口作为空类而创建。Visual Basic .NET 中有了新的语法,接口与类完全分开,进行单独声明。在下面的步骤 1 中,我们会看到如何实现这一操作。
让我们设计一个可以动态加载类的示例。假设我们要编写一个需要操纵数据集的数据清理应用程序。但是,正如所有数据清理程序一样,它们似乎从来都没有完成。似乎总是需要为新的校验类型和清理任务编写代码。
当然,可以创建新的清理逻辑并重新编译整个应用程序,但如果可以动态插入新的数据清理功能而不必重新编译主应用程序,是不是会更好?下面让我们从头开始创建这样一个示例。
步骤 1:创建类文件以存放接口
从概念上讲,在 Visual Basic .NET 中创建接口与在 Visual Basic 6.0 中创建接口相似,但在语法上有很大不同。要创建初始接口类,请创建一个类型为 Class Library(类库)的新项目。将其命名为 ScrubberInterface。
在创建的类文件中,用以下代码替换文件中现有的代码行:
Public Interface IScrubber Sub Scrub(ByVal ds As DataSet) End Interface
我们的接口非常简单。我们需要一个操作数据集的方法。已将此方法命名为 Scrub。
现在,创建项目。ScrubberInterface.DLL 将创建在项目的 \bin 目录下。
步骤 2:创建清理应用程序
清理应用程序与前面的窗体应用程序示例在许多方面都很相似。不同之处在于,我们将依次使用所有的数据清理类,而不是选择使用一个特定的类。
创建一个新的 Windows 窗体应用程序,然后将其命名为 ClassesOnTheFly。在显示的 Form1 中放置以下控件:
控件类型 | 名称 | 属性设置 |
---|---|---|
Button | btnLoadDataset | Text = "Load Dataset" |
Button | btnScrubDataset | Text = "Scrub Dataset" |
DataGrid | DataGrid1 | 不作更改 |
在 Form1 的代码顶部,放置与前述示例顶部代码相同的代码:
Imports System.Configuration Imports System.Reflection
这里也需要一个模块级变量来存放配置信息集合。将以下代码行置于 Inherits System.Windows.Forms.Form
代码行的下方:
Dim colAvailableClasses As ArrayList
步骤 3:将数据集加载到网格中进行处理
为简化操作,将从 XML 文件中加载数据集。将以下 XML 文件放置到 ClassesOnTheFly 项目的 \bin 目录中,然后将其命名为 TimeCardData.xml:
<TimeCardData> <Employee> <Name>Sherlock Holmes</Name> <ID>123</ID> </Employee> <Employee> <Name>John Watson</Name> <ID>345</ID> </Employee> <Employee> <Name>Irene Adler</Name> <ID>567</ID> </Employee> <Employee> <Name>Jabez Wilson</Name> <ID>789</ID> </Employee> </TimeCardData>
我们需要一个模块级的数据集引用,所以请将以下代码行放置在声明 colAvailableClasses
的代码行的下方:
Dim ds As DataSet
要读取数据集并加载网格,请将以下代码放置到 btnLoadDataset
的 click 事件中:
ds = New DataSet() ds.ReadXml("TimeCardData.xml") DataGrid1.DataSource = ds.Tables(0)
然后运行程序,确保正确加载网格。
步骤 4:添加对清理程序接口的引用
下面,我们需要添加对 DLL 的引用,该 DLL 是以前创建的,用来存放数据清理类的接口。选择 Project | Add Reference(项目|添加引用)。单击 Browse(浏览)按钮,浏览到 ScrubberInterface.DLL(在步骤 1 中创建),然后单击 Open(打开)。单击 OK(确定),添加引用。
步骤 5:使类在可用窗体上存放数据
现在,复制以前窗体示例中使用的 DynamicClass.vb 模块,将其插入到 ClassesOnTheFly 项目中。可以不进行修改,直接使用。
步骤 6:创建配置文件以存放可用窗体
创建一个与前面窗体示例中使用的配置文件完全相同的配置文件。配置文件应如下显示:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="availableclasses" type="System.Configuration.NameValueSectionHandler" /> </configSections> <availableclasses> <add key="Placeholder – do not load" value="DLLPathnameGoesHere~NameOfTypeGoesHere"></add> </availableclasses> </configuration>
请记住,将此文件放置到 ClassesOnTheFly 项目的 \bin 目录中,然后将其命名为 ClassesOnTheFly.exe.config。
步骤 7:将配置信息读入集合中
此操作几乎与前面窗体示例中的操作完全相同,不同之处在于我们现在未将集合绑定到组合框。所以,复制窗体示例的 Form Load 事件中使用的逻辑,但是省略最后三行代码(此三行代码用于执行绑定到组合框的操作)。将此逻辑粘贴到 ClassesOnTheFly 项目中 Form1 的 Form Load 事件中。
步骤 8:插入逻辑以加载和使用数据清理类
现在,在 btnScrubDataset 的 click 事件中放置以下逻辑:
Dim objScrubberClass As DynamicClass For Each objScrubberClass In colAvailableClasses Dim asmAssemblyContainingForm As [Assembly] = _ [Assembly].LoadFrom(objScrubberClass.Location) Dim TypeToLoad As Type = _ asmAssemblyContainingForm.GetType(objScrubberClass.Type) Dim GenericInstance As Object GenericInstance = Activator.CreateInstance(TypeToLoad) Dim Scrubber As ScrubberInterface.IScrubber = _ CType(GenericInstance, ScrubberInterface.IScrubber) Scrubber.Scrub(ds) Next
此逻辑与动态加载窗体的逻辑有很多相似之处,这里不再作详细说明。主要的不同之处在于:
- 对配置文件中的每一个类都进行动态加载。
- 新实例化的对象被转换为 ScrubberInterface.IScrubber 类型。这样就可以绑定到 IScrubber 接口。
- 执行每个对象的 Scrub 方法,在数据集中进行传递。
步骤 9:编译并运行应用程序
运行应用程序以确保能够正确编译。但是,先不要单击 Scrub Dataset(清理数据集)按钮,因为尚未创建清理类。
应用程序编译完成后,关闭 ClassesOnTheFly 项目中的 Visual Studio。
步骤 10:创建数据清理类
在 Visual Studio 中,创建一个类型为 Class Library(类库)的新项目。将其命名为 FirstClass。用以下代码替换 Class1 中自动插入的代码:
Public Class FirstClass Implements ScrubberInterface.IScrubber Public Sub Scrub(ByVal ds As DataSet) _ Implements ScrubberInterface.IScrubber.Scrub Dim dr As DataRow dr = ds.Tables(0).NewRow dr.Item(0) = "Professor Moriarty" dr.Item(1) = "666" ds.Tables(0).Rows.Add(dr) End Sub End Class
此类实现 IScrubber 接口(只是一个方法)。该方法获取数据集,并在数据集中添加一个单独的行。当然,实际的数据清理类中应已具备所有必要的数据操作逻辑。
创建此项目以获取 FirstClass.DLL。将 DLL 从项目的 \bin 目录复制到名为 C:\ScrubberClasses 的新目录中。
步骤 11:用新类更新配置文件
现在,返回到 ClassOnTheFly.exe.config。更改 <availableclasses>
标记中的内容,使其如下所示:
<add key="First Class" value="C:\ScrubberClasses\FirstClass.dll~FirstClass.FirstClass">
保存配置文件,然后执行最后一步操作。
步骤 12:测试新的数据清理类的操作
现在,运行 ClassesOnTheFly.exe 并单击 Load Dataset(加载数据集)按钮。请注意网格包含四行。单击 Scrub Dataset(清理数据集)按钮。网格中将显示第五行(这是由数据清理类添加的)。
如果需要,可以添加其他清理类,在数据集上执行所需的任何操作。只需创建清理类并将其添加到配置文件中即可。这样,单击 Scrub Dataset(清理数据集)按钮时,将自动使用这些清理类。
小结
这两个示例中最重要的一点是,在创建和编译原始应用程序(FormsOnTheFly 和 ClassesOnTheFly)时,并不包含对后来动态加载的窗体和类的引用。实际上,这些窗体和类在编译应用程序时还没有创建!
创建窗体和类之后,只需在配置文件中引用它们的位置和类型,即可使用它们更新应用程序。可以根据需要创建新的窗体和类并进行动态添加。如果您的应用程序需要具有这种扩展功能,则可以使用 .NET 提供的完善解决方案,它具有反映和动态加载类的功能。
评论