在Windows上,使用C#创作的COM服务器,可以为早期绑定和后期绑定代码返回SAFEARRAY吗?

时间:2016-08-02 17:24:55

标签: c# vba com com-interop safearray

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

简介

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

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

      1. 风格选择和决定
        • 我是一名优秀的COM公民,并将接口定义与类定义分开。
          • 我在类上使用[ClassInterface(ClassInterfaceType.None)]和[ComDefaultInterface(typeof(< interface>))]属性来实现这种明确的划分。
        • 由于客户端是Excel VBA,我们需要坚持自动化兼容类型,因此SAFEARRAY
      2. 两个C#classes / com类:
        • Apples是一个简单的状态容器,用于将数据编组回客户端,并且除了getter和setter之外不会执行任何方法。
        • FruitCounter是一个工人类,它有一个方法enumerateApples(),它返回一个苹果实例的SAFEARRAY。
      3. 所以Apples界面和类的源代码是:

        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‡
        2. 早期绑定Excel VBA客户端代码

          1. 打开调试启动选项中指定的工作簿(参见上面的†)
          2. 添加基本标准模块(不是类模块)。
          3. 转到工具 - &gt;生成的类型库的引用和复选框(参见上文‡)
          4. 添加以下代码
          5.     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时,可以检查数组内容以获得成功的编组。 早期结合的成功!

            后期绑定Excel VBA客户端代码

            1. 我还使用Excel VBA中的后期限制来摆脱部署刮擦,我也可以热插拔dll,即安装新的COM服务器而不关闭Excel(我应该在SO上发布这个技巧)。
            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
              
            4. 运行此代码会在标记的行处引发类型不匹配(VB错误13)。因此,相同的COM服务器代码在Excel VBA后期绑定模式下不起作用。 失败的后期约束!

              变通方法

              1. 所以在调查过程中我写了一个返回类型为Object []的第二个方法,最初这个没有用,因为生成的idl下降为星号。 idl来自

                {{1}}

                    // Return type Apples[] works for early binding but not late binding
                    // works
                    HRESULT enumerateApples([out, retval] SAFEARRAY(IApples*)* pRetVal);
                
              2. 使用MarshalAs属性修复了星号数

                    // 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 
                

                这适用于后期绑定但不适用于绑定! Aaaargh!

              3. 摘要

                我有两个方法正在工作,一个用于早期绑定,一个用于后期绑定,这是不能令人满意的,因为它意味着将每个方法加倍。

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

1 个答案:

答案 0 :(得分:2)

我通过将返回的值编组到Variant对此进行了一些测试,然后转储返回的VARIANT结构的内存以查看VARTYPE是什么。对于早期绑定调用,它返回Variant,其中VARTYPE为VT_ARRAY & VT_DISPATCH。对于后期绑定调用,它返回的是VT_ARRAY & VT_UNKNOWN的VARTYPE。苹果 应该已经被定义为在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