简体   繁体   English

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

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

I've researched this question quite a bit, and while I've found a lot about C# and parameterized properties (using an indexer is the only way), I haven't found an actual answer to my question. 我已经对这个问题进行了相当多的研究,虽然我发现了很多关于C#和参数化的属性(使用索引器是唯一的方法),但我还没有找到问题的实际答案。

First, what I'm trying to do: 首先,我正在尝试做什么:

I have an existing COM DLL written in VB6 and I'm trying to create a C# DLL that uses a similar interface. 我有一个用VB6编写的现有COM DLL,我正在尝试创建一个使用类似接口的C#DLL。 I say similar because the VB6 DLL is only used with late binding, so it doesn't have to have the same GUIDs for the calls (that is, it doesn't have to be "binary compatible"). 我之所以说类似,因为VB6 DLL仅用于后期绑定,因此它不必具有相同的GUID用于调用(也就是说,它不必是“二进制兼容的”)。 This VB6 COM DLL uses parameterized properties in a few places, which I know aren't supported by C#. 这个VB6 COM DLL在一些地方使用参数化属性,我知道C#不支持这些属性。

When using a VB6 COM DLL with parameterized properties, the reference in C# will access them as methods in the form "get_PropName" and "set_PropName". 当使用带参数化属性的VB6 COM DLL时,C#中的引用将以“get_PropName”和“set_PropName”形式的方式访问它们。 However, I'm going in the opposite direction: I'm not trying to access the VB6 DLL in C#, I'm trying to make a C# COM DLL compatible with a VB6 DLL. 但是,我正朝着相反的方向前进:我没有尝试在C#中访问VB6 DLL,我正在尝试使C#COM DLL与VB6 DLL兼容。

So, the question is: How do I make getter and setter methods in a C# COM DLL that appear as a single parameterized property when used by VB6? 因此,问题是: 如何在C6 COM DLL中创建getter和setter方法,这些方法在VB6使用时显示为单个参数化属性?

For example, say the VB6 property is defined as follows: 例如,假设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

The equivalent in C# would be something like this: C#中的等价物将是这样的:

public string get_MyProperty(string Param1, string Param2)
{
}

public void set_MyProperty(string Param1, string Param2, ref string NewValue)
{
}

So, how would I make those C# methods look like (and function like) a single parameterized property when used by VB6? 那么,当VB6使用时,我如何使这些C#方法看起来像(和函数一样)单个参数化属性?

I tried creating two methods, one called "set_PropName" and the other "get_PropName", hoping it would figure out that they're supposed to be a single parameterized property when used by VB6, but that didn't work; 我尝试创建两个方法,一个名为“set_PropName”,另一个名为“get_PropName”,希望它能够确定它们在VB6使用时应该是一个参数化属性,但是没有用; they appeared as two different method calls from VB6. 它们出现在VB6的两个不同方法调用中。

I thought maybe some attributes needed to be applied to them in C# so that they'd be seen as a single parameterized property in COM and VB6, but I couldn't find any that seemed appropriate. 我想可能需要在C#中应用一些属性,以便它们在COM和VB6中被视为单个参数化属性,但我找不到任何合适的属性。

I also tried overloading the methods, removing "get_" and "set_", hoping it would see them as a single property, but that didn't work either. 我也尝试重载方法,删除“get_”和“set_”,希望它将它们视为单个属性,但这也不起作用。 That one generated this error in VB6: "Property let procedure not defined and property get procedure did not return an object" . 那个在VB6中生成了这个错误: “属性让过程没有定义,属性获取过程没有返回一个对象”

I'm almost positive that there should be a way of doing this, but I just can't seem to find it. 我几乎肯定应该有这样做的方法,但我似乎无法找到它。 Does anyone know how to do this? 有谁知道如何做到这一点?

Update: 更新:

I took Ben's advice and added an accessor class to see if this could solve my problem. 我接受了Ben的建议,并添加了一个访问者类,看看这是否可以解决我的问题。 However, now I'm running into another issue... 但是,现在我遇到了另一个问题......

First, here's the COM interface I'm using: 首先,这是我正在使用的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; }

}

Here's the accessor 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);
        }
    }
}

Here's the implementation: 这是实施:

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

}

Here's what I'm doing to test this in VB6, but I get an error: 这是我在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)

However, this works: 但是,这有效:

Set sys = CreateObject("MyApp.System")

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

