互操作性疑难解答 (Visual Basic)

在 COM 和 .NET Framework 的托管代码之间交互操作时,可能会遇到下面的一个或多个常见问题。

互操作封送处理

有时,您可能需要使用 .NET Framework 中没有的数据类型。 互操作程序集处理 COM 对象的大部分工作,但在向 COM 公开托管对象时,可能需要您控制所用的数据类型。 例如,在传递给由 Visual Basic 6.0 版本或更早版本创建的 COM 对象的字符串中,类库中的结构必须指定 BStr 非托管类型。 此时,可以用 MarshalAsAttribute 特性使托管类型作为非托管类型公开。

向非托管代码导出固定长度的字符串

在 Visual Basic 6.0 版本或更早版本中,字符串是作为不带 null 终止字符的字节序列导出到 COM 对象的。 为了与其他语言兼容,Visual Basic 2005 在导出字符串时包含了一个终止字符。 解决这种不兼容问题的最佳方法是将缺少终止字符的字符串作为 Byte 或 Char 数组导出。

导出继承层次结构

当作为 COM 对象公开时,托管类结构将变成平面结构。 例如,如果定义了一个具有成员的基类,然后在作为 COM 对象公开的派生类中继承该基类,则在 COM 对象中使用该派生类的客户端将不能使用继承成员。 基类成员只可以从作为基类实例的 COM 对象进行访问(同时要求该基类也是作为 COM 对象创建的)。

重载方法

尽管可以使用 Visual Basic 创建重载方法,但 COM 不支持这些重载方法。 当包含重载方法的类公开为 COM 对象时,系统将为这些重载方法生成新的方法名。

例如,请考虑包含两个 Synch 重载方法的类。 当该类公开为 COM 对象时,新生成的方法名可能为 Synch 和 Synch_2。

重命名可能会给该 COM 对象的使用者带来两个问题。

  1. 客户端可能预见不到生成的方法名。

  2. 在公开为 COM 对象的类中,当向该类或其基类添加新的重载时,该类中生成的方法名可能会发生变化。 这可能导致版本控制问题。

若要解决这两个问题,可在开发将公开为 COM 对象的对象时,给每个方法命名一个唯一名称,而不要使用重载。

通过 Interop 程序集使用 COM 对象

使用互操作程序集时,它们几乎就像是其所表示的 COM 对象的托管代码替换。 然而,因为它们是包装类,并非实际的 COM 对象,所以使用 Interop 程序集与使用标准程序集有些不同。 这些不同之处包括类的公开以及参数和返回值的数据类型。

同时作为接口和类公开的类

不同于标准程序集中的类,在 Interop 程序集中 COM 类同时作为一个接口和一个表示 COM 类的类进行公开。 接口名称与 COM 类的名称完全相同。 Interop 类的名称与原始 COM 类的名称相同,但要附加“Class”一词。 例如,假设有一个具有对 COM 对象的 Interop 程序集的引用的项目。 若 COM 类被命名为 MyComClass,Intellisense 和对象浏览器就会显示一个名为 MyComClass 的接口和一个名为 MyComClassClass 的类。

创建 .NET Framework 类的实例

通常,我们将 New 语句和类名一起使用,以此创建 .NET Framework 类的实例。 让互操作程序集来呈现 COM 类便是将 New 语句用于接口的一个例子。 除非在 Inherits 语句中使用了 COM 类,否则就可以像使用类一样地使用接口。 下面的代码演示如何在具有对 Microsoft ActiveX Data Objects 2.8 Library COM 对象的引用的项目中创建 Command 对象:

Dim cmd As New ADODB.Command

然而,如果正在使用 COM 类作为派生类的基,则必须使用表示 COM 类的 Interop 类,就像下面的代码一样:

Class DerivedCommand
    Inherits ADODB.CommandClass
End Class

提示

互操作程序集隐式实现表示 COM 类的接口。 不应尝试使用 Implements 语句来实现这些接口,否则将导致错误。

参数和返回值的数据类型

