互操作性疑难解答 (Visual Basic)
在 COM 与 .NET Framework 的托管代码之间互操作时,可能会遇到以下一个或多个常见问题。
互操作封送处理
有时,可能必须使用不属于 .NET Framework 的数据类型。 互操作程序集处理 COM 对象的大部分工作,但可能需要控制在向 COM 公开托管对象时使用的数据类型。 例如,在类库中的结构中,当将字符串发送至由 Visual Basic 6.0 及更早版本创建的 COM 对象时,必须指定 BStr
非托管类型。 在这种情况下,可以使用 MarshalAsAttribute 属性使托管类型作为非托管类型进行公开。
将定长字符串导出到非托管代码
在 Visual Basic 6.0 和更早版本中,字符串作为字节序列导出到 COM 对象,而不使用 null 终止字符。 为了与其他语言兼容,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
语句一起使用,否则可以如同使用类一样来使用接口。 以下代码演示如何在引用 Microsoft ActiveX 数据对象 2.8 Library COM 对象的项目中创建 Command
对象:
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 中使用这些方法,请声明所需类的变量,并使用新关键字将对象分配给对象变量。 如果要确保只创建一个类实例,则可以使用 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 框架控件
Up-Down 控件,也称为旋转控件
Sheridan Tab 控件
对于不支持的 ActiveX 控件问题,只有一些解决方法。 如果拥有原始源代码,则可以将现有控件迁移到 Visual Studio。 否则,您可以与软件供应商核实是否有更新的 .NET 兼容控件版本,以替换不支持的 ActiveX 控件。
传递控件 ByRef 的 ReadOnly 属性
Visual Basic 有时会引发 COM 错误,例如“错误0x800A017F CTL_E_SETNOTSUPPORTED”,当你将某些较旧的 ActiveX 控件的 ReadOnly
属性作为 ByRef
参数传递给其他过程时。 来自 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。
未共享的程序集应与调用应用程序并排位于目录中。