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

3
我已经对这个问题进行了相当多的研究,虽然我找到了很多关于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兼容。
因此,问题是:如何在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)
{
}

那么,我应该如何将这些C#方法呈现为(并且在VB6中使用时能够)一个具有参数的属性?
我尝试创建两个方法,一个名为“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的建议,并添加了一个访问器类来解决我的问题。但是,现在我遇到了另一个问题...首先,这是我正在使用的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; }

}

这是访问器类:

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操作?这是我在StackOverflow上的第一个问题:-)

更新#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);
        }
    }
}

这个方法在“获取”时很好用。然而,当我尝试设置值时,.NET会出现以下错误:

托管调试助手“FatalExecutionEngineError”在“blahblah.exe”中检测到问题。

其他信息:运行时遇到了致命错误。错误地址为0x734a60f4,在线程0x1694上。错误代码为0xc0000005。这个错误可能是CLR或用户代码中的不安全或不可验证部分中的错误。此错误的常见来源包括COM-Interop或PInvoke的用户编组错误,这可能会破坏堆栈。

我认为问题在于.NET尝试将值设置给方法,而不是返回对象的默认属性,或类似的东西。如果在设置行中添加“.Value”,它就可以正常工作了。
更新#3:成功!
我终于让它工作了。但是需要注意几件事情。
首先,访问器的默认值必须返回标量,而不是对象,如下所示:
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 时,您需要将返回类型设置为对象:
    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而不是字符串。由于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在这方面的帮助!
2个回答

2

C#可以实现索引属性,但必须使用具有索引器的辅助类来实现。这种方法适用于早期绑定的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 允许我们省略默认属性的名称。

只读属性

如果属性是只读的,您不需要担心这个问题。只需使用一个函数,这将对后期绑定代码的数组访问行为相同。

读写属性

使用上述两个特点,我们可以看到这些在 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作为参数的函数时出现。在这种情况下,将传递访问器对象。只要对象在非对象上下文中使用(例如赋值或转换为字符串),默认属性就会被提取。然而,这将是在转换时的值,而不是最初访问时的值。这可能是一个问题,也可能不是一个问题。
但是,可以通过让访问器对象缓存其返回的值来解决此问题,以确保它是创建访问器时的值。

你说得对,这个方法不能在后期绑定的VB中使用。我有一个替代方案,很快就会发布。 - Ben
好的,我接下来会添加一个完全通用的版本(对象类型的“Value”,因此只适用于后期绑定的代码)。您可以轻松地扩展它,以便为您需要的类型添加类型化版本。 - Ben
再次感谢你,Ben。这非常有帮助。然而,我遇到了另一个问题。我在原来的问题中更新了这个问题,以及我如何实现这些更改的更多细节。 - Paul Parkhurst
我在“解决方法”部分漏掉了一行,这一行展示了如何将其声明为属性。 - Ben
1
@SomeDude 为了明确起见,上述方法不适用于“后期绑定”属性,而VBScript是后期绑定的。 - Ben
显示剩余9条评论

2

您所需要的功能通常称为“索引属性”。VB6使用的风格是COM接口支持的风格。

这个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 Automation服务器(如Excel)的互操作性而添加的。然而,它不支持声明这样的接口,或者创建即使在其他地方合法声明也实现了这样的COM接口的对象。 Ben's answer告诉你如何在C#中创建索引属性,或者至少得到等效的C#代码语法。如果你只想在编写C#代码时获得语法风格,那么它非常好用。但是当然它不是一个COM风格的索引属性。
这是C#语言的一种限制,而不是.NET平台的限制。VB.NET支持COM索引属性,因为他们需要替换VB6并且需要额外努力。
如果你真的想要COM索引属性,你可以考虑在VB.NET中编写你的对象的COM版本,并让该对象将调用转发到你的C#实现。这对我来说听起来很费力。或将所有代码移植到VB.NET。这真的取决于你有多渴望它。

参考资料

但是此功能仅适用于COM互操作性;您无法在C# 4.0中创建自己的索引属性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接