问题很长,所以我将用要点格式化以便于讨论
介绍
- 我正在编写一个 C# COM 服务器。
- COM 服务器可在 Excel VBA 中以早期绑定和后期绑定模式使用。
- 我的绊脚石是如何返回在早期和后期绑定模式下都可以工作的实例化类的 SAFEARRAY;我收到错误。
- I have done plenty of work on this (all day):
- 我已经完成了一些诊断并设置了调试器以阐明我收到的错误。
- 我已经做了一些相当详尽的谷歌搜索。
- 我发现了一些不太令人满意的解决方法。
- 我现在真的很困惑,正在寻找 COM 互操作专家来帮助我找到一个好的解决方案。
设置项目类型和项目属性
- 创建一个新的 C# 类库项目。
- 我将我的文件命名为 LateBoundSafeArraysProblem,并将源文件重命名为 LateBoundSafeArraysProblem.cs。
- 在 AssemblyInfo.cs 中将第 20 行修改为 ComVisible(true) ,因此可见性是通用的(仍然需要 public 关键字)。
- Set the Project Properties:
- 设置构建选项,在“项目属性”->“构建”->“输出”中,选中“注册 COM 互操作”复选框。
- Set the debug options to launch Excel and load an excel workbook client:
- 在“项目属性”->“调试”->“启动操作”中,选择单选按钮“启动外部问题”,然后输入 Microsoft Excel 的路径,对我来说是“C:\Program Files\Microsoft Office 15\root\office15\excel.exe”。
- 在“项目属性”->“调试”->“启动选项”中,输入启用客户端 Excel 宏的工作簿的名称,对于我来说,该名称为 C:\Temp\LateBoundSafeArraysProblemClient.xlsm。 †
创建 COM 服务器源代码
- Style choices and decisions
- I'm being a good COM citizen and dividing the interface definitions from the class definitions.
- 我在类上使用 [ClassInterface(ClassInterfaceType.None)] 和 [ComDefaultInterface(typeof())] 属性来实现这种清晰的划分。
- 由于客户端是 Excel VBA,我们需要坚持自动化兼容类型,因此 SAFEARRAY
- The two C# classes/ com classes:
- 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();
}
}
这适用于早期绑定,但不适用于后期绑定。
- Build project, one should have a dll and a tlb.
- 一种输出是 LateBoundSafeArraysProblem.dll
- 还将输出一个类型库,LateBoundSafeArraysProblem.tlb ‡
早期绑定的 Excel VBA 客户端代码
- 打开调试启动选项中指定的工作簿(参见上文 †)
- 添加基本标准模块(不是类模块)。
- 转到“工具”->“参考”并选中生成的类型库的复选框(参见上文 ‡)
- 添加以下代码
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
当执行到达停止时,人们可以检查数组内容是否成功编组。
早期绑定成功!
后期绑定 Excel VBA 客户端代码
- 我还使用 Excel VBA 中的后期绑定来摆脱部署麻烦,我还可以热交换 dll,即安装新的 COM 服务器而不关闭 Excel(我应该在 SO 上发布该技巧)。
- 从 (1) 开始,我将使用相同的 Excel VBA 工作簿作为测试后期绑定的平台,并相应地更改声明。
-
在同一模块中添加以下代码
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 后期绑定模式下不起作用。
后期绑定失败!
解决方法
-
因此,在调查过程中,我编写了第二个返回类型为 Object[] 的方法,最初这不起作用,因为生成的 idl 作为星号丢弃。 idl 来自
// Return type Apples[] works for early binding but not late binding
// works
HRESULT enumerateApples([out, retval] SAFEARRAY(IApples*)* pRetVal);
to
// 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
-
使用 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);
这适用于后期装订,但不适用于耳环装订!啊啊啊!
Summary
我使用了两种方法,一种用于早期绑定,一种用于后期绑定,这并不令人满意,因为这意味着每种方法都要加倍。
如何让一种方法同时适用于早期和后期绑定?
我对此进行了一些测试,将返回值编组到Variant
,然后转储返回的 VARIANT 结构的内存以查看 VARTYPE 是什么。对于早期绑定调用,它返回一个Variant
VARTYPE 为VT_ARRAY & VT_DISPATCH
。对于后期绑定调用,它返回的 VARTYPE 为VT_ARRAY & VT_UNKNOWN
。苹果should已经被定义为实施IDispatch
在 tlb 中,但由于某种我无法理解的原因,VBA 很难处理一系列IUnknown
来自后期绑定呼叫。解决方法是将返回类型更改为object[]
在 C# 方面...
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();
}
...并将它们拉入Variant
在 VBA 方面:
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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)