How to make C# COM class support parameterized properties from VB6
我对这个问题进行了相当多的研究,虽然我发现了很多关于C和参数化属性的内容(使用索引器是唯一的方法),但我还没有找到我的问题的实际答案。
首先,我要做的是:
我有一个用vb6编写的现有COM DLL,我正在尝试创建一个使用类似接口的C DLL。我说类似,因为vb6 dll只用于后期绑定,所以它不必具有相同的调用guid(也就是说,它不必是"二进制兼容")。这个vb6 COM DLL在一些地方使用参数化属性,我知道C不支持这些属性。
当使用带有参数化属性的VB6 COM DLL时,C中的引用将以"get_propname"和"set_propname"的形式访问它们。但是,我的方向是相反的:我不是要访问C中的vb6 dll,而是要使C com dll与vb6 dll兼容。
因此,问题是:当vb6使用时,如何在C com dll中生成作为单个参数化属性出现的getter和setter方法?
例如,假设vb6属性定义如下:
1 2 3 4 5 | Public Property Get MyProperty(Param1 As String, Param2 as String) As String End Property Public Property Let MyProperty(Param1 As String, Param2 As String, NewValue As String) End Property |
C中的等价物如下所示:
1 2 3 4 5 6 7 | public string get_MyProperty(string Param1, string Param2) { } public void set_MyProperty(string Param1, string Param2, ref string NewValue) { } |
号
那么,当vb6使用这些c方法时,我如何使它们看起来像(和函数一样)一个参数化属性?
我尝试创建两个方法,一个称为"set_propname",另一个称为"get_propname",希望它能发现,当vb6使用时,它们应该是一个单一的参数化属性,但这不起作用;它们看起来是来自vb6的两个不同的方法调用。
我认为可能需要在C中对它们应用一些属性,以便在COM和VB6中将它们视为单个参数化属性,但我找不到任何合适的属性。
我还尝试重载这些方法,删除"get"和"set",希望它将它们视为单个属性,但这也不起作用。该错误在VB6中生成:"property let procedure not defined and property get procedure not return an object"。
我几乎肯定会有办法做到这一点,但我似乎找不到。有人知道怎么做吗?
更新:
我接受了本的建议,并添加了一个访问器类,看看这是否能解决我的问题。不过,现在我又遇到了另一个问题…
首先,这里是我使用的COM接口:
1 2 3 4 5 6 7 8 9 10 11 | [ComVisible(true), Guid("94EC4909-5C60-4DF8-99AD-FEBC9208CE76"), InterfaceType(ComInterfaceType.InterfaceIsDual)] public interface ISystem { object get_RefInfo(string PropertyName, int index = 0, int subindex = 0); void set_RefInfo(string PropertyName, int index = 0, int subindex = 0, object theValue); RefInfoAccessor RefInfo { get; } } |
这是访问器类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class RefInfoAccessor { readonly ISystem mySys; public RefInfoAccessor(ISystem sys) { this.mySys = sys; } public object this[string PropertyName, int index = 0, int subindex = 0] { get { return mySys.get_RefInfo(PropertyName, index, subindex); } set { mySys.set_RefInfo(PropertyName, index, subindex, value); } } } |
。
实现方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] [Guid(MySystem.ClassId)] [ProgId("MyApp.System")] public class MySystem : ISystem { internal const string ClassId ="60A84737-8E96-4DF3-A052-7CEB855EBEC8"; public MySystem() { _RefInfo = new RefInfoAccessor(this); } public object get_RefInfo(string PropertyName, int index = 0, int subindex = 0) { // External code does the actual work return"Test"; } public void set_RefInfo(string PropertyName, int index = 0, int subindex = 0, object theValue) { // External code does the actual work } private RefInfoAccessor _RefInfo; public RefInfoAccessor RefInfo { get { return _RefInfo; } } } |
下面是我在vb6中测试的操作,但我得到一个错误:
1 2 3 4 5 | Set sys = CreateObject("MyApp.System") ' The following statement gets this error: '"Wrong number of arguments or invalid property assignment" s = sys.RefInfo("MyTestProperty", 0, 0) |
。
但是,这是可行的:
1 2 3 4 | Set sys = CreateObject("MyApp.System") Set obj = sys.RefInfo s = obj("MyTestProperty", 0, 0) |
。
似乎它正在尝试使用属性本身的参数,但由于该属性没有参数,因此会出现错误。如果我在它自己的对象变量中引用了refinfo属性,那么它将正确应用索引器属性。
对于如何安排这样做,以便它知道如何将参数应用于访问器的索引器,而不是尝试将其应用于属性,有什么想法吗?
另外,如何执行A+1?这是我关于stackoverflow的第一个问题:-)
更新2:
为了了解它的工作原理,我还尝试了默认值方法。下面是访问器的外观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class RefInfoAccessor { readonly ISystem mySys; private int _index; private int _subindex; private string _propertyName; public RefInfoAccessor(ISystem sys, string propertyName, int index, int subindex) { this.mySys = sys; this._index = index; this._subindex = subindex; this._propertyName = propertyName; } [DispId(0)] public object Value { get { return mySys.get_RefInfo(_propertyName, _index, _subindex); } set { mySys.set_RefInfo(_propertyName, _index, _subindex, value); } } } |
这对"GET"很有用。但是,当我尝试设置值时,.NET会弹出以下错误:
Managed Debugging Assistant 'FatalExecutionEngineError' has detected a
problem in 'blahblah.exe'.Additional information: The runtime has encountered a fatal error. The
address of the error was at 0x734a60f4, on thread 0x1694. The error
code is 0xc0000005. This error may be a bug in the CLR or in the
unsafe or non-verifiable portions of user code. Common sources of this
bug include user marshaling errors for COM-interop or PInvoke, which
may corrupt the stack.
号
我假设问题是.NET试图将值设置为方法,而不是返回对象的默认属性,或者类似的东西。如果我在设定行中添加".value",它就可以正常工作。
更新3:成功!
我终于把它做好了。不过,还有一些事情要找。
首先,访问器的默认值必须返回一个定标器,而不是一个对象,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class RefInfoAccessor { readonly ISystem mySys; private int _index; private int _subindex; private string _propertyName; public RefInfoAccessor(ISystem sys, string propertyName, int index, int subindex) { this.mySys = sys; this._index = index; this._subindex = subindex; this._propertyName = propertyName; } [DispId(0)] public string Value // <== Can't be"object" { get { return mySys.get_RefInfo(_propertyName, _index, _subindex).ToString(); } set { mySys.set_RefInfo(_propertyName, _index, _subindex, value); } } } |
。
第二,使用访问器时,需要将返回类型设置为对象:
1 2 3 4 | public object RefInfo(string PropertyName, int index = 0, int subindex = 0) { return new RefInfoAccessor(this,PropertyName,index,subindex); } |
这将使C高兴,因为默认值是COM对象(dispid 0)而不是C对象,所以C希望返回refinfo访问器,而不是字符串。由于refinfo访问器可以强制转换为对象,因此没有编译器错误。
当在vb6中使用时,以下内容将全部工作:
1 2 3 4 5 6 | s = sys.RefInfo("MyProperty", 0, 0) Debug.Print s sys.RefInfo("MyProperty", 0, 0) ="Test" ' This now works! s = sys.RefInfo("MyProperty", 0) Debug.Print s |
。
非常感谢本在这方面的帮助!
您寻找的这个特性通常被称为"索引属性"。VB6使用的风格是COM接口支持的风格。
这个idl片段与vb6生成的片段类似,它显示了引擎盖下的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | interface ISomething : IDispatch { [id(0x68030001), propget] HRESULT IndexedProp( [in, out] BSTR* a, // Index 1 [in, out] BSTR* b, // Index 2 [out, retval] BSTR* ); [id(0x68030001), propput] HRESULT IndexedProp( [in, out] BSTR* a, // Index 1 [in, out] BSTR* b, // Index 2 [in, out] BSTR* ); [id(0x68030000), propget] HRESULT PlainProp( [out, retval] BSTR* ); [id(0x68030000), propput] HRESULT PlainProp( [in, out] BSTR* ); }; |
。
不幸的是,C_对COM样式索引属性的支持非常有限。
C 4.0支持使用实现带索引属性的COM接口的COM对象(在其他地方编写)。这是为了提高与Excel等COM自动化服务器的互操作性而增加的。但是,它不支持声明这样的接口,也不支持创建实现这样的COM接口的对象,即使在其他地方合法声明。
Ben的答案告诉您如何在C中创建索引属性,或者至少是在C代码中生成等效语法的内容。如果您只想在编写C代码时使用语法风格,那就太好了。当然,它不是一个COM风格的索引属性。
这是C语言的一个限制,而不是.NET平台。vb.net确实支持COM索引的属性,因为它们必须替换vb6,因此需要付出更多的努力。
如果您真的想要COM索引的属性,可以考虑在vb.net中编写对象的COM版本,并让该对象将调用转发到C实现。对我来说,这听起来是一项很大的工作。或者将所有代码移植到vb.net。这真的取决于你有多想要它。
工具书类- C团队博客:关于C 4.0中新功能的常见问题解答:
But this feature is available only for COM interop; you cannot create your own indexed properties in C# 4.0.
号
为什么C不实现索引属性?
- 埃里克·利珀特回答
VB.NET中的COM样式索引属性:带参数的属性
C可以执行索引属性,但必须使用具有索引器的助手类来实现这些属性。此方法适用于早期绑定的vb,但不适用于后期绑定的vb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | using System; class MyClass { protected string get_MyProperty(string Param1, string Param2) { return"foo:" + Param1 +"; bar:" + Param2; } protected void set_MyProperty(string Param1, string Param2, string NewValue) { // nop } // Helper class public class MyPropertyAccessor { readonly MyClass myclass; internal MyPropertyAccessor(MyClass m){ myclass = m; } public string this [string param1, string param2]{ get { return myclass.get_MyProperty(param1, param2); } set { myclass.set_MyProperty(param1, param2, value); } } } public readonly MyPropertyAccessor MyProperty; public MyClass(){ MyProperty = new MyPropertyAccessor(this); } } public class Program { public static void Main() { Console.WriteLine("Hello World"); var mc = new MyClass(); Console.WriteLine(mc.MyProperty["a","b"]); } } |
这里有一个教程:
- https://msdn.microsoft.com/en-us/library/aa288464(v=vs.71).aspx
后期绑定的VB解决方案
这是一个解决方法,它利用了关于VB的两个事实。一种是数组中的索引运算符与函数调用运算符圆括号(parens)相同。另一个原因是vb允许我们省略默认属性的名称。
只读属性如果该属性是"只获取",则无需为此费心。只需使用一个函数,它的行为将与对后期绑定代码的数组访问相同。
读写属性使用上面的两个事实,我们可以看到它们在VB中是等价的。
1 2 3 4 5 6 7 8 | // VB Syntax: PropName could either be an indexed property or a function varName = obj.PropName(index1).Value obj.PropName(index1).Value = varName // But if Value is the default property of obj.PropName(index1) // this is equivalent: varName = obj.PropName(index1) obj.PropName(index1) = varName |
号
这意味着,不要这样做:
1 2 3 | //Property => Object with Indexer // C# syntax obj.PropName[index1]; |
我们可以这样做:
1 2 | // C# syntax obj.PropName(index1).Value |
。
这里是示例代码,有一个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class HasIndexedProperty { protected string get_PropertyName(int index1){ // replace with your own implementation return string.Format("PropertyName: {0}", index1); } protected void set_PropertyName(int index1, string v){ // this is an example - put your implementation here } // This line provides the indexed property name as a function. public string PropertyName(int index1){ return new HasIndexedProperty_PropertyName(this, index1); } public class HasIndexedProperty_PropertyName{ protected HasIndexedProperty _owner; protected int _index1; internal HasIndexedProperty_PropertyName( HasIndexedProperty owner, int index1){ _owner = owner; _index1 = index1; } // This line makes the property Value the default [DispId(0)] public string Value{ get { return _owner.get_PropertyName(_index1); } set { _owner.set_PropertyName(_index1, value); } } } } |
限制
限制条件是要工作,这取决于在将结果强制为非对象类型的上下文中进行的调用。例如
1 | varName = obj.PropName(99) |
。
由于没有使用
同样,当传递给以字符串为例的函数时,这将起作用。在内部,将调用
当直接作为参数传递给以变量为参数的函数时,可能会发生此问题。在这种情况下,访问器对象将被传递。一旦对象在非对象上下文中使用(例如,赋值或转换为字符串),将获取默认属性。但是,这将是转换时的值,而不是最初访问时的值。这可能是问题,也可能不是问题。
但是,这个问题可以通过让访问器对象缓存它返回的值来解决,以确保它是创建访问器时的值。