How to Implement NewEnum in a VBA Class that wraps a C# library

Viewed 279

I'm struggling to find out how to persuade VBA to do a For Each on a class which wraps a C# library I wrote.

The C# library is itself a wrapper for an OrderedDictionary and the GetEnumerator I've implemented in the C# class works fine with VBA. I can do For Each on straightforward instances of the C# library with no problems.

However, a limitation of C#, which is well documented, is that it cannot correctly implement the .Item(Key) and consequently you have to resort to getitem and setitem methods.

To get around this issue, and also to Type the instances of the C# class I am creating VBA Class wrappers. The problem I am facing is how to enable For Each Loops for these wrapper classes.

In the VBA wrapper class I have the code below, where s.HostKvp is the instance of the C# Kvp Library.

NB The enum attribute is set through the RubberDuck '@Enumerator annotation.

'@Enumerator
Public Property Get NewEnum() As IUnknown

    Set NewEnum = s.HostKvp.NewEnum  // the function in the c# library is 'NewEnum' and not '[_NewEnum]'

End Property

In the C# library I have

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]

    [DispId(-4)]
    public stdole.IUnknown NewEnum()
    {
        return (stdole.IUnknown) GetEnumerator();
    }

    public IEnumerator GetEnumerator() 
    {
        foreach (KeyValuePair<dynamic, dynamic> myPair in MyKvp)
        {
            yield return new KVPair(myPair.Key, myPair.Value);
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

and in the interface

    [ComVisible(true)]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

    [DispId(-4)]
    stdole.IUnknown NewEnum();

    //[DispId(-4)]
    IEnumerator GetEnumerator(); 

When I try to iterate the Wrapped instance I get the error message:

Unable to cast object of type '<GetEnumerator>d__67' to type 'stdole.IUnknown'.

IN the reading I've done I've come across IEnumVARIANT and have tested by replacing 'stdole.IUnknown' by 'System.Runtime.InteropServices.ComTypes.IEnumVARIANT' but then get the error

Unable to cast object of type '<GetEnumerator>d__67' to type 'System.Runtime.InteropServices.ComTypes.IEnumVARIANT'.

I've also tried replacing 'stdole.IUnknown' with 'KVPair' with similar results.

I'm feeling well out of my depth here so would appreciate a pointer as to what I should be reading to get the KVPair produced by GetEnumerator into NewEnum in VBA.

I do have a workaround for this issue which is to implement the Keys and Values methods of the C# class in the VBA wrapper.

Edit 06 March 2020

I'm struggling to follow the comments provided so I'm adding more information to try and make my issue clearer.

the C# class provides a Kvp (Key Value Pair) object. The private member which provides the host dictionary in the c# code is

private OrderedDictionary MyKvp = new OrderedDictionary();

In VBA the following works fine.

Dim myKvp as Kvp: Set myKvp = New Kvp
myKvp.AddbyIndexFromArray Array(1,True, 5.0, "Hello World")

Dim myItem as Variant

For Each myItem in myKvp
    Debug.Print myItem
Next

Produces the expected output

1
True
5.0
Hello World.

As you can see the Kvp class is not strongly typed. To achieve strong typing I use wrapper classes in VBA, The code below is an example of a simple wrapper class.

Option Explicit

Private Type State
    ' Key:Long by Value:String -> "L88,R67,U89 etc"
    HostKvp As Kvp
End Type

Private s As State

Private Sub Class_Initialize()
    Set s.HostKvp = New Kvp
End Sub

'@DefaultMember
Public Property Get Item(ByVal Key As Long) As String
    Item = s.HostKvp.Item(Key)
End Property


Public Property Let Item(ByVal Key As Long, ByVal Value As Long)
   s.HostKvp.Item(Key) = Value
End Property


'@Enumerator
Public Function NewEnum() As IUnknown
    Set NewEnum = s.HostKvp.NewEnum

End Function

' Many other method wrappers not reproduced here

The issue I am having is how to code the NewEnum part of the c# code.

the method described above

public stdole.IUnknown NewEnum()

does not work, nor to any of my interpretations of the advice offered in the comments.

Further comment would be appreciated.

It might also help if I advise readers that I am not a professional programmer, I dabble in VBA to assist writing and massaging technical documents. The impetus for my c# class was to provide a simpler way of getting information into a dictionary and simple tasks like what key does this value correspond to etc.

Second Edit 6 March2020 Using the Test wrapper class

Class KvpEnumTest
Option Explicit

Private Type State

    HostKvp As KvpOD

End Type

Private s As State

Private Sub Class_Initialize()

    Set s.HostKvp = New KvpOD

End Sub

'@DefaultMember
Public Property Get Item(ByVal Key As Long) As String

    Item = s.HostKvp.FromKey(Key)

End Property

Public Property Let Item(ByVal Key As Long, ByVal Value As String)

    s.HostKvp.ToKeyKey Key, Value

End Property


Public Sub AddByIndex(ByVal Value As String)
    s.HostKvp.AddByIndex Value
End Sub

Public Sub AddByIndexFromArray(ByVal Value As Variant)
    s.HostKvp.AddByIndexFromArray Value
End Sub


Public Function Keys() As Variant
    Keys = s.HostKvp.GetKeys
End Function

Public Function Values() As Variant
    Values = s.HostKvp.GetValues
End Function

'@Enumerator
Public Property Get NewEnum() As Variant
    Set NewEnum = s.HostKvp.KvpEnum

End Property
In the VBA wrapper class I have

'@Enumerator
Public Property Get NewEnum() As Variant

    set NewEnum = s.HostKvp.KvpEnum

End Property

And the test code

Public Sub TestKvpEnumTest()

    Dim myTest As KvpEnumTest

    Set myTest = New KvpEnumTest

    myTest.AddByIndexFromArray Split("Hello there world its a nice day")
    Dim myKeys As Variant
    myKeys = myTest.Keys
    ' myKeys is visible in the locales window as an Array(0 to 6) of Variant/Long

    Dim myValues As Variant
    myValues = myTest.Values
    ' myValues is visible in the locales window as an Array(0 to 6) of Variant/String

    Dim myItem As Variant
    For Each myItem In myTest

        Debug.Print myItem

    Next

End Sub  

In the c# code I have tried the following for

    public IEnumerable KvpEnum
    {
        get; set;
    }

The value returned to VBA is a Variant/Object with a value of Nothing. and a subsequent error message of

451 Property let procedure not defined and property get procedure did not return an object

with

    public IEnumerable KvpEnum
    {
        get
        {
            foreach (DictionaryEntry myPair in MyKvp)
            {
                yield return new KVPair(myPair.Key, myPair.Value);
            }
        }
        set
        { }
    }

I have a breakpoint on the Get and attach to the word process but the breakpoint isn't triggered. In VBA I get a returned value of Variant/Object/Object which is empty and the same error message

451 Property let procedure not defined and property get procedure did not return an object

with

    public IEnumerable KvpEnum()
    {
        foreach (DictionaryEntry myPair in MyKvp)
        {
            yield return new KVPair(myPair.Key, myPair.Value);
        }
    }

With again the same results

    with public IEnumerator KvpEnum()
    {
        foreach (DictionaryEntry myPair in MyKvp)
        {
            yield return new KVPair(myPair.Key, myPair.Value);
        }
    }

Err 13 Type Mismatch

with

    public IEnumerable KvpEnum()
    {
        return (IEnumerable)GetEnumerator();
    }

error &80004002 Unable to cast object of type 'd__64' to type 'System.Collections.IEnumerable'.

But (hurrah hurrah) finally

    public IEnumerator KvpEnum()
    {
        return GetEnumerator();
    }

Produces the correct result.

This discovery has been dependant on also footling with the Class and Com interface settings.

with interface

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]

and class

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]