与标准程序集的成员不同,互操作程序集的成员具有的数据类型可以与原始对象声明中所用的不同。 尽管互操作程序集将 COM 类型隐式转换为兼容的公共语言运行时类型,但仍应注意双方所用的数据类型以防出现运行时错误。 例如,在用 Visual Basic 6.0 及更早版本创建的 COM 对象中,Integer 类型的值与 .NET Framework 的类型 Short 等价。 建议您用对象浏览器检查导入成员的特性后再使用它们。

模块级 COM 方法

使用大多数 COM 对象时,都是先用 New 关键字创建 COM 类的一个实例,然后再调用该对象的方法。 但这一规则对于包含 AppObj 或 GlobalMultiUse COM 类的 COM 对象是个例外。 这些类与 Visual Basic 2005 类中的模块级方法相似。 第一次调用这些对象的方法时,Visual Basic 6.0 及更早版本会隐式创建这些对象的实例。 例如,在 Visual Basic 6.0 中,可以添加对 Microsoft DAO 3.6 版对象库的引用,然后直接调用 DBEngine 方法,而无需首先创建一个实例:

Dim db As DAO.Database
' Open the database.
Set db = DBEngine.OpenDatabase("C:\nwind.mdb")
' Use the database object.

Visual Basic 2005 要求必须始终先创建 COM 对象的实例,然后才能使用其方法。 若要在 Visual Basic 2005 中使用这些方法,应声明一个所需类的变量,并使用 New 关键字将对象赋给该对象变量。 如果要确保只创建该类的一个实例,可以使用 Shared 关键字。

' Class level variable.
Shared DBEngine As New DAO.DBEngine

Sub DAOOpenRecordset()
    Dim db As DAO.Database
    Dim rst As DAO.Recordset
    Dim fld As DAO.Field
    ' Open the database.
    db = DBEngine.OpenDatabase("C:\nwind.mdb")

    ' Open the Recordset.
    rst = db.OpenRecordset(
        "SELECT * FROM Customers WHERE Region = 'WA'",
        DAO.RecordsetTypeEnum.dbOpenForwardOnly,
        DAO.RecordsetOptionEnum.dbReadOnly)
    ' Print the values for the fields in the debug window.
    For Each fld In rst.Fields
        Debug.WriteLine(fld.Value.ToString & ";")
    Next
    Debug.WriteLine("")
    ' Close the Recordset.
    rst.Close()
End Sub

事件处理程序中未处理的错误

一个常见的互操作问题涉及处理 COM 对象所引发的事件的事件处理程序中的错误。 除非使用 On Error 或 Try...Catch...Finally 语句进行明确检查,否则将忽略这些错误。 例如,下面的示例摘自一个 Visual Basic 2005 项目,此项目具有对 Microsoft ActiveX Data Objects 2.8 Library COM 对象的引用。

' To use this example, add a reference to the 
'     Microsoft ActiveX Data Objects 2.8 Library  
' from the COM tab of the project references page.
Dim WithEvents cn As New ADODB.Connection
Sub ADODBConnect()
    cn.ConnectionString =
    "Provider=Microsoft.Jet.OLEDB.4.0;" &
    "Data Source=C:\NWIND.MDB"
    cn.Open()
    MsgBox(cn.ConnectionString)
End Sub

Private Sub Form1_Load(ByVal sender As System.Object,
    ByVal e As System.EventArgs) Handles MyBase.Load

    ADODBConnect()
End Sub

Private Sub cn_ConnectComplete(
    ByVal pError As ADODB.Error,
    ByRef adStatus As ADODB.EventStatusEnum,
    ByVal pConnection As ADODB.Connection) Handles cn.ConnectComplete

    '  This is the event handler for the cn_ConnectComplete event raised 
    '  by the ADODB.Connection object when a database is opened.
    Dim x As Integer = 6
    Dim y As Integer = 0
    Try
        x = CInt(x / y) ' Attempt to divide by zero.
        ' This procedure would fail silently without exception handling.
    Catch ex As Exception
        MsgBox("There was an error: " & ex.Message)
    End Try
