在 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?
问题很长,所以我会用要点来格式化以便于讨论
简介
- 我正在编写 C# COM 服务器。
- COM 服务器用于 Excel VBA 早期绑定和后期绑定模式。
- 我的绊脚石是如何 return 实例化 classes 的 SAFEARRAY,它可以在早期和晚期绑定模式下工作;我收到错误。
- 我为此做了很多工作(一整天):
- 我已经完成了一些诊断并设置了调试器来阐明我遇到的错误。
- 我已经进行了相当详尽的谷歌搜索。
- 我发现了一些不太令人满意的解决方法。
- 我现在真的很困惑,正在寻找 COM Interop 专家来帮助我找到一个好的解决方案。
设置项目类型和项目属性
- 创建一个新的 C# Class 库项目。
- 我将我的文件命名为 LateBoundSafeArraysProblem,并将源文件重命名为 LateBoundSafeArraysProblem.cs。
- 在AssemblyInfo.cs中将第20行修改为ComVisible(true),这样可见性是通用的(仍然需要public关键字)。
- 设置项目属性:
- 设置构建选项,在 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 服务器源代码
- 风格选择和决定
- 我是一名优秀的 COM 公民,将接口定义与 class 定义区分开来。
- 我在 class 上使用 [ClassInterface(ClassInterfaceType.None)] 和 [ComDefaultInterface(typeof(
))] 属性来实现这种清晰的划分。
- 因为客户端是 Excel VBA 我们需要坚持自动化兼容类型,因此 SAFEARRAY
- 两个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();
}
}
这适用于早期绑定,但不适用于后期绑定。
- 构建项目,应该有一个dll和一个tlb。
- 一个输出将是LateBoundSafeArraysProblem.dll
- 也会输出一个类型库,LateBoundSafeArraysProblem.tlb‡
早期绑定ExcelVB客户端代码
- 打开调试启动选项中指定的工作簿(见上文†)
- 添加基本标准模块(不是 class 模块)。
- 转到“工具”->“参考”并选中生成的类型库的复选框(见上 ‡)
- 添加以下代码
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客户端代码
- 我还在 Excel VBA 中使用后期绑定来避免部署问题,我也可以热交换 dll,即安装新的 COM 服务器而不关闭 Excel(我应该 post 那个技巧。
- 从 (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 后期绑定模式下不起作用。
后期绑定失败!
解决方法
所以在调查期间我写了第二个方法 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
使用 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 一个 Variant
和 VT_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
问题很长,所以我会用要点来格式化以便于讨论
简介
- 我正在编写 C# COM 服务器。
- COM 服务器用于 Excel VBA 早期绑定和后期绑定模式。
- 我的绊脚石是如何 return 实例化 classes 的 SAFEARRAY,它可以在早期和晚期绑定模式下工作;我收到错误。
- 我为此做了很多工作(一整天):
- 我已经完成了一些诊断并设置了调试器来阐明我遇到的错误。
- 我已经进行了相当详尽的谷歌搜索。
- 我发现了一些不太令人满意的解决方法。
- 我现在真的很困惑,正在寻找 COM Interop 专家来帮助我找到一个好的解决方案。
设置项目类型和项目属性
- 创建一个新的 C# Class 库项目。
- 我将我的文件命名为 LateBoundSafeArraysProblem,并将源文件重命名为 LateBoundSafeArraysProblem.cs。
- 在AssemblyInfo.cs中将第20行修改为ComVisible(true),这样可见性是通用的(仍然需要public关键字)。
- 设置项目属性:
- 设置构建选项,在 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 服务器源代码
- 风格选择和决定
- 我是一名优秀的 COM 公民,将接口定义与 class 定义区分开来。
- 我在 class 上使用 [ClassInterface(ClassInterfaceType.None)] 和 [ComDefaultInterface(typeof(
))] 属性来实现这种清晰的划分。
- 我在 class 上使用 [ClassInterface(ClassInterfaceType.None)] 和 [ComDefaultInterface(typeof(
- 因为客户端是 Excel VBA 我们需要坚持自动化兼容类型,因此 SAFEARRAY
- 我是一名优秀的 COM 公民,将接口定义与 class 定义区分开来。
- 两个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();
}
}
这适用于早期绑定,但不适用于后期绑定。
- 构建项目,应该有一个dll和一个tlb。
- 一个输出将是LateBoundSafeArraysProblem.dll
- 也会输出一个类型库,LateBoundSafeArraysProblem.tlb‡
早期绑定ExcelVB客户端代码
- 打开调试启动选项中指定的工作簿(见上文†)
- 添加基本标准模块(不是 class 模块)。
- 转到“工具”->“参考”并选中生成的类型库的复选框(见上 ‡)
- 添加以下代码
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客户端代码
- 我还在 Excel VBA 中使用后期绑定来避免部署问题,我也可以热交换 dll,即安装新的 COM 服务器而不关闭 Excel(我应该 post 那个技巧。
- 从 (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 后期绑定模式下不起作用。 后期绑定失败!
解决方法
所以在调查期间我写了第二个方法 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
使用 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 一个 Variant
和 VT_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