everything works as it should except for the fact if I return a KVPair from KpEnum i have to cast it to a KVPair variable in VBA before I can access the Key and Value. If I use the For Each myItem I get a 424 object required error.

But MS discourage the use of AutoDual in the classinterface due to versioning issues.

with

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]

I get an error at the .AddByIndexFromArray method call

&1B6 Object doesn't support this property or method

with

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDispatch)]

I get an error

&DD Type mismatch

on the myItem control variable but again if I cast myPair to a KVPar object

I can access the .Key and .Value

So to reiterate in the c# code

    public IEnumerator KvpEnum()
    {
        return  GetEnumerator();
    }

    public IEnumerator GetEnumerator()
    {
        foreach (DictionaryEntry myPair in MyKvp)
        {
            yield return (dynamic)new KVPair(myPair.Key, myPair.Value);
        }
    }

everything works except that I have to cast the MyItem control variable to a KVPair even though the locals widow says myItem is a Variant/KVPair

If I return a primitive (i.e. just the key) using

    public IEnumerator KvpEnum()
    {
        return GetEnumerator();
    }

    public IEnumerator GetEnumerator()
    {
        foreach (DictionaryEntry myPair in MyKvp)
        {
            yield return (dynamic)myPair.Key;
        }
    }

Then I can access the value of the myItem control variable without any need to cast.

In conclusion my problem is Solved, but I'm happy hear any comments on the appropriate com and class interface settings.

0 Answers
Related