Context:
I have a legacy VB6 application that uses DAO and Jet. The owner of the company does not want to change any of the DAO code as we move into VB.Net, so I've been working on a set of VB wrappers to present an interface that looks just like DAO. The advantage to this is that we can subsequently replace the underlying DAO code in the wrappers with ADO.Net to use SQL Server in the future.
The design:
I have a set of abstract classes that look an awful lot like the DAO object model. Underlying that, I have implemented a set of concrete classes from each of the abstract classes and implemented a pass-through to each of the DAO routines. DAO continues to manage the database access, for now. Ultimately, I or someone else, will write a new set of concrete classes for some other DB engine. So far, this is working out pretty well.
The problem:
I have implemented custom enumerators for each of the DAO collection objects as per http://msdn2.microsoft.com/en-us/library/1t2267t6(VS.80).aspx.
The legacy VB6 code now looks like
dim idx as ABXDB.Index
For Each idx in ABXDB.TableDefs.Indexes
This throws a Type Mismatch error on the For Each because, I think, the Current property of the custom enumerator is returning an Object and VB6 can't make the conversion to the ABXDB.Index type. I thought I might change the definition of Current to return an ABXDB.Index type as well, but that didn't fly as the IEnumerator doesn't define such a thing.
I've also tried changing the VB6 code to
dim obj as object
dim idx as ABXDB.Index
For each obj in ABXDB.TableDefs.Indexes
set idx = obj
But that fails in the same way. Similar results if I declare the loop var as variant.
So I think I've got a com interop problem and I have no clue how to move forward. What are my options
Sean

