通过COM接口将.NET DataTable属性公开给VBA

时间:2013-03-12 15:16:14

标签: vb.net vba excel-vba com interop

我试图创建一个.Net DLL,基本上作为数据库连接的抽象层;它将取代我们用VB6编写的当前DLL,我试图尽可能地匹配当前的功能。

无论如何,我遇到的基本问题是我无法找到一种方法来获取像DataColumnCollection或DataColumn这样的.Net类来显示在VBA解释器中 - 例如,可能会说“Column”的类型“MarshalByValueComponent”,但该值将为“No Variables”。

如果我完全重新创建两个类(即Fields作为字段的集合,它继承自DataColumn,然后为两者定义接口),我可以使它工作,但这似乎是很多额外的开销什么(应该是?)一个非常简单的想法。我觉得我只是因为marshaller处理DataColumn类的方式而错过了一些非常简单的东西。

我在网上找到的很多东西都是关于如何将DataTable或DataReader转换为传统的ADODB Recordset,但这也会增加很多开销...我宁愿把它留作DataTable并创建允许VBA与之交互的COM接口;那样,例如,如果他们想把表写入excel表,我就不会重复工作(转换为ADODB记录集,然后读/写excel表。你需要迭代整个表两次。 ..)

对于书本长度的解释感到抱歉 - 我觉得这个问题需要一些澄清,因为根本原因是试图匹配遗留功能。以下是我当前界面不起作用的示例:

Public Interface IDataTable
    ReadOnly Property Column As DataColumn
End Interface

<ClassInterface(ClassInterfaceType.None)> _
<System.ComponentModel.DesignerCategory("")> _
<ComDefaultInterface(GetType(Recordset.IDataTable))> _
<Guid("E7AFBBB6-CB20-44EC-9CD2-BC70B94CD8B7")> _
Public Class Recordset : Inherits Data.DataTable : Implements IDataTable

Public ReadOnly Property Column As DataColumn Implements IDataTable.Column
    Get
        Return MyBase.Columns(0)
    End Get
End Property

注意:我最初尝试将属性Columns as DataColumnCollection返回MyBase.Columns。这是一个Object,而不是MarshalByValueComponent,但也是空的。我知道MyBase.Column(0)有一个值,因为我可以将Msgbox(MyBase.Columns(0).ColumnName)放在get的返回上方,它弹出正常(不要判断;这比使用调试器)...

我不介意只定义它们,但是我不能继承DataColumnCollection,并且COM接口在处理泛型时已经很糟糕了。如果没有重新发明轮子,还有其他方法吗?

感谢您的帮助!

2 个答案:

答案 0 :(得分:3)

我在过去的3个星期里做了一些非常相似的事情。

我最终制作了两个.NET程序集:

  1. 与数据存储区通信的纯.NET程序集(供.NET应用程序使用)。
  2. “COM Interop”程序集,它包装第一个程序集并添加COM开销(ADODB引用和COM-Visible接口)。
  3. 我使用VSTO“AddIn.Object”属性从Excel VBA调用第二个程序集。

    如上所述,我最终将System.Data.DataTables转换为ADODB.Recordsets。让.NET和VBA谈论除了原始类型以外的任何东西对我来说都是令人沮丧的。实际上,我最终将一些对象序列化为JSON,这样两个世界就可以进行通信。

    看起来确实很疯狂,但我重新发明了轮子。


    • 我按照这篇MSDN文章,让我的.NET代码可由VBA调用。
    • 我使用这篇Code Project文章(我相信你已经看到)转换为Recordset *。
    • 我让框架处理字符串,整数等。
    • 对于所有其他数据类型,我使用Json.Net和自定义VBA类来执行JSON序列化。

      *将文章转换为VB.Net并添加了一些额外的错误处理。

答案 1 :(得分:1)

好吧,这可能不是最优雅(或完整,此时)的解决方案;但我认为这是我要去的路线。

