Tell me more ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

I am trying to create a .Net DLL basically as an abstraction layer for database connections; it is going to replace a current DLL we have that is written in VB6 and I am trying to match the current functionality as much as possible.

Anyway, the essential issue I am having is that I can't find a way to get .Net classes like DataColumnCollection or DataColumn to display in the VBA Interpreter -- It may say, for example, "Column" with the type "MarshalByValueComponent," but the value will be "No Variables".

I can get it to work if I completely re-create both classes (i.e. Fields as a collection of field, which inherits from DataColumn, and then define an interface for both), but that seems like a lot of added overhead for what (should be?) a pretty simple idea. I feel like I am just missing something very simple with the way the marshaller is handling the DataColumn class.

A lot of the stuff I am finding online is on how to convert a DataTable or DataReader to a legacy ADODB Recordset, but that also would add a lot of overhead... I'd rather leave it as a DataTable and create a COM interface to allow VBA to interact with it; that way if, for example, they want to write the table to an excel sheet, I wouldn't be duplicating work (convert to ADODB recordset, then read/write to excel sheet. You'd need to iterate the entire table twice...)

Sorry for the book-length explanation -- I felt the problem needed a bit of clarification since the root-cause is trying to match legacy functionality. Here is an example of my current interface that does not work:

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

Note: I originally tried the property Columns as DataColumnCollection which returned MyBase.Columns. That came through as an Object, instead of MarshalByValueComponent, but was also empty. I know MyBase.Column(0) has a value, because I can put Msgbox(MyBase.Columns(0).ColumnName) right above the return in the get and it pops up fine (don't judge; this is way easier than using a debugger for this)...

I wouldn't mind just defining them both, but I can't inherit DataColumnCollection and the COM interface already sucks at dealing with generics. Is there any other way around this without re-inventing the wheel?

Thanks for your help!

share|improve this question
Have you considered using the .NET DAO 3.6 Object Library? – ron tornambe Mar 12 at 15:31
Do you mean to use a DAO Recordset instead of a DataTable, or is there some way to use a DAO-class to expose members of the DataTable? I am not sure I see how to convert DataTable.Columns to DAO.Fields other than to iterate through them... – Karter Mar 12 at 15:44
I guess I am just not clear on what the current DLL does in terms of DB interface. Is the VBA application Excel or Access? – ron tornambe Mar 12 at 16:01
It's used in Excel, Access, and VBScript (that's a whole other issue..); but I can use the DAO object model in any/all of those if it is easy to load a data reader/data set/data table to DAO? – Karter Mar 12 at 16:10

2 Answers

up vote 3 down vote accepted

I just spent the last 3 weeks doing something eerily similar.

I ended up making two .NET assemblies:

  1. A pure .NET assembly that talks to the datastore (for use by .NET apps).
  2. A "COM Interop" assembly that wraps the first assembly and adds the COM overhead (ADODB references and COM-Visible interfaces).

I call the second assembly from Excel VBA using the VSTO "AddIn.Object" property.

I ended up converting System.Data.DataTables to ADODB.Recordsets as you mentioned. Getting .NET and VBA talking about anything other than primitive types was beyond-frustrating for me. In fact, I ended up serializing some objects as JSON so the two worlds could communicate.

It does seem insane, but I reinvented the wheel.


  • I followed this MSDN article to make my .NET code callable by VBA.
  • I used this Code Project article (I'm sure you've seen) to convert to Recordset*.
  • I let the frameworks handle string, integers, etc.
  • For all other data types I used Json.Net and a custom VBA class to do JSON serialization.

    *Converted article to VB.Net and added some extra error handling.

share|improve this answer
On the one hand, I feel vindicated that the answer really is that insane. On the other hand, I really wish there were a better answer. Would you be willing/able to share any of the interfaces/conversion code you created? – Karter Mar 12 at 16:46
I'll update my answer with some details. – nunzabar Mar 12 at 17:17
+1 for some really crazy interop edness... thanks Microsoft, we're grateful! – Philip Mar 12 at 17:46

Okay, this probably isn't the most elegant (or complete, at this point) solution; but I think it's the route I am going to go.

Instead of converting the whole thing to an ADODB Recordset (and duplicating any iterations), I just threw out the DataTable class entirely and wrote my own Recordset class as a COM Wrapper for the a generic Data Reader (via the IDataReader interface) and added a new Field class to manage the type conversion and set up Fields as an array of Field (since interop hates generics)

It basically creates a forward-only ADODB Recordset (same limitations) but has the benefit of only loading one row at a time, so the bulk of the data can be handled as managed code until you know what they want to do with it (I'm going to add methods for ToArray, ToAccessDB, ToFile, etc that use the reader) while still allowing the ability to iterate through the Recordset from excel/access/vbscript/vb6 (if that's really what they want to do.. mostly needed that for legacy support anyway) Here is an example, in case anyone else has to do this again; somewhat modified for brevity:

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

From here you just need to define some type like Field or Column and add an interop wrapper for that type:

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

All of this is rather silly, but it seems to work well.

Note: I've only been using .Net for about 4 days now, so this might be terrible, please feel free to comment on anything extremely stupid I might be doing.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.