custom enumerator, com interop, and vb6 client
Wayner7
Hot Dawg, Peter! This was what I needed to get over the hump. It turns out that I was darn close on my own, with your earlier posts. The problem that I had was in the custom enumerator class, I was returning a DAO object, and not one of my abstract objects. Looking at your code gave me the clue, and it's now working!
Thanks ever so much!
Sean
Aldrenn
Well, after posting a while ago, I discovered that the syntax in question is intended to decorate the return type of the GetEnumerator property in VB, so it ought to look something like:
Public Function GetEnumerator() As <System.Runtime.InteropServices.MarshalAsAttribute(UnmanagedType.CustomMarshaler, MarshalType = "System.Runtime.Interop-Services.CustomMarshalers.EnumeratorToEnumVariantMarshaler,CustomMarshalers, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")> System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumeratorHowever, it's now complaining that the
Name 'MarshalType' is not declared.
I'm guessing that C# (which I admittedly know little about) handles named parameters while VB does not. I thought that I rememberd VB6 doing this, but I may be thinking of something else.
I still don't understand all of the implications of the MarshalAsAttribute decoration - I see that it has lots of parameters, but so far, I've not found a construction that compiles.
I must be getting closer, but I still feel far, far away.
Sean
mjoc69
Hi ,
Based on my understanding, you wanted to expose the .NET class to COM. So that the COM client(VB6 application) can access to the class via foreach.
If I misunderstood, please feel free let me know.
I think you may try to decorate the GetEnumerator with the Marshal attribute.
e.g.
[DispId(-4)]
[return:
MarshalAs(UnmanagedType.CustomMarshaler,MarshalType="System.Runtime.Interop-S
ervices.CustomMarshalers.EnumeratorToEnumVariantMarshaler,CustomMarshalers,
Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public IDictionaryEnumerator GetEnumerator()
Also as you said, we need to expose the type we hope the current to return too.
What do you mean by "ABXDB.Index"
Do you mean the type DAO.Index in VB6
If so, I think you may try add reference to the DAO COM Library, so that you will get a .NET wrapper for the type. Or you can define one yourself.
Here is a link for your reference.
http://groups.google.com/group/microsoft.public.dotnet.framework.interop/browse_frm/thread/7ca14c08352adb69/94ca86ddae382ead lnk=st&q=%22v-phuang%22+IEnumerable+for+each+vbscript&rnum=1&hl=en#94ca86ddae382ead
If you still have any concern, please feel free to post here.
Best regards,
Peter Huang
LiLo
Digging deeper, I discovered that named parameters in attributes use :=, so my declaration needed to look like
<System.Runtime.InteropServices.DispId(-4)> _
Public Function GetEnumerator() As <System.Runtime.InteropServices.MarshalAsAttribute(UnmanagedType.CustomMarshaler, MarshalType:="System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler,CustomMarshalers, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")> System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator Return New CTableDefsEnumerator(moInternal_DAO_TableDefs) End FunctionBy the way, I discovered this by reading through the O'Reilly VB.Net 2005 in a Nutshell. I LOVE the O'Reilly Nutshell books - I've got a half dozen of them for various topics. If you don't use them, you should!
So, it now compiles cleanly, but the GetEnumerator routine is never called. I've set a breakpoint on the on the return statement and on the appropriate routines in the CTableDefsEnumerator class and they are never hit. Something is still quite wrong.
Sean
windiswebwerkstatt
Hi,
I am sorry for that.
I am considered the issue as a ComInterop issue and I did not notice this is a VB.NET forum.
Anyway, here goes the code in VB.NET version for your reference.
'[.NET class] which will expose a customer class TestObj.
Imports System.Runtime.InteropServices
'<ClassInterface(ClassInterfaceType.AutoDual)> _
Public Class TestObj
Dim _name As String
Public Sub New(ByVal index As Integer)
_name = "Object " & index.ToString() & ": " & Rnd().ToString()
End Sub
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
End Class
'<ClassInterface(ClassInterfaceType.AutoDual)> _
Public Class TestObjCol
Implements ICollection, IEnumerator
Dim _objs As ArrayList = New ArrayList()
Dim ide As IEnumerator
Public Sub New()
For i As Integer = 1 To 10
_objs.Add(New TestObj(i))
Next
End Sub
Public Sub CopyTo(ByVal array As System.Array, ByVal index As Integer) Implements System.Collections.ICollection.CopyTo
_objs.CopyTo(array, index)
End Sub
Public ReadOnly Property Count() As Integer Implements System.Collections.ICollection.Count
Get
Return _objs.Count
End Get
End Property
Public ReadOnly Property IsSynchronized() As Boolean Implements System.Collections.ICollection.IsSynchronized
Get
Return _objs.IsSynchronized
End Get
End Property
Public ReadOnly Property SyncRoot() As Object Implements System.Collections.ICollection.SyncRoot
Get
Return _objs.SyncRoot
End Get
End Property
<DispId(-4)> _
Public Function GetEnumerator() As <MarshalAs(UnmanagedType.CustomMarshaler, MarshalType:="System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler,CustomMarshalers,Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a")> System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
Debug.WriteLine("GetEnumerator")
ide = _objs.GetEnumerator()
Dim i As IEnumerator = Me
Return i
End Function
Public ReadOnly Property Current() As Object Implements System.Collections.IEnumerator.Current
Get
Debug.WriteLine(ide.Current.ToString())
Dim o As TestObj = ide.Current
Return o
End Get
End Property
Public Function MoveNext() As Boolean Implements System.Collections.IEnumerator.MoveNext
Return ide.MoveNext()
End Function
Public Sub Reset() Implements System.Collections.IEnumerator.Reset
ide.Reset()
End Sub
End Class
I call it from VB6 via the code below.
Private Sub Command1_Click()
Dim obj As Enumerators2COM.TestObj
Dim objs As New Enumerators2COM.TestObjCol
For Each obj In objs
Debug.Print obj.Name
Next
End Sub
If you still have any concern, please feel free to post here.
Best regards,
Peter Huang
Matt Ahlgren
See the next post for updates:
----------------------------------------------
Thanks for your reply, Peter.
I should have mentioned that my wrapper classes are written in VB.Net and I'm having some trouble converting the decoration to a syntax that VB likes. Specifically, I can't seem to find a construct for
[return:
MarshalAs(UnmanagedType.CustomMarshaler,MarshalType="System.Runtime.InteropS
ervices.CustomMarshalers.EnumeratorToEnumVariantMarshaler,CustomMarshalers,
Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public IDictionaryEnumerator GetEnumerator()
that the VB compiler will accept. I've looked around on the web to see if there's an example, but I so far haven't come up with one. The closest I've come (by guessing) is
<System.Runtime.InteropServices.MarshalAs(UnmanagedType.CustomMarshaler, MarshalType =
"System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler,CustomMarshalers, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")> _Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator Return New CFieldsEnumerator(moInternal_DAO_Fields) End Function
but this generates a compiler error that says
Error 5 Attribute 'MarshalAsAttribute' cannot be applied to 'GetEnumerator' because the attribute is not valid on this declaration type.
I have been able to apply the Dispid(-4) attribute without error, but by itself, that doesn't solve my problem.
This is the deepest that I've gotten so far into Interop, and I'm at the very ragged edge of what I understand. Any clues are most appreciated.
To answer your other question,
What do you mean by "ABXDB.Index"
Do you mean the type DAO.Index in VB6
I have defined abstract database classes for nearly every corresponding DAO object. So I have a
Public Class AbstractDatabase
Public Class AbstractTableDef
Public Class AbstractIndex
each with suitable MustInherit interfaces to ensure a child correctly implements the right things.
Then, I have defined a set of concrete classes for Jet
Public Class JetDatabase
Inherits AbstractDatabase
Public Class JetTableDef
Inherits AbstractTableDef
and so on.
Lastly, in my legacy code, any declaration that relates to a DAO object is changed to refer to the corresponding Abstract object, and I can then maintain very nearly all of the syntactical constructs that were written for the DAO objects, but now do essentially the same thing through an extra layer of indirection (i.e., through my wrappers). As I said in my earlier post, the goal of this exercise is to allow someone else to write another set of concrete classes for SQL Server using ADO.Net. The fact that they present the same interface to the legacy code means that I shouldn't have to make any other changes to connect to a new DB.
So far, with the exception of the enumerators not working as I think they should, this approach is working well, albeit with some negative performance hits. It remains to be seen whether we can shoehorn ADO.Net into this scheme.
Thanks for your help.
Sean