【问题标题】:How to make C# COM class support parameterized properties from VB6如何使 C# COM 类支持来自 VB6 的参数化属性
【发布时间】:2016-01-07 22:32:09
【问题描述】:

我对这个问题进行了相当多的研究,虽然我发现了很多关于 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)
{
}

那么,当 VB6 使用这些 C# 方法时,我如何使这些方法看起来(和功能)是单个参数化属性?

我尝试创建两种方法,一种称为“set_PropName”,另一种称为“get_PropName”,希望在 VB6 使用时它们应该是单个参数化属性,但这不起作用;它们作为来自 VB6 的两种不同的方法调用出现。

我认为可能需要在 C# 中将某些属性应用于它们,以便它们在 COM 和 VB6 中被视为单个参数化属性,但我找不到任何似乎合适的属性。

我还尝试重载方法,删除“get_”和“set_”,希望它会将它们视为单个属性,但这也不起作用。那个在 VB6 中产生了这个错误:“Property let procedure not defined and property get procedure didn't 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' 中的问题。

附加信息:运行时遇到致命错误。这 错误地址位于线程 0x1694 上的 0x734a60f4。错误 代码是 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);
        }
    }
}

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

    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# .net dll com vb6


    【解决方案1】:

    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将允许我们省略默认属性的名称。

    只读属性

    如果该属性是 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 作为参数的函数时,可能会出现问题。在这种情况下,将传递访问器对象。只要在非对象上下文中使用对象(例如,赋值或转换为字符串),就会获取默认属性。 然而这将是它被转换时的值,而不是它最初被访问的时间。这可能是也可能不是问题。

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

    【讨论】:

    • 谢谢本。不幸的是,VB6 DLL 属性返回一个 Variant,我将其转换为一个对象。正如我所说,VB6 DLL 仅用作后期绑定,而我尝试创建的兼容 DLL 也只能用于后期绑定。我需要一些方法来告诉编译器组合两种方法(setter 和 getter),并让它们在 VB6 使用时看起来像 COM 中的单个参数化属性。
    • 你是对的,这不适用于后期绑定的 VB。我有一个替代方案,我将很快发布。
    • 好的,接下来我将添加一个完全通用的版本,(对象类型Value,所以只适合后期绑定代码)。您可以使用所需类型的类型化版本轻松对此进行扩展。
    • 再次感谢本。这非常有帮助。但是,我确实遇到了另一个问题。我用这个问题更新了我的原始问题,以及我如何实施更改的更多信息。
    • 由于答案的“解决方法”部分中解释的原因,它必须是一种方法而不是属性。
    【解决方案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 接口。添加此功能是为了提高与 Excel 等 COM 自动化服务器的互操作性。但是,它不支持声明这样的接口,或创建实现这样的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 中创建自己的索引属性。

    【讨论】:

      猜你喜欢
      • 2011-07-13
      • 1970-01-01
      • 2011-10-27
      • 2017-02-11
      • 2020-04-30
      • 1970-01-01
      • 1970-01-01
      • 2015-01-14
      • 2010-11-17
      相关资源
      最近更新 更多