在 Windows 上,使用 C# 编写的 COM 服务器,一个 return SAFEARRAY 可以同时用于早期绑定代码和后期绑定代码吗?

On Windows, with a C# authored COM server, can one return a SAFEARRAY both for early bound and late bound code?

问题很长,所以我会用要点来格式化以便于讨论

简介

  1. 我正在编写 C# COM 服务器。
  2. COM 服务器用于 Excel VBA 早期绑定和后期绑定模式。
  3. 我的绊脚石是如何 return 实例化 classes 的 SAFEARRAY,它可以在早期和晚期绑定模式下工作;我收到错误。
  4. 我为此做了很多工作(一整天):
    • 我已经完成了一些诊断并设置了调试器来阐明我遇到的错误。
    • 我已经进行了相当详尽的谷歌搜索。
    • 我发现了一些不太令人满意的解决方法。
    • 我现在真的很困惑,正在寻找 COM Interop 专家来帮助我找到一个好的解决方案。

设置项目类型和项目属性

  1. 创建一个新的 C# Class 库项目。
  2. 我将我的文件命名为 LateBoundSafeArraysProblem,并将源文件重命名为 LateBoundSafeArraysProblem.cs。
  3. 在AssemblyInfo.cs中将第20行修改为ComVisible(true),这样可见性是通用的(仍然需要public关键字)。
  4. 设置项目属性:
    • 设置构建选项,在 Project Properties->Build->Output 我勾选 'Register for COM interop' 复选框。
    • 设置调试选项以启动 Excel 并加载 excel 工作簿客户端:
      • 在 Project Properties->Debug->Start Action 和 select 单选按钮 'Start external problem' 中输入 Microsoft Excel 的路径,对我来说是 'C:\Program Files\Microsoft Office 15\root\office15\excel.exe'.
      • 在 Project Properties->Debug->Start Options 中输入启用了宏的客户端 Excel 工作簿的名称,对我来说是 C:\Temp\LateBoundSafeArraysProblemClient.xlsm。 †

创建 COM 服务器源代码

  1. 风格选择和决定
    • 我是一名优秀的 COM 公民,将接口定义与 class 定义区分开来。
      • 我在 class 上使用 [ClassInterface(ClassInterfaceType.None)] 和 [ComDefaultInterface(typeof())] 属性来实现这种清晰的划分。
    • 因为客户端是 Excel VBA 我们需要坚持自动化兼容类型,因此 SAFEARRAY
  2. 两个C#classes/comclasses:
    • Apples 是一个简单的状态容器,用于将数据编组回客户端,除了 getter 和 setter 之外不包含任何方法。
    • FruitCounter 是一个 worker class,它有一个方法 enumerateApples(),它是 return Apples 实例的 SAFEARRAY。

所以苹果界面的源代码和class是:

public interface IApples 
{
    string variety { get; set; }
    int quantity { get; set; }
}

[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IApples))]
public class Apples : IApples
{
    public string variety { get; set; }
    public int quantity { get; set; }
}

以上代码没有争议并且工作正常。

FruitContainer接口的源代码和class是

public interface IFruitCounter
{
    Apples[] enumerateApples();
}

[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IFruitCounter))]
public class FruitCounter : IFruitCounter
{
    public Apples[] enumerateApples()
    {
        List<Apples> applesList = new List<Apples>();

        //* Add some apples - well, one in fact for the time being 
        Apples app = new Apples();
        app.variety = "Braeburn";
        app.quantity = 4;
        applesList.Add(app);

        // * finished adding apples want to convert to SAFEARRAY 
        return applesList.ToArray();
    }
}

这适用于早期绑定,但不适用于后期绑定。

  1. 构建项目,应该有一个dll和一个tlb。
    • 一个输出将是LateBoundSafeArraysProblem.dll
    • 也会输出一个类型库,LateBoundSafeArraysProblem.tlb‡

早期绑定ExcelVB客户端代码

  1. 打开调试启动选项中指定的工作簿(见上文†)
  2. 添加基本标准模块(不是 class 模块)。
  3. 转到“工具”->“参考”并选中生成的类型库的复选框(见上 ‡)
  4. 添加以下代码
    Sub TestEarlyBound()
        'Tools -> References to type library LateBoundSafeArraysProblem.tlb 
        Dim fc As LateBoundSafeArraysProblem.FruitCounter
        Set fc = New LateBoundSafeArraysProblem.FruitCounter

        Dim apples() As LateBoundSafeArraysProblem.apples
        apples() = fc.enumerateApples()

        Stop

    End Sub

