如何使C#COM类支持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兼容.

因此,问题是:如何在V#6使用C#COM DLL中的getter和setter方法时显示为单个参数化属性?

例如,说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中产生的错误:“未定义属性让过程,属性获取过程未返回对象”.

我几乎肯定应该有一种方法可以做到这一点,但我似乎还是找不到.有谁知道如何做到这一点?

更新:

我听了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?这是我对*的第一个问题:-)

更新#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跳出并显示以下错误:

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:成功!

我终于完成了这项工作.但是,有一些要寻找的东西.

首先,访问器的默认值必须返回缩放器,而不是对象,如下所示:

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

其次,使用访问器时,需要使返回类型成为对象:

    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在这方面的帮助!

解决方法:

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

}

这里有一个教程:

> https://msdn.microsoft.com/en-us/library/aa288464(v=vs.71).aspx

后期绑定VB解决方法

这是一种变通办法,它利用了有关VB的两个事实.一是数组索引运算符与函数调用运算符相同-圆括号(括号).另一个是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作为参数的函数时,可能会出现问题.在这种情况下,访问器对象将被传递.一旦在非对象上下文中使用对象(例如,分配或转换为字符串),就会获取默认属性.但是,这将是转换时的值,而不是原始访问时的值.这可能是问题,也可能不是问题.

但是,可以通过使访问器对象缓存其返回的值以确保它是创建访问器时的值来解决此问题.

上一篇:这个正则表达式(VBScript / JavaScript风格)有什么问题?


下一篇:[Kotlin] 用Kotlin和Swing实现VB6中的MsgBox