有时,编程中的革新会使人不得不放弃以前的所有工作。举一个很极端的例子,假设您已有多年 Visual Basic 编程经验,象许多开发人员一样,您当时已建立起一个颇具规模的代码库,而且,由于您听从了各种语言专家的建议,代码都是“组件化的”。也就是说,通过使用 COM(组件对象模型,即以前的 Microsoft® ActiveX®)服务器,已将应用程序分成了可调用的功能块。当然,您还很有可能获得了其他开发人员或其他公司对组件(如 ActiveX 控件)的可观投资。
但是,如果您决定彻底改变,转到其他操作系统上进行开发,会怎样呢?这时,您对 COM 的全部投资将变得一文不值;而且您无法使用任何现有的代码,而不得不从头学习如何在新的平台上进行开发。毫无疑问,这一切会使您的工作效率大大降低。
幸运的是,从 COM 转换到 .NET 不会对您的工作效率造成如此巨大的影响。之所以能够从 COM 开发轻松地转为 .NET 开发,而毫不损失代码库或工作效率,得益于两个关键概念:
- .NET 组件可以调用 COM 组件。
- COM 组件可以调用 .NET 组件。
这种双向的互操作性是从 COM 迁移到 .NET 的关键。在了解 .NET 的复杂性的同时,您可以继续使用 COM 组件。在很多情况下都能体现出这种互操作性的好处:
- 转到 .NET 不可能一蹴而就。因为学习 .NET 的编程概念和实现方案需要一些时间,所以您可能发现尽管您、您的同事和供应商学得很快,您仍然需要继续使用 COM 代码。
- 可以迁移到 .NET 中的代码不可能一次全部迁移完成,您需要分别迁移各个组件并进行测试。
- 也许您所使用的第三方 COM 组件无法转换到 .NET,而供应商还没有发行 .NET 版本。
- 虽然 Visual Basic 6.0 代码将迁移到 .NET,但效果不理想。由于实现方案或语言特性,可能有一些组件无法迁移到 .NET。
本文介绍了从 .COM 客户端调用 .NET 服务器的详细信息。在本系列的另一篇文章“从 .NET 调用 COM 组件”中,您将学习另一个方向的调用,即从 .NET 客户端调用 COM 服务器。
创建可供 COM 应用程序使用的 .NET 类
尽管 COM 客户端可以调用由 .NET 服务器在公共类中公开的代码,但是 COM 客户端不能直接访问 .NET 代码。为了能从 COM 客户端使用 .NET 代码,需要创建一个称为 COM 可调用包装 (CCW) 的代理。本节将介绍 CCW 的体系结构,以及创建和部署供 COM 客户端使用的 .NET 类所需的必要步骤。
COM 可调用包装
在 .NET 公共语言运行时 (CLR) 中运行的代码称为“管理代码”。它可以访问 CLR 提供的服务,如跨语言集成、安全性和版本控制以及垃圾回收。在 CLR 外运行的代码称为“非管理代码”。由于在 CLR 出现之前就已经设计出了 COM,而且 COM 代码不在 CLR 提供的基础结构中运行,所以它无法使用 CLR 的任何服务。根据定义,所有 COM 组件都是非管理代码。
管理代码组件不仅依靠 CLR,还需要能与它们进行交互操作的组件,以便依靠 CLR。由于 COM 组件不在 CLR 内运行,所以它们无法直接调用管理代码组件。非管理代码不能到达 CLR,所以无法直接调用管理组件。
解决这个难题的办法是使用代理。通俗的讲,代理是一种接收并修改一个组件的命令、再将这些命令转发给另一个组件的软件。在从非管理代码中调用管理代码的过程中所用到的特殊类型的代理称为 COM 可调用包装,即 CCW。图 1 说明了 CCW 如何跨越管理代码和非管理代码之间的边界。此图包括一个 COM 程序 (ComUI.exe) 和两个 .NET 组件(NETService.dll 和 Utility.dll),以及连接它们的必要技术。
图 1:使用 CCW 调用管理代码
创建 COM 可调用类的前提条件
在创建供 COM 客户端使用的 .NET 类时,请记住两个前提条件:
第一,要在 Visual Basic .NET 代码中明确定义一个接口,并让类实现该接口。例如,以下代码片段定义了一个名为 iFile 的接口以及一个实现该接口的类:
Public Interface iFile Property Length() As Integer End Interface Public Class TextFile Implements iFile ' 省略详细内容 End Class
通过接口来实现功能,对 COM 客户端很有帮助。在生成 CCW 时,.NET 使接口与早期版本保持兼容,这有助于防止当更改 .NET 服务器时中断 COM 客户端。
第二,COM 客户端可见的任何类都必须是已声明的公共类。创建 CCW 的工具只基于公共类定义类型。本规则同样适用于 COM 客户端使用的方法、属性和事件。
还应当考虑使用加密的密钥对,对包含将由 COM 使用的类的 .NET 程序集签名。Microsoft 称此操作为使用“强名称”对程序集签名。使用强名称对程序集签名,有助于 .NET 确保程序集中的代码在程序集发布后不被更改。这是对所有“全局程序集”(将由多个客户端共享的程序集)的要求,尽管 COM 客户端也可以调用未签名的程序集。
注意:通过直接将程序集作为“私有程序集”部署到 COM 客户端的目录下,可以从 COM 客户端使用未签名的程序集。本文不介绍私有程序集方法,因为全局程序集比具有大多数 COM 应用程序所具有的体系结构的私有程序集更具兼容性。
对所有程序集(甚至包括私有程序集)签名是个好方法。这有助于为管理类生成更好的 CLSID,并可以避免在不同程序集的类之间发生冲突。
要创建强名称,可以使用 sn 工具。此命令行工具有许多选项,在命令提示符处键入 sn /? 可以查看所有选项。对程序集签名所需的选项是“-k”,它将创建一个密钥文件。默认情况下,该文件使用 .snk 作为扩展名。例如,要创建一个名为 NETServer.snk 的密钥文件,可以使用以下命令行:
sn -k NETServer.snk
部署访问 COM 的应用程序
在创建了包含 COM 客户端可调用的类的 .NET 程序集之后,为了让 COM 可以使用该类,请执行以下三个步骤:
首先,必须为该程序集创建一个类型库。类型库是 .NET 程序集中所包含的元数据的 COM 等效组件。类型库通常包含在扩展名为 .tlb 的文件中。类型库包含必要信息,COM 客户端使用这些信息可以确定在特定服务器中有哪些类,以及这些类支持的方法、属性和事件。.NET 框架 SDK 包含一个名为 tlbexp(类型库导出程序)的工具,它可以从程序集创建类型库。tlbexp 包含许多选项,可以在命令提示符处键入 tlbexp /? 查看所有选项。其中一个是 /out 选项,用于指定已生成的类型库的名称。(如果您不自己指定名称,将自动为您创建一个名称。)例如,要将元数据从一个名为 NETServer.dll 的程序集提取到一个名为 NETServer.tlb 的类型库中,可以使用以下命令行:
tlbexp NETServer.dll /out:NETServer.tlb
然后,应使用 .NET 框架 SDK 中的程序集注册工具 (regasm),通过一次操作同时完成类型库的创建和注册。这是在一台计算机上同时进行 .NET 和 COM 开发所能使用的最简单的工具。与 tlbexp 类似,regasm 有许多选项。在命令提示符处键入 regasm /?,可以查看所有选项。要使用 regasm 创建并注册一个类型库,可以使用相应的命令行,如:
regasm /tlb:NETServer.tlb NETServer.dll
最后,必须将 .NET 程序集安装到全局程序集缓存 (GAC) 中,这样它才能作为共享程序集使用。要将程序集安装到 GAC 中,请使用 gacutil 工具:
gacutil /i NETServer.dll
同样,在命令提示符处键入 gacutil /?,可以查看 gacutil 的所有选项列表。
练习从 COM 调用 .NET 组件
在下例中,您将从 COM 代码使用 .NET 组件中的属性和方法。您将使用 regasm 从 .NET 程序集创建一个类型库并注册一个程序集,然后使用 gacutil 使该程序集可以在全局范围内可用。之后,您将看到如何在 Visual Basic 6.0 COM 代码中使用此 .NET 程序集。
创建 .NET 程序集
要创建包含公共类的 .NET 程序集,请执行以下步骤:
- 打开 Microsoft® Visual Studio® .Net,在起始页中单击 Class Library(新建项目)。
- 从屏幕左侧的树视图中选择 Visual Basic Project(Visual Basic 项目)。
- 选择 Class Library(类库)作为项目模板。
- 将应用程序名设置为 PhysServer2,然后单击 OK(确定),以创建项目。
- 突出显示 Solution Explorer(解决方案资源管理器)窗口中名为 Class1.vb 的类,并将其重命名为 NETTemperature.vb。
- 在 NETTemperature.vbt 中选择 Class1 的代码(这应是一个空的类定义),并用以下代码替换它:
Public Interface iTemperature Property Celsius() As Double Property Fahrenheit() As Double Function GetCelsius() As Double Function GetFahrenheit() As Double End Interface Public Class NET_Temperature Implements iTemperature Private mdblCelsius As Double Private mdblFahrenheit As Double Public Property Celsius() As Double _ Implements iTemperature.Celsius Get Celsius = mdblCelsius End Get Set(ByVal Value As Double) mdblCelsius = Value mdblFahrenheit = ((Value * 9) / 5) + 32 End Set End Property Public Property Fahrenheit() As Double _ Implements iTemperature.Fahrenheit Get Fahrenheit = mdblFahrenheit End Get Set(ByVal Value As Double) mdblFahrenheit = Value mdblCelsius = ((Value - 32) * 5) / 9 End Set End Property Public Function GetCelsius() As Double _ Implements iTemperature.GetCelsius GetCelsius = mdblCelsius End Function Public Function GetFahrenheit() As Double _ Implements iTemperature.GetFahrenheit GetFahrenheit = mdblFahrenheit End Function End Class
此代码从定义一个名为 iTemperature 的接口开始。由于此接口是用 Public 关键字定义的,所以将被导出到从此程序集创建的类型库中。可以将接口定义看作整个或部分类定义的基本结构。与类相同的是,接口定义也可以包含很多成员,如属性、方法(功能或子功能)和事件;而与类不同的是,接口定义不包含上述任何成员的代码。一个类可以实现一个(如本例)或多个接口。
此代码定义了使用 iTemperature 接口的 NET_Temperature 类。类定义中的下面这一行,设置了类与接口之间的一个合同:
Implements iTemperature
合同声明类将实现接口的所有成员。也可以包含不属于接口的其他成员,但是如果您试图建立一个不完全实现接口的类,将出现错误。
请注意用于将类中的成员与类所实现的接口的成员相关联的语法。例如,NET_Temperature 类中的 Celsius 属性是通过以下方法定义的:
Public Property Celsius() As Double _ Implements iTemperature.Celsius
该代码行定义一个将返回 Double 的属性,并告知编译器此属性为 iTemperature 接口中的 Celsius 属性的实现。
创建密钥对并对程序集签名
要使程序集在全局范围内可用,需要创建密钥对并使用它对程序集签名。另外,通过添加标题和说明,可以更加轻松地使用程序集。
要对程序集签名,可以运行 sn 实用程序并手动添加密钥文件的名称,也可以使用 Visual Studio .NET 用户界面生成强名称。我们使用后一种方法。要完成此操作,请执行以下步骤:
- 在 Visual Studio .NET 的 Solution Explorer(解决方案资源管理器)中,双击 AssemblyInfo.vb 文件,将它在编辑窗口中打开。
- 在文件顶部的 Assembly Attributes(程序集属性)部分,将 AssemblyTitle 和 AssemblyDescription 行修改为:
<Assembly: AssemblyTitle("PhysServer2")> <Assembly: AssemblyDescription(".NET Version of PhysServer")>
提示:Visual Basic .NET 中的 Assembly Attributes(程序集属性)相当于 Visual Basic 6.0 中的 Project Properties(项目属性)。
- 在 Solution Explorer(解决方案资源管理器)中,用右键单击项目节点,然后选择 Properties(属性)。单击 Common Properties(公共属性)文件夹,然后单击 Strong Name(强名称)属性页。选择标为 Generate Strong Name Using(使用下列选项生成强名称)的方框。单击 Generate Key(生成密钥)创建一个密钥文件,并将其添加到项目中。单击 OK(确定)关闭属性对话框。
现在,可以开始创建程序集了。单击 Build(生成),或按 Ctrl+Shift+B 组合键,以生成程序集。
注册该程序集并创建一个类型库
此时,您可以从另一个 .NET 应用程序使用这个新建的程序集和 NET_Temperature 类。但是,仍须使类及其成员对 COM 应用程序可用。打开 Visual Studio .NET 命令提示符窗口(依次单击“开始”>“程序”>“Microsoft Visual Studio .NET 7.0”>“Visual Studio .NET Tools”>“Visual Studio .NET Command Prompt”),变换到 PhysServer2 的项目目录,并键入:
regasm /tlb:PhysServer2.tlb PhysServer2.dll
regasm 实用程序将创建一个类型库并在 Windows 注册表中对其进行注册,以使 PhysServer2.dll 中的类对 COM 客户端可用 。
将程序集添加到全局程序集缓存中
最后,为使新注册的程序集在全局范围内对所有 COM 客户端(无论位于硬盘驱动器上的什么位置)可用,请切换回 Visual Studio .NET 命令提示符并键入:
gacutil /I PhysServer2.dll
gacutil 实用程序将程序集添加到 GAC 中,并显示状态消息,通知您操作已完成。
编写 Visual Basic 6.0 代码以调用 .NET 类
现在可以编写 COM 客户端,以使用 NET_Temperature 类。请执行以下步骤:
- 打开 Visual Basic 6.0,在 New Project(新建项目)对话框中,单击 New(新建)选项卡。
- 选择 Standard EXE(标准 EXE),然后单击 Open(打开)。
- 在 Project Explorer(项目资源管理器)窗口中,突出显示名为 Form1 的窗体,将其重命名为 frmTemperature。
- 通过添加相应的控件并按表 1 设置这些控件的属性,创建如图 2 所示的窗体:
表 1:frmTemperature 的控件
控件类型 属性 值 Label Name lblFahrenheit Caption 华氏度 TextBox Name txtFahrenheit Text (空) Button Name cmdConvertToC Caption 转换为摄氏度 Label Name lblCelsius Caption 摄氏度 TextBox Name txtCelsius Text (空) Button Name cmdConvertToF Caption 转换为华氏度 图 2:测试窗体设计
- 要通过 CCW 使用 PhysServer2 中的类,请单击 Project(项目),然后单击 References(引用)打开 References(引用)对话框。选择对 .NET 版的 PhysServer 的引用,如图 3 所示。单击 OK(确定)关闭对话框。
图 3:设置对 .NET 组件的引用
现在,可以编写使用 NET_Temperature 类的方法和属性的代码了。在 View(视图)菜单中,单击 Code(代码),然后在 frmTemperature 的窗体模块中输入以下代码:
Private moTempClass As PhysServer2.NET_Temperature Private moTemp As PhysServer2.iTemperature Private Sub cmdConvertToC_Click() With moTemp .Fahrenheit = txtFahrenheit.Text txtCelsius.Text = .GetCelsius End With End Sub Private Sub cmdConvertToF_Click() With moTemp .Celsius = txtCelsius.Text txtFahrenheit.Text = .GetFahrenheit End With End Sub Private Sub Form_Load() Set moTempClass = New PhysServer2.NET_Temperature Set moTemp = moTempClass End Sub
请记住,在 .NET 项目中,您使用了 iTemperature 接口定义 Net_Temperature 类。此代码演示了如何从对象检索回接口(即名为 moTemp 的对象)。虽然这段代码看起来好像多余,但是如果在 Visual Basic 中执行操作,您会发现使用接口比使用对象要方便得多。这是因为接口支持 Microsoft® IntelliSense® 的命令完成方式。
试验
要查看 NET_Temperature 类的运行情况,请执行以下步骤:
- 按 F5 键启动该项目。
- 在“华氏度”文本框中输入 95,然后单击“转换为摄氏度”。“摄氏度”文本框中将会填入值 35。
- 在“摄氏度”文本框中输入 -14,然后单击“转换为华氏度”。“华氏度”文本框中将会填入值 6.8。
- 关闭窗体停止该项目。
Visual Basic 6.0 之后的新增功能
当然,从 Visual Basic 6.0 调用 .NET 组件的整个过程都是新增的,因为在发布 Visual Basic 6.0 时,.NET 还没有出现。Visual Basic 6.0 的确具有创建由 COM 调用连接的多个组件的能力,并且 .NET CCW 使 .NET 调用的运行与 COM 调用类似。整个过程的好处就在于,您无需担心通过 COM 代码使用 .NET 组件的安全性和稳定性。通过 CLR 进行调用,使得无论在哪里使用 .NET 组件,CCW 都使其具有管理代码的所有好处。
小结
尽管 .NET 是一个全新的开发环境,但是设计者并没有忽略与现有代码的兼容问题。通过正确构建 .NET 组件,并使用 sn、tlbexp、regasm 和 gacutil 等工具,可以使 .NET 程序集中的类对 COM 客户端公开。
从 COM 组件调用 .NET 组件不是一件轻而易举的事情,正如本文所述,需要对 .NET 组件的代码进行明确的修改,才能启用此方案。但是,修改只是小问题,而且可以肯定的是,使 COM 客户端可以调用 .NET 服务器会带来很多好处。如果您尝试一次将一个复杂的应用程序从 COM 迁移到 .NET,您会发现本文介绍的技术至关重要。