如何使 C# COM class 支持 VB6 的参数化属性

How to make C# COM class support parameterized properties from VB6

我已经对这个问题进行了大量研究,虽然我发现了很多关于 C# 和参数化属性的信息(使用索引器是唯一的方法),但我还没有找到问题的实际答案。

首先,我想做的是:

我有一个用 VB6 编写的现有 COM DLL,我正在尝试创建一个使用类似接口的 C# DLL。我说类似是因为 VB6 DLL 仅用于后期绑定,因此它不必为调用具有相同的 GUID(也就是说,它不必是 "binary compatible")。这个 VB6 COM DLL 在几个地方使用了参数化属性,我知道 C# 不支持这些属性。

使用具有参数化属性的 VB6 COM DLL 时,C# 中的引用将以 "get_PropName" 和 "set_PropName" 形式的方法访问它们。但是,我的方向相反:我不是试图在 C# 中访问 VB6 DLL,而是试图使 C# COM DLL 与 VB6 DLL 兼容。

所以,问题是:如何使 C# COM DLL 中的 getter 和 setter 方法在使用时显示为单个参数化 属性通过 VB6?

例如VB6属性定义如下:

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# 中的等价物是这样的:

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 did not return an object".

我几乎肯定应该有办法做到这一点,但我似乎找不到它。有人知道怎么做吗?

更新:

我采纳了 Ben 的建议并添加了一个访问器 class 看看这是否可以解决我的问题。但是,现在我 运行 进入另一个问题...

首先,这是我正在使用的 COM 接口:

[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; }

}

这是访问器 class:

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

实现如下:

[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 中进行测试的方法,但出现错误:

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)

但是,这有效:

Set sys = CreateObject("MyApp.System")

Set obj = sys.RefInfo
s = obj("MyTestProperty", 0, 0)

它似乎试图在 属性 本身上使用参数并出现错误,因为 属性 没有参数。如果我在它自己的对象变量中引用 RefInfo 属性,那么它会正确应用索引器属性。

关于如何安排它以便它知道将参数应用于访问器的索引器而不是尝试将其应用于 属性 的任何想法?

此外,我该如何 +1?这是我在 Whosebug 上的第一个问题:-)

更新#2:

只是为了看看它是如何工作的,我也尝试了默认值方法。这是访问器现在的样子:

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 尝试将值设置为方法,而不是 returned 对象的默认值 属性,或类似的东西。如果我将“.Value”添加到设置行,它就可以正常工作。

更新#3:成功!

我终于搞定了。但是,有几件事需要注意。

首先,访问器的默认值必须return一个缩放器,而不是一个对象,像这样:

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

其次,使用accessor时,需要让return类型成为对象:

    public object RefInfo(string PropertyName, int index = 0, int subindex = 0)
    {
        return new RefInfoAccessor(this,PropertyName,index,subindex);
    }

这将使 C# 满意,因为默认值是 COM 事物 (dispid 0) 而不是 C# 事物,因此 C# 期望 RefInfoAccessor 是 returned,而不是字符串。由于可以将 RefInfoAccessor 强制转换为对象,因此不会出现编译错误。

在 VB6 中使用时,以下所有内容现在都可以使用:

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

非常感谢 Ben 在这方面的帮助!

C# 可以执行索引属性,但这些必须使用具有索引器的助手 class 来实现。此方法适用于早期绑定 VB 但不适用于后期绑定 VB:

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"]);
    }

}

这里有教程:

后期绑定VB解决方法

这是一种解决方法,它利用了关于 VB 的两个事实。一是数组中的索引运算符与函数调用运算符相同——圆括号(parens)。另一个是 VB 将允许我们省略默认的名称 属性.

只读属性

如果 属性 是 get-only 的,你不需要为此操心。只需使用一个函数,这将与后期绑定代码的数组访问行为相同。

读写属性

利用上面的两个事实,我们可以看出它们在VB

中是等价的
// 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

这意味着不要这样做:

//Property => Object with Indexer
// C# syntax
obj.PropName[index1];

我们可以做到:

// C# syntax
obj.PropName(index1).Value

下面是示例代码,只有一个参数。

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

限制

限制是要工作,这取决于在结果被强制为非对象类型的上下文中进行的调用。例如

varName = obj.PropName(99)

由于没有使用 Set 关键字,VB 知道必须获取默认的 属性 才能在此处使用。

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

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

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

您寻找的这个功能通常称为 "indexed properties"。 VB6使用的flavor是COM接口支持的flavor

此 IDL 片段类似于 VB6 生成的片段,并显示了幕后发生的事情:

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,当然是非索引的常规属性.

遗憾的是,C# 对 COM 样式索引属性的支持非常有限。

C# 4.0 支持使用 COM 对象(在其他地方编写),这些对象实现了具有索引属性的 COM 接口。这是为了提高与 COM 自动化服务器(如 Excel)的互操作性。但是,它不支持声明 这样的接口,也不支持创建 实现这样的 COM 接口的对象,即使在其他地方合法声明也是如此。

告诉您如何在 C# 中创建索引属性 - 或者至少是在 C# 代码中产生等效语法的内容。如果您在编写 C# 代码时只需要语法风格,那效果很好。但当然它不是 COM 风格的索引 属性.

这是 C# 语言的限制,不是 .NET 平台的限制。 VB.NET 确实支持 COM 索引属性,因为他们有取代 VB6 的授权,因此需要付出更多努力。

如果您确实需要 COM 索引属性,您可以考虑在 VB.NET 中编写对象的 COM 版本,并让该对象转发对 C# 实现的调用。对我来说,这听起来像是很多工作。或者将所有代码移植到 VB.NET。这真的取决于你有多想要它。

参考资料

But this feature is available only for COM interop; you cannot create your own indexed properties in C# 4.0.

  • Why C# doesn't implement indexed properties?

    • Eric Lippert answers
  • VB.NET 中的 COM 样式索引属性:Property with parameter