关于.net:如何使C#COM类支持VB6中的参数化属性

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* );
};

IndexedProp是一个字符串属性,它采用两个字符串参数作为索引。与PlainProp相比,EDOCX1当然是一种无索引的常规属性。

不幸的是,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)

由于没有使用Set关键字,所以vb知道必须获取默认属性才能在这里使用。

同样,当传递给以字符串为例的函数时,这将起作用。在内部,将调用VariantChangeType将对象转换为正确的类型,如果强制转换为非对象,将访问默认属性。

当直接作为参数传递给以变量为参数的函数时,可能会发生此问题。在这种情况下,访问器对象将被传递。一旦对象在非对象上下文中使用(例如,赋值或转换为字符串),将获取默认属性。但是,这将是转换时的值,而不是最初访问时的值。这可能是问题,也可能不是问题。

但是,这个问题可以通过让访问器对象缓存它返回的值来解决,以确保它是创建访问器时的值。