我没有将整个事物转换为ADODB Recordset(并重复任何迭代),而是完全抛弃了DataTable类,并将自己的Recordset类编写为通用数据读取器的COM包装器(通过IDataReader接口)并添加了一个新的Field类来管理类型转换,并将Fields设置为Field数组(因为interop讨厌泛型)

它基本上创建了一个仅向前的ADODB Recordset(相同的限制),但它的好处是一次只能加载一行,所以大部分数据都可以作为托管代码处理,直到你知道他们想要做什么为止。它(我将为使用读者的ToArray,ToAccessDB,ToFile等添加方法),同时仍然允许从excel / access / vbscript / vb6迭代Recordset(如果这真的是他们想要做的那样)。 。无论如何,主要需要传统支持) 这是一个例子,万一其他人必须再次这样做;为简洁起见略有修改:

Public Interface IRecordset
    ReadOnly Property CursorPosition As Integer
    ReadOnly Property FieldCount As Integer
    ReadOnly Property Fields As Field()

    Function ReadNext() As Boolean
    Sub Close()
End Interface

 <System.ComponentModel.DesignerCategory("")> _
 <ClassInterface(ClassInterfaceType.None)> _
 <ComDefaultInterface(GetType(IRecordset))> _
 <Guid("E7AFBBB6-CB20-44EC-9CD2-BC70B94CD8B7")> _
 Public Class Recordset : Implements IRecordset : Implements IDisposable
    Private _Reader = Nothing
    Private _FieldCount As Integer = Nothing
    Private _Fields() As Field
    Public ReadOnly Property CursorPosition As Integer Implements IRecordset.CursorPosition...
    Public ReadOnly Property FieldCount As Integer Implements IRecordset.FieldCount...
    Public ReadOnly Property Fields As Field() Implements IRecordset.Fields...

    Friend Sub Load(ByVal reader As IDataReader)
        _Reader = reader
        _FieldCount = _Reader.FieldCount
        _Fields = Array.CreateInstance(GetType(DataColumn), _FieldCount)
        For i = 0 To _FieldCount - 1
            _Fields(i) = New Field(i, Me)
        Next
    End Sub

    'This logic kinda sucks and is dumb.
    Public Function ReadNext() As Boolean Implements IRecordset.ReadNext
        _EOF = Not _Reader.Read()
        If _EOF Then Return False
        _CursorPosition += 1
        For i = 0 To _FieldCount - 1
            _Fields(i)._Value = _Reader.GetValue(i).ToString
        Next
        Return True
    End Function

从这里你只需要定义一些类型如Field或Column,并为该类型添加一个interop包装器:

Public Interface IField
    ReadOnly Property Name As String
    ReadOnly Property Type As String
    ReadOnly Property Value As Object
End Interface

<System.ComponentModel.DesignerCategory("")> _
<ClassInterface(ClassInterfaceType.None)> _
<Guid("6230C670-ED0A-48D2-9429-84820DC2BE6C")> _
<ComDefaultInterface(GetType(IField))> _
Public Class Field : Implements IField
    Private Reader As IDataReader = Nothing
    Private Index As Integer = Nothing
    Public ReadOnly Property Name As String Implements IField.Name
        Get
            Return Reader.GetName(Index)
        End Get
    End Property
    Public ReadOnly Property Value As Object Implements IField.Value
        Get
            Return Reader.GetValue(Index)
        End Get
    End Property
    Public ReadOnly Property Type As String Implements IField.Type
        Get
            Return Reader.GetDataTypeName(Index).ToString
        End Get
    End Property
    Sub New(ByVal i As Integer, ByRef r As IDataReader)
        Reader = r
        Index = i
    End Sub
End Class

所有这些都很愚蠢,但似乎运作良好。

注意:我现在只使用.Net大约4天了,所以这可能很糟糕,请随时评论我可能做的任何非常愚蠢的事情。