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

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

互操作封送处理

有时,你可能必须使用不属于 .NET Framework 一部分的数据类型。 互操作程序集会为 COM 对象处理大部分工作,但是你可能必须控制向 COM 公开托管对象时所使用的数据类型。 例如,类库中的结构必须对发送到由 Visual Basic 6.0 及更早版本创建的 COM 对象的字符串指定 BStr 非托管类型。 在这种情况下,可以使用 MarshalAsAttribute 属性使托管类型作为非托管类型进行公开。

将定长字符串导出到非托管代码

在 Visual Basic 6.0 及更早版本中,字符串会以不带 null 终止字符的字节序列形式导出到 COM 对象。 为了与其他语言兼容,Visual Basic .NET 在导出字符串时包含终止字符。 解决此不兼容性的最佳方法是将缺少终止字符的字符串导出为 ByteChar 的数组。

导出继承层次结构

作为 COM 对象公开时,托管类层次结构会平展。 例如,如果定义一个包含成员的基类,然后在作为 COM 对象公开的派生类中继承该基类,则使用 COM 对象中的派生类的客户端无法使用继承的成员。 基类成员只能作为基类的实例从 COM 对象进行访问,因而仅当基类也创建为 COM 对象时才能进行访问。

重载方法

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

例如,考虑一个具有 Synch 方法的两个重载的类。 当该类作为 COM 对象公开时,新生成的方法名称可以是 SynchSynch_2

重命名可能会导致 COM 对象的使用者面临两个问题。

  1. 客户端可能不需要生成的方法名称。

  2. 将新重载添加到类或其基类时,作为 COM 对象公开的类中生成的方法名称可能会更改。 这可能会导致版本控制问题。

若要解决这两个问题,请在开发将作为 COM 对象公开的对象时,为每个方法提供唯一名称(而不是使用重载)。

通过互操作程序集使用 COM 对象

使用互操作程序集几乎如同它们是其所表示的 COM 对象的托管代码替换一样。 但是,由于它们是包装器而不是实际 COM 对象,因此使用互操作程序集与标准程序集之间存在一些差异。 这些差异领域包括类的公开以及参数和返回值的数据类型。

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

与标准程序集中的类不同,COM 类在互操作程序集中同时作为接口和类(表示 COM 类)公开。 接口的名称与 COM 类的名称相同。 互操作类的名称与原始 COM 类的名称相同,但追加了“Class”一词。 例如,假设你有一个项目,该项目引用了 COM 对象的互操作程序集。 如果 COM 类名为 MyComClass,则 IntelliSense 和对象浏览器会显示名为 MyComClass 的接口和名为 MyComClassClass 的类。

创建 .NET Framework 类的实例

一般而言,会使用 New 语句及类名创建 .NET Framework 类的实例。 通过互操作程序集表示 COM 类是可以将 New 语句与接口一起使用的一种情况。 除非将 COM 类与Inherits 语句一起使用,否则可以如同使用类一样来使用接口。 以下代码演示如何在项目中创建一个 Command 对象,该对象引用 Microsoft ActiveX 数据对象 2.8 库 COM 对象:

Dim cmd As New ADODB.Command

但是,如果使用 COM 类作为派生类的基类,则必须使用表示 COM 类的互操作类,如以下代码所示:

Class DerivedCommand
    Inherits ADODB.CommandClass
End Class

注意

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

参数和返回值的数据类型

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

模块级别 COM 方法

大多数 COM 对象的使用方式是使用 New 关键字创建 COM 类的实例,然后调用对象的方法。 此规则的一个例外涉及包含 AppObjGlobalMultiUse COM 类的 COM 对象。 这种类与 Visual Basic .NET 类中的方法类似。 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 .NET 要求始终先创建 COM 对象的实例,然后才能使用其方法。 若要在 Visual Basic 中使用这些方法,请声明所需类的变量,并使用 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 ErrorTry...Catch...Finally 语句专门检查错误,否则会忽略此类错误。 例如,以下示例来自一个 Visual Basic .NET 项目,该项目引用了 Microsoft ActiveX 数据对象 2.8 库 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 = "..."
    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 .NET,不会出现问题。 主要例外是容器控件,或是以可视方式包含其他控件的控件。 无法正确适用于 Visual Studio 的较旧控件的一些示例如下所示:

  • Microsoft Forms 2.0 Frame 控件

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

  • Sheridan Tab 控件

对于不支持的 ActiveX 控件问题,只有几种解决方法。 如果拥有原始源代码,则可以将现有控件迁移到 Visual Studio。 否则,可以与软件供应商协商,获取与 .NET 兼容的更新控件版本,以替换不支持的 ActiveX 控件。

对控件的 ReadOnly 属性进行 ByRef 传递

将某些较旧 ActiveX 控件的 ReadOnly 属性作为 ByRef 参数传递给其他过程时,Visual Basic .NET 有时会引发 COM 错误,如“错误 0x800A017F CTL_E_SETNOTSUPPORTED”。 来自 Visual Basic 6.0 的类似过程调用不会引发错误,参数会被视为如同按值传递一样。 Visual Basic .NET 错误消息指示你在尝试更改没有属性 Set 过程的属性。

如果你有权访问所调用的过程,则可以通过使用 ByVal 关键字声明接受 ReadOnly 属性的参数来防止此错误。 例如:

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

如果无权访问所调用过程的源代码,则可以通过在调用过程周围添加一组额外的括号来强制按值传递属性。 例如,在引用 Microsoft ActiveX 数据对象 2.8 库 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

部署公开互操作的程序集

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

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

未共享的程序集应与调用应用程序并排位于目录中。

另请参阅