It appears that it's trying to use the parameters on the property itself and getting an error because the property has no parameters. 它似乎正在尝试使用属性本身的参数并获取错误,因为该属性没有参数。 If I reference the RefInfo property in its own object variable, then it applies the indexer properties correctly. 如果我在自己的对象变量中引用RefInfo属性,那么它会正确应用索引器属性。

Any ideas on how to arrange this so that it knows to apply the parameters to the accessor's indexer, rather than attempting to apply it to the property? 有关如何安排这一点的任何想法,以便它知道将参数应用于访问者的索引器,而不是试图将其应用于属性?

Also, how do I do a +1? 另外,我如何做+1? This is my first question on StackOverflow :-) 这是我在StackOverflow上的第一个问题:-)

Update #2: 更新#2:

Just to see how it would work, I also tried the default value approach. 只是为了看看它是如何工作的,我也尝试了默认值方法。 Here's how the accessor looks now: 以下是访问者现在的样子:

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

This works great for a "get". 这对于“获取”非常有用。 However, when I try setting the value, .NET flips out with the following error: 但是,当我尝试设置该值时,.NET会因以下错误而退出:

Managed Debugging Assistant 'FatalExecutionEngineError' has detected a problem in 'blahblah.exe'. 托管调试助手'FatalExecutionEngineError'检测到'blahblah.exe'中存在问题。

Additional information: The runtime has encountered a fatal error. 附加信息:运行时遇到致命错误。 The address of the error was at 0x734a60f4, on thread 0x1694. 错误的地址是0x734a60f4,在线程0x1694上。 The error code is 0xc0000005. 错误代码是0xc0000005。 This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code. 此错误可能是CLR中的错误,也可能是用户代码的不安全或不可验证部分中的错误。 Common sources of this bug include user marshaling errors for COM-interop or PInvoke, which may corrupt the stack. 此错误的常见来源包括COM-interop或PInvoke的用户编组错误,这可能会破坏堆栈。

I'm assuming the problem is that .NET tried setting the value to the method, rather to the default property of the returned object, or something similar. 我假设问题是.NET尝试将值设置为方法,而不是返回对象的默认属性,或类似的东西。 If I add ".Value" to the set line, it works fine. 如果我将“.Value”添加到设置行,它可以正常工作。

Update #3: Success! 更新#3:成功!

I finally got this working. 我终于搞定了这个。 There's a few things to look for, however. 但是,有一些事情需要寻找。

First, the default value of the accessor must return a scaler, not an object, like so: 首先,访问器的默认值必须返回一个缩放器,而不是一个对象,如下所示:

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

Second, when using the accessor, you need to make the return type an object: 其次,在使用访问器时,需要使返回类型成为对象:

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

This will make C# happy, since the default value is a COM thing (dispid 0) and not a C# thing, so C# expects a RefInfoAccessor to be returned, not a string. 这将使C#开心,因为默认值是COM事物(dispid 0)而不是C#事物,所以C#期望返回RefInfoAccessor,而不是字符串。 Since RefInfoAccessor can be coerced into an object, no compiler error. 由于RefInfoAccessor可以强制转换为对象,因此没有编译器错误。

When used in VB6, the following will now all work: 在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

Many thanks to Ben for his help on this! 非常感谢Ben对此的帮助!

C# can do indexed properties, but these must be implemented using a helper class which has an indexer. C#可以执行索引属性,但这些必须使用具有索引器的辅助类来实现。 This method will work with early-bound VB but not with late-bound VB: 这个方法适用于早期绑定的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"]);
    }

}

There is a tutorial here: 这里有一个教程:

Late-bound VB Workaround 后期VB解决方法

This is a workaround which takes advantage of two facts about VB. 这是一个利用有关VB的两个事实的解决方法。 One is that in the array index operator is the same as the function call operator - round brackets (parens). 一个是在数组中索引运算符与函数调用运算符相同 - 圆括号(parens)。 The other is that VB will allow us to omit the name of the default property. 另一个是VB将允许我们省略默认属性的名称。

Read-only Properties 只读属性

If the property is get-only, you don't need to bother with this. 如果属性是get-only,则无需为此烦恼。 Just use a function, and this will behave the same as array access for late-bound code. 只需使用一个函数,这与后期绑定代码的数组访问行为相同。

Read-Write properties 读写属性

Using the two facts above, we can see that these are equivalent in 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

