【问题标题】:On Windows, with a C# authored COM server, can one return a SAFEARRAY both for early bound and late bound code?在 Windows 上,使用 C# 编写的 COM 服务器,可以为早期绑定和后期绑定代码返回 SAFEARRAY 吗?
【发布时间】:2016-12-08 04:31:51
【问题描述】:

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

简介

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

设置项目类型和项目属性

  1. 创建一个新的 C# 类库项目。
  2. 我将我的文件命名为 LateBoundSafeArraysProblem,并将源文件重命名为 LateBoundSafeArraysProblem.cs。
  3. 在 AssemblyInfo.cs 中将第 20 行修改为 ComVisible(true) ,因此可见性是通用的(仍然需要公共关键字)。
  4. 设置项目属性:
    • 设置构建选项,在 Project Properties->Build->Output 中选中“Register for COM interop”复选框。
    • 设置调试选项以启动 Excel 并加载 Excel 工作簿客户端:
      • 在项目属性->调试->开始操作并选择单选按钮“开始外部问题”并输入 Microsoft Excel 的路径,对我来说是“C:\Program Files\Microsoft Office 15\root\office15\excel.exe '。
      • 在项目属性->调试->启动选项中输入客户端 Excel 启用宏的工作簿的名称,对我来说是 C:\Temp\LateBoundSafeArraysProblemClient.xlsm。 †

创建COM服务器源代码

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

所以苹果的接口和类的源代码是:

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接口和类的源代码是

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 ‡

早期绑定 Excel VBA 客户端代码

  1. 打开调试启动选项中指定的工作簿(见上文†)
  2. 添加基本标准模块(不是类模块)。
  3. 转到工具->生成的类型库的参考和复选框(见上文‡)
  4. 添加以下代码
子TestEarlyBound() '工具 -> 对类型库 LateBoundSafeArraysProblem.tlb 的引用 将 fc 调暗为 LateBoundSafeArraysProblem.FruitCounter 设置 fc = New LateBoundSafeArraysProblem.FruitCounter Dim apples() As LateBoundSafeArraysProblem.apples 苹果() = fc.enumerateApples() 停止 结束子

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

后期绑定 Excel VBA 客户端代码

  1. 我还在 Excel VBA 中使用后期边界来让自己摆脱部署问题,我还可以热交换 dll,即在不关闭 Excel 的情况下安装新的 COM 服务器(我应该在 SO 上发布这个技巧)。
  2. 从 (1) 开始,我将使用相同的 Excel VBA 工作簿作为测试后期绑定的平台,并相应地更改声明。
  3. 在同一个模块中添加如下代码

    子 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. 因此,在调查期间,我编写了第二个返回类型为 Object[] 的方法,最初这不起作用,因为生成的 idl 以星号形式删除。 idl 来自

    // 返回类型 Apples[] 适用于早期绑定,但不适用于后期绑定 // 有效 HRESULT enumerateApples([out, retval] SAFEARRAY(IApples*)* pRetVal);

    // 返回类型 Object[] 失败,因为我们放弃了一个间接级别 //(可能是值类型和引用类型之间的混淆) // 根本不工作(迟到或早) HRESULT enumerateApplesLateBound([out, retval] SAFEARRAY(VARIANT)* pRetVal); // 当星号变成 SAFEARRAY 值类型时删除,不好
  2. 使用 MarshalAs 属性固定星号的数量

    // 仍然使用 Object[] 但使用 MarshalAs // [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = System.Runtime.InteropServices.VarEnum.VT_UNKNOWN)] // 适用于后期绑定但不适用于早期绑定!!!啊啊啊!!! HRESULT enumerateApplesLateBound([out, retval] SAFEARRAY(IDispatch*)* pRetVal);

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

总结

我有两种方法可以工作,一种用于早期绑定,另一种用于后期绑定,这并不令人满意,因为这意味着将每种方法都加倍。

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

【问题讨论】:

  • 看起来您的 VBA 代码中存在错误,您必须使用 Set 关键字来分配 apples 变量。如果没有 Set 它将尝试正确检索默认值。一个数组有一个,即索引器。接下来是猫和狗。不完全确定为什么它会提前结束,大概VBA编译器知道当它可以看到它是一个数组时不使用默认属性。考虑实际返回一个枚举器(System.Collections.IEnumerator)而不是一个数组所以你可以使用 For Each。
  • 嗨,汉斯,VBA 语法很好。刚刚用一些代码对工作簿中的类进行了确认。
  • 奇怪的是,Dim apples As Variant: apples = fc.enumerateApples() 在后期绑定时有效。它甚至用Debug.Print IsArray(apples) 返回“True”。但随后Debug.Print apples(0).variety 是错误 424:需要对象。
  • @ComIntern:啊!如果为真,则表明它正在创建一个 VARIANT 数组而不是 SAFEARRAY。 VARIANT 数组仅限于基元。 IUnknown 和 IDispatch。嗯,怎么可能? idl 很清楚地说是 SAFEARRAY。
  • 我实际上想到了这一点并尝试返回IApples[],但我得到了同样的错误。它还应该在 tlb 中将 IApples 声明为 IDispatch。

标签: c# vba com com-interop safearray


【解决方案1】:

我通过将返回值编组到Variant 对此进行了一些测试,然后转储返回的 VARIANT 结构的内存以查看 VARTYPE 是什么。对于早期绑定调用,它返回一个Variant,VARTYPE 为VT_ARRAY &amp; VT_DISPATCH。对于后期绑定调用,它返回的 VARTYPE 为 VT_ARRAY &amp; VT_UNKNOWN。苹果应该已经被定义为在 tlb 中实现IDispatch,但由于某种我无法理解的原因,VBA 难以处理来自后期绑定调用的IUnknown 数组。解决方法是在 C# 端将返回类型更改为 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

【讨论】:

  • 太棒了!谢谢一百万。
猜你喜欢
  • 1970-01-01
  • 2011-07-17
  • 2010-10-21
  • 1970-01-01
  • 1970-01-01
  • 2016-12-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多