当执行到达 Stop 时,可以检查数组内容是否成功编组。 提前绑定成功!

后期绑定ExcelVB客户端代码

  1. 我还在 Excel VBA 中使用后期绑定来避免部署问题,我也可以热交换 dll,即安装新的 COM 服务器而不关闭 Excel(我应该 post 那个技巧。
  2. 从 (1) 开始,我将使用相同的 Excel VBA 工作簿作为测试后期绑定的平台,并相应地更改声明。
  3. 在同一模块中添加以下代码

        Sub TestFruitLateBound0()
    
    
        Dim fc As Object 'LateBoundSafeArraysProblem.FruitCounter
        Set fc = CreateObject("LateBoundSafeArraysProblem.FruitCounter")
    
        Dim apples() As Object 'LateBoundSafeArraysProblem.apples
        apples() = fc.enumerateApples()   '<==== Type Mismatch thrown
    
        Stop
    
    End Sub
    

运行 此代码在标记的行抛出类型不匹配(VB 错误 13)。因此相同的 COM 服务器代码在 Excel VBA 后期绑定模式下不起作用。 后期绑定失败!

解决方法

  1. 所以在调查期间我写了第二个方法 return 类型 Object[],最初这不起作用,因为生成的 idl 作为星号掉落。 idl 来自

        // Return type Apples[] works for early binding but not late binding
        // works
        HRESULT enumerateApples([out, retval] SAFEARRAY(IApples*)* pRetVal);
    

        // Return type Object[] fails because we drop a level of indirection 
        // (perhaps confusion between value and reference types)
        // does NOT work AT ALL (late or early)
        HRESULT enumerateApplesLateBound([out, retval] SAFEARRAY(VARIANT)* pRetVal);      // dropped as asterisk becomes SAFEARRAY to value types, no good 
    
  2. 使用 MarshalAs 属性固定星号的数量

        // Still with Object[] but using MarshalAs
        // [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = System.Runtime.InteropServices.VarEnum.VT_UNKNOWN)]
        // works for late-bound but not early bound !!! Aaaargh !!!!
        HRESULT enumerateApplesLateBound([out, retval] SAFEARRAY(IDispatch*)* pRetVal);   
    

    这适用于后期装订,但不适用于早期装订!啊啊啊!

总结

我有两种方法可以工作,一种用于早期绑定,一种用于后期绑定,但效果不理想,因为这意味着将每种方法加倍。

如何让一种方法同时适用于早期绑定和后期绑定?

我通过将 returned 值封送到 Variant 中对此做了一些测试,然后转储 returned VARIANT 结构的内存以查看什么VARTYPE 是。对于早期的绑定调用,它是 returning 一个 VariantVT_ARRAY & VT_DISPATCH 的 VARTYPE。对于后期绑定调用,它是 returning VT_ARRAY & VT_UNKNOWN 的 VARTYPE。苹果 应该 已经被定义为在 tlb 中实现 IDispatch,但由于某些我无法理解的原因,VBA 难以处理 [=17= 的数组] 来自后期绑定电话。解决方法是在 C# 端将 return 类型更改为 object[]...

public object[] enumerateApples()
{
    List<object> applesList = new List<object>();

    //* Add some apples - well, one in fact for the time being 
    Apples app = new Apples();
    app.variety = "Braeburn";
    app.quantity = 4;
    applesList.Add(app);

    // * finished adding apples want to convert to SAFEARRAY 
    return applesList.ToArray();
}

...并将它们拉入 VBA 侧的 Variant

Sub TestEarlyBound()
    'Tools -> References to type library LateBoundSafeArraysProblem.tlb
    Dim fc As LateBoundSafeArraysProblem.FruitCounter
    Set fc = New LateBoundSafeArraysProblem.FruitCounter

    Dim apples As Variant
    apples = fc.enumerateApples()

    Debug.Print apples(0).variety   'prints "Braeburn"
End Sub

Sub TestFruitLateBound0()
    Dim fc As Object
    Set fc = CreateObject("LateBoundSafeArraysProblem.FruitCounter")

    Dim apples As Variant
    apples = fc.enumerateApples()

    Debug.Print apples(0).variety   'prints "Braeburn"
End Sub