互操作性疑难解答 (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 在导出字符串时包含终止字符。 解决此不兼容性的最佳方法是将缺少终止字符的字符串导出为 Byte
或 Char
的数组。
导出继承层次结构
作为 COM 对象公开时,托管类层次结构会平展。 例如,如果定义一个包含成员的基类,然后在作为 COM 对象公开的派生类中继承该基类,则使用 COM 对象中的派生类的客户端无法使用继承的成员。 基类成员只能作为基类的实例从 COM 对象进行访问,因而仅当基类也创建为 COM 对象时才能进行访问。
重载方法
尽管可以使用 Visual Basic 创建重载方法,但 COM 不支持这些方法。 当包含重载方法的类作为 COM 对象公开时,会为重载方法生成新方法名称。
例如,考虑一个具有 Synch
方法的两个重载的类。 当该类作为 COM 对象公开时,新生成的方法名称可以是 Synch
和 Synch_2
。
重命名可能会导致 COM 对象的使用者面临两个问题。
客户端可能不需要生成的方法名称。
将新重载添加到类或其基类时,作为 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 类的实例,然后调用对象的方法。 此规则的一个例外涉及包含 AppObj
或 GlobalMultiUse
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 Error
或 Try...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。
未共享的程序集应与调用应用程序并排位于目录中。