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.