如何在包装 C# 库的 VBA 类中实现 NewEnum

fre*_*low 5 c# com vba interop com-interop

我正在努力找出如何说服 VBA 对包装我编写的 C# 库的类执行 For Each 操作。

C# 库本身是 OrderedDictionary 的包装器,我在 C# 类中实现的 GetEnumerator 可以与 VBA 配合良好。我可以在 C# 库的简单实例上执行 For Each 操作,没有任何问题。

然而,C# 的一个局限性(有详细记录)是它无法正确实现 .Item(Key),因此您必须求助于 getitem 和 setitem 方法。

为了解决这个问题,并且为了键入 C# 类的实例,我创建了 VBA 类包装器。我面临的问题是如何为这些包装类启用 For Each 循环。

在 VBA 包装类中,我有以下代码,其中 s.HostKvp 是 C# Kvp 库的实例。

注意:枚举属性是通过 RubberDuck '@Enumerator 注释设置的。

'@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
Run Code Online (Sandbox Code Playgroud)

在我的 C# 库中

    [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();
    }
Run Code Online (Sandbox Code Playgroud)

并在界面中

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

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

    //[DispId(-4)]
    IEnumerator GetEnumerator(); 
Run Code Online (Sandbox Code Playgroud)

当我尝试迭代 Wrapped 实例时,我收到错误消息:

Unable to cast object of type '<GetEnumerator>d__67' to type 'stdole.IUnknown'.
Run Code Online (Sandbox Code Playgroud)

在我完成的阅读中,我遇到了 IEnumVARIANT 并通过将 'stdole.IUnknown' 替换为 'System.Runtime.InteropServices.ComTypes.IEnumVARIANT' 进行了测试,但随后出现错误

Unable to cast object of type '<GetEnumerator>d__67' to type 'System.Runtime.InteropServices.ComTypes.IEnumVARIANT'.
Run Code Online (Sandbox Code Playgroud)

我还尝试用“KVPair”替换“stdole.IUnknown”,结果类似。

我感觉超出了我的深度,因此希望您能指导我应该阅读哪些内容,以将 GetEnumerator 生成的 KVPair 放入 VBA 中的 NewEnum 中。

我确实有解决此问题的方法,即在 VBA 包装器中实现 C# 类的 Keys 和 Values 方法。

编辑 2020 年 3 月 6 日

我正在努力遵循所提供的评论,因此我添加了更多信息以尝试使我的问题更清晰。

C#类提供了一个Kvp(键值对)对象。在c#代码中提供主机字典的私有成员是

private OrderedDictionary MyKvp = new OrderedDictionary();
Run Code Online (Sandbox Code Playgroud)

在 VBA 中,以下工作正常。

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
Run Code Online (Sandbox Code Playgroud)

产生预期的输出

1
True
5.0
Hello World.
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,Kvp 类不是强类型的。为了实现强类型,我在 VBA 中使用包装类,下面的代码是一个简单包装类的示例。

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
Run Code Online (Sandbox Code Playgroud)

我遇到的问题是如何编写 C# 代码的 NewEnum 部分。

上述方法

public stdole.IUnknown NewEnum()
Run Code Online (Sandbox Code Playgroud)

不起作用,也不符合我对评论中提供的建议的任何解释。

如有进一步评论,我们将不胜感激。

如果我告诉读者我不是专业程序员,我会涉足VBA来协助编写和整理技术文档,这可能也会有所帮助。我的 C# 类的动力是提供一种更简单的方法将信息放入字典中,并执行简单的任务,例如该值对应的键等。

第二次编辑 2020 年 3 月 6 日 使用测试包装类

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
Run Code Online (Sandbox Code Playgroud)

和测试代码

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  
Run Code Online (Sandbox Code Playgroud)

在 C# 代码中,我尝试了以下操作

    public IEnumerable KvpEnum
    {
        get; set;
    }
Run Code Online (Sandbox Code Playgroud)

返回到 VBA 的值是一个值为 Nothing 的 Variant/Object。以及随后的错误消息

451 属性 let 过程未定义且属性 get 过程未返回对象

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

我在获取并附加到字进程上有一个断点,但断点未触发。在 VBA 中,我得到 Variant/Object/Object 的返回值,该值是空的并且有相同的错误消息

451 属性 let 过程未定义且属性 get 过程未返回对象

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

再次得到相同的结果

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

Err 13 类型不匹配

    public IEnumerable KvpEnum()
    {
        return (IEnumerable)GetEnumerator();
    }
Run Code Online (Sandbox Code Playgroud)

错误 &80004002 无法将类型“d__64”的对象转换为类型“System.Collections.IEnumerable”。

但是(万岁万岁)终于

    public IEnumerator KvpEnum()
    {
        return GetEnumerator();
    }
Run Code Online (Sandbox Code Playgroud)

产生正确的结果。

这一发现还依赖于 Class 和 Com 接口设置的基础。

带接口

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
Run Code Online (Sandbox Code Playgroud)

和班级

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
Run Code Online (Sandbox Code Playgroud)

一切都按预期工作,除了以下事实:如果我从 KpEnum 返回 KVPair,我必须将其转换为 VBA 中的 KVPair 变量,然后才能访问键和值。如果我使用 For Each myItem,我会收到 424 object required 错误。

但由于版本控制问题,MS 不鼓励在类接口中使用 AutoDual。

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
Run Code Online (Sandbox Code Playgroud)

我在 .AddByIndexFromArray 方法调用时收到错误

&1B6 对象不支持此属性或方法

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
Run Code Online (Sandbox Code Playgroud)

我收到一个错误

&DD 类型不匹配

在 myItem 控制变量上,但如果我将 myPair 转换为 KVPar 对象,那么再次

我可以访问 .Key 和 .Value

所以在c#代码中重申一下

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

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

一切正常,除了我必须将 MyItem 控制变量转换为 KVPair,即使当地人寡妇说 myItem 是 Variant/KVPair

如果我使用返回一个原语(即只是键)

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

    public IEnumerator GetEnumerator()
    {
        foreach (DictionaryEntry myPair in MyKvp)
        {
            yield return (dynamic)myPair.Key;
        }
    }
Run Code Online (Sandbox Code Playgroud)

然后我可以访问 myItem 控制变量的值,而无需进行任何转换。

总之,我的问题已解决,但我很高兴听到有关适当的 com 和类接口设置的任何评论。