End Sub

此示例必然会引发一个错误。 但如果在没有 Try...Catch...Finally 块的情况下尝试执行相同的示例,该错误将被忽略,就像使用了 OnError Resume Next 语句一样。 如果没有错误处理机制,被零除将导致失败,并且没有任何提示。 由于这种错误从不引发未经处理的异常错误,因此在处理 COM 对象所引发的事件的事件处理程序中使用某种形式的异常处理机制非常重要。

理解 COM 互操作错误

如果没有错误处理机制,互操作调用产生错误时提供的信息通常很少。 应尽量使用结构化错误处理机制在发生问题时提供更多相关信息。 这在调试应用程序时特别有用。 例如:

Try
    ' Place call to COM object here.
Catch ex As Exception
    ' Display information about the failed call.
End Try

您可以通过检查异常对象的内容找到很多信息,例如,错误说明、HRESULT,以及导致 COM 错误的原因,等等。

ActiveX 控件问题

大多数可在 Visual Basic 6.0 中使用的 ActiveX 控件也可在 Visual Basic 2005 中正常使用。 例外主要在于容器控件,也就是实际上包含其他控件的控件。 在 Visual Studio 中无法正常使用的旧控件的示例如下:

  • Microsoft Forms 2.0 Frame 控件

  • Up-Down 控件,也称作数值调节控件

  • Sheridan Tab 控件

对于不受支持的 ActiveX 控件问题,只有很少几种解决方法。 如果您拥有原始的源代码,则可以将现有控件迁移到 Visual Studio。 否则,您可以向软件供应商咨询,询问是否有更新的 .NET 兼容控件,以替换不受支持的 ActiveX 控件。

按址传递控件的只读属性

将某些旧 ActiveX 控件的 ReadOnly 属性作为 ByRef 参数传递到其他过程时,Visual Basic 2005 有时会引发 COM 错误,例如,“Error 0x800A017F CTL_E_SETNOTSUPPORTED”。 而在 Visual Basic 6.0 中,类似的过程调用不会引发错误,而且处理那些参数时就像按值传递一样。 您在 Visual Basic 2005 中看到的错误消息是 COM 对象的报告,指出您尝试更改没有属性 Set 过程的属性。

如果您能够访问被调用的过程,可以通过使用 ByVal 关键字声明接受 ReadOnly 属性的参数来避免此错误。 例如:

Sub ProcessParams(ByVal c As Object)
    'Use the arguments here.
End Sub

如果您不能访问被调用的过程的源代码,可以通过在调用过程中参数的两边添加额外的括号来强制按值传递属性。 例如,在具有对 Microsoft ActiveX Data Objects 2.8 Library COM 对象的引用的项目中,可以使用:

Sub PassByVal(ByVal pError As ADODB.Error)
    ' The extra set of parentheses around the arguments
    ' forces them to be passed by value.
    ProcessParams((pError.Description))
End Sub

部署公开 Interop 的程序集

部署公开 COM 接口的程序集带来了一些特殊的难题。 例如,当不同的应用程序引用同一 COM 程序集时将产生潜在的问题。 当安装一个新版本的程序集而另一应用程序仍在使用旧版本的程序集时,这种情况很常见。 如果卸载共享某个 DLL 的程序集,则可能会无意中使其对于其他程序集不可用。

为避免此问题,应将共享的程序集安装到全局程序集缓存 (GAC) 中,并使用组件的 MergeModule。 如果无法在全局程序集缓存中安装应用程序,应将其安装到特定于版本的子目录中的 CommonFilesFolder 中。

未共享的程序集应与调用应用程序一起放在该目录下。

请参见

任务

演练:用 COM 对象实现继承 (Visual Basic)

如何:向部署项目中添加合并模块

参考

MarshalAsAttribute

Tlbimp.exe(类型库导入程序)

Tlbexp.exe(类型库导出程序)

Inherits 语句

概念

全局程序集缓存

其他资源

COM 互操作 (Visual Basic)