This means that instead of doing the this: 这意味着不要这样做:

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

We can do this: 我们做得到:

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

So here is the example code, with a single parameter. 所以这是带有单个参数的示例代码。

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

Limitations 限制

The limitation is that to work, this depends on the call being made in a context where the result is coerced to a non-object type. 限制是要工作,这取决于在结果被强制转换为非对象类型的上下文中进行的调用。 For example 例如

varName = obj.PropName(99)

Since the Set keyword was not used, VB knows that it must get the default property for use here. 由于未使用Set关键字,VB知道它必须在此处获取默认属性。

Again, when passing to a function which takes for example a string, this will work. 同样,当传递给一个以字符串为例的函数时,这将起作用。 Internally VariantChangeType will be called to convert the object to the correct type, which if coercing to a non-object will access the default property. 将调用内部VariantChangeType将对象转换为正确的类型,如果强制转换为非对象将访问默认属性。

The problem may occur when passing directly as a parameter to a function which takes a Variant as an argument. 直接作为参数传递给以Variant作为参数的函数时,可能会出现此问题。 In this case the accessor object will be passed. 在这种情况下,将传递访问者对象。 As soon as the object is used in a non-object context (eg an assignment or conversion to string) the default property will be fetched. 只要对象在非对象上下文中使用(例如,赋值或转换为字符串),就会获取默认属性。 However this will be the value at the time it is converted, not the time it was originally accessed. 但是,这将是转换时的值,而不是最初访问的时间。 This may or may not be an issue. 这可能是也可能不是问题。

This issue can be worked around however by having the accessor object cache the value it returns to ensure it is the value as at the time the accessor was created. 但是,通过让访问者对象缓存它返回的值来确保它是创建访问者时的值,可以解决此问题。

This feature you seek is usually called "indexed properties". 您寻求的此功能通常称为“索引属性”。 The flavor that VB6 uses is the flavor supported by COM interfaces. VB6使用的风格是COM接口支持的风格。

This IDL fragment is similar to what VB6 would generate, and shows what's going on under the hood: 这个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 is a String property that takes two String parameters as indices. IndexedProp是一个String属性,它将两个String参数作为索引。 Contrast with PlainProp , which is of course a non-indexed conventional property. PlainProp形成对比,后者当然是一种非索引的传统属性。

Unfortunately, C# has very limited support for COM-style indexed properties. 不幸的是,C#对COM风格的索引属性的支持非常有限。

C# 4.0 supports consuming COM objects (written elsewhere) that implement a COM interface with indexed properties. C#4.0支持耗时实现带索引属性的COM接口的COM对象(其他地方写入)。 This was added to improve interoperability with COM Automation servers like Excel. 添加此项是为了改善与Excel等COM自动化服务器的互操作性。 However, it doesn't support declaring such an interface, or creating an object that implements such a COM interface even if legally declared elsewhere. 但是,它不支持声明这样的接口,或者创建实现此类COM接口的对象,即使在其他地方合法声明也是如此。

Ben's answer tells you how to create indexed properties in C# - or at least something that results in an equivalent syntax in C# code. Ben的回答告诉你如何在C#中创建索引属性 - 或者至少是在C#代码中产生等效语法的东西。 If you just want the syntax flavor while writing C# code, that works great. 如果您只是在编写C#代码时想要语法风格,那么效果很好。 But of course it's not a COM-style indexed property. 但当然它不是COM风格的索引属性。

This is a limitation of the C# language, not the .NET platform. 这是C#语言的限制,而不是.NET平台。 VB.NET does support COM indexed properties, because they had the mandate to replace VB6 and therefore needed to go the extra mile. VB.NET确实支持COM索引属性,因为它们有权替换VB6,因此需要加倍努力。

If you really want COM indexed properties, you could consider writing the COM version of your object in VB.NET, and have that object forward calls to your C# implementation. 如果您真的想要COM索引属性,可以考虑在VB.NET中编写对象的COM版本,并让该对象将调用转发给您的C#实现。 It sounds like a lot of work to me. 这对我来说听起来很多。 Or port all your code to VB.NET. 或者将所有代码移植到VB.NET。 It really depends on how badly do you want it. 这真的取决于你想要它多么糟糕。

References 参考

But this feature is available only for COM interop; 但此功能仅适用于COM互操作; you cannot create your own indexed properties in C# 4.0. 您无法在C#4.0中创建自己的索引属性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM