65.9K
CodeProject 正在变化。 阅读更多。
Home

我如何爱上 COM 互操作性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (34投票s)

2014年5月20日

CPOL

9分钟阅读

viewsIcon

45986

downloadIcon

377

本文提供了一种将 .NET 程序集暴露给 COM 的实用方法。

引言

好吧,也许这篇文章的标题有点夸张,但这是关于我——尽管极不情愿——如何成功地将我的一个 .NET 库暴露给 COM 的故事。此外,在这个过程中,我最初的 .NET 库变得更好,我实际上甚至有点喜欢上这个额外的 COM API。

显然,和大多数 .NET 开发者一样,我不得不处理大量的遗留非托管代码。即使我宁愿避免,但时不时地,我也不得不使用 .NET 互操作服务来使用一些 ActiveX 组件或其他类型的遗留非托管代码。我已经习惯了。但是,为什么有人会想到要把一个良好、干净的 .NET 库暴露给一个几十年前就被淘汰的开发平台 (COM) 呢?

总之,最近我发现自己处于一个别无选择的境地。我做了一个很棒的 .NET 库,有很多很棒的功能,但客户要求通过 COM 提供相同的功能。

背景

互联网上有很多关于如何将 .NET 程序集暴露给 COM 的解释——例如,CodeProject 上这篇教程。我之所以还要写这篇文章,是因为在我找到的互联网资源中,没有一个描述了我最终为我的项目选择的确切方法。此外,我也没有找到任何提供我在此过程中遇到的所有小挑战概述的资源。众所周知,细节决定成败。

示例代码

本文的示例代码基于经典的“Hello World!”示例。与这类示例通常非常简单的结构相比,我的代码可能显得不必要地复杂,以便完成显示著名文本消息的简单任务,但它仍然相对简单,并且能够优雅地说明我在处理真实代码库时遇到的所有挑战。

示例库中的核心类是 Greeter 类,它依赖于一个 IMessageWriter 实例,该实例通过构造函数注入到 Greeter 类中(是的,这就是依赖注入的实际应用)。

public class Greeter
{
    ...

    private IMessageWriter messageWriter;

    public Greeter(IMessageWriter messageWriter) : this()
    {
        this.messageWriter = messageWriter;
    }

    ...

    public void Greet(GreetingType greetingType)
    {
        this.messageWriter.Write(greetingType.Greeting);
        ...
    }
}

IMessageWriter 实例在 Greet 方法中用于写入消息。GreetingType 决定了问候语的确切措辞(稍后会详细介绍)。IMessageWriter 接口包含一个单一的 Write 方法。

public interface IMessageWriter
{
    void Write(string message);
} 

示例库附带 IMessageWriter 接口的一个具体实现——ConsoleMessageWriter,它将文本消息写入控制台。

public class ConsoleMessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.Write(message);
    }
} 

从控制台应用程序中,以下代码创建一个简单的问候语。

internal class Program
{
    private static void Main()
    {
        var greeter = new Greeter(new ConsoleMessageWriter());
        greeter.Greet(GreetingType.Silly);
    }
}

总体方法

现在,让我们深入探讨这个问题。可能由于我最初不愿意处理 COM 互操作,我决定为自己制定一个明确的规则——可以说是一种自我强加的信条。我绝不会用任何与 COM 相关的代码(如 COM 特定的接口或任何 ComVisible-、Guid- 或 ClassInterface 属性)来“污染”我原始的 .NET 库。我不会允许任何对 System.Runtime.InteropServices 命名空间的引用。此外,我不会接受我原始库的重大降级。因此,我的项目结构如下:

所有与 COM 相关的代码都封装在 ClassLibrary.Interop 程序集中,而我原始的 ClassLibrary 程序集仍然是一个干净的 .NET 库。

ClassLibrary.Interop 程序集中,我显式定义了所有 COM 接口,并用 Guid 属性修饰它们。

namespace ClassLibrary.Interop
{
    using System;
    using System.Runtime.InteropServices;

    [Guid("2c0fe71f-fb44-4fae-ab8e-779053f86737")]
    public interface IGreeter
    {
         ... 
    }
}

此外,我创建了继承自原始类并实现相应显式定义的 COM 接口的新类。我用 Guid 属性和 ClassInterface 属性(带有 ClassInterfaceType.None 参数)修饰这些类。ClassInterfaceType.None 参数阻止在将类元数据导出到 COM 类型库时自动生成类接口。因此,在下面的示例中,只暴露 IGreeter 接口的成员。

namespace ClassLibrary.Interop
{
    using System;
    using System.Runtime.InteropServices;

    [Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
    [ClassInterface(ClassInterfaceType.None)]
    public class Greeter : ClassLibrary.Greeter, IGreeter
    {
        ...
    }
} 

我不会费心用 ComVisible 属性修饰单个类,因为关键在于,在 ClassLibrary.Interop 程序集中,我只处理我想暴露给 COM 的 .NET 类型,所以我会一次性在 AssemblyInfo 文件中声明此设置。

[assembly: ComVisible(true)]

处理挑战

如前所述,我在过程中遇到了一些挑战——主要是因为 COM 不支持某些 .NET/C# 功能。接下来,我将描述各个挑战及其解决方案。

带参数的构造函数

COM 不支持带参数的构造函数。COM 要求使用默认(无参数)构造函数。

如前所示,Greeter 类使用依赖注入,并通过构造函数要求提供一个 IMessageWriter 接口的实例。

public class Greeter
{
    ...

    private IMessageWriter messageWriter;

    public Greeter(IMessageWriter messageWriter) : this()
    {
        this.messageWriter = messageWriter;
    }

    ...
}

因此,我所做的是创建了一个额外的 protected 无参数默认构造函数和一个 protected MessageWriter 属性。这些附加成员是 protected 的这一事实很重要,因为这样我就可以在 ClassLibrary.Interop 程序集中的 Greeter 扩展类中使用它们来提供 COM 互操作性,同时仍将这些成员隐藏起来,不让它们在 .NET Framework 中“正常”使用 Greeter 类——从而强制使用者使用公共构造函数。

public class Greeter
{
    private readonly List<string> greetings;
    private IMessageWriter messageWriter;

    public Greeter(IMessageWriter messageWriter) : this()
    {
        this.messageWriter = messageWriter;
    }

    protected Greeter()
    {
        this.greetings = new List<string>();
    }
 
    ...

    protected IMessageWriter MessageWriter
    {
        get { return this.messageWriter; }
        set { this.messageWriter = value; }
    }

    ...
}

然后,我可以在 Greeter 类的 COM 接口中引入一个 Initialize 方法,并使用该方法设置 MessageWriter 属性。

[Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
[ClassInterface(ClassInterfaceType.None)]
public class Greeter : ClassLibrary.Greeter, IGreeter
{
    ...

    public void Initialize(IMessageWriter messageWriter)
    {
        this.MessageWriter = messageWriter;
    }
}

因此,现在,从 COM 消费者那里,将需要先使用默认构造函数创建 Greeter 对象,然后调用 Initialize 方法。

重载方法

COM 不支持重载方法。在 Greeter 类中,我确实有两个具有不同签名的 Greet 方法——一个始终进行中性问候,另一个则允许我提供一个特定的问候类型作为参数。

public class Greeter
{
    ...

    public void Greet()
    {
        this.Greet(GreetingType.Neutral);
    }

    public void Greet(GreetingType greetingType)
    {
        this.messageWriter.Write(greetingType.Greeting);
        this.greetings.Add(greetingType.Greeting);
    }
} 

处理此问题的唯一方法是在 COM 接口中引入不同的名称。

[Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
[ClassInterface(ClassInterfaceType.None)
public class Greeter : ClassLibrary.Greeter, IGreeter
{
    ...

    public void Greet(GreetingType greetingType)
    {
        base.Greet(greetingType);
    }

    public void GreetNeutral()
    {
        base.Greet(GreetingType.Neutral);
    }

    ...
}

Generics

.NET 泛型对 COM 来说是无法理解的。因此,如果您使用了任何泛型类或方法,或者使用了任何内置的泛型类型,那么您就需要发挥一些创造力。在 Greeter 类中,我使用了泛型的 ReadOnlyCollection<> 来保存问候历史记录。

public class Greeter
{
    ...

    public ReadOnlyCollection<string> Greetings
    {
        get { return new ReadOnlyCollection<string>(this.greetings); }
    }
   
    ...
} 

此问题的解决方案非常直接。只需让 ClassLibrary.Interop 程序集中的 Greeter 扩展返回字符串数组即可。

[Guid("8ea7f6d2-d86f-4070-aaee-b43bbed3706e")]
[ClassInterface(ClassInterfaceType.None)
public class Greeter : ClassLibrary.Greeter, IGreeter
{
    public new string[] Greetings
    {
        get { return base.Greetings.ToArray(); }
    }

    ...
}

继承

我遇到的一个挑战与其他挑战属于不同的类别。这个挑战并非源于 COM 对某些 .NET/C# 功能的缺失支持。相反,它源于我将原始 .NET 库保持在 COM 相关代码之外的自我强加的信条。由于我希望通过继承来扩展原始 .NET 类型,因此只有可继承的类型才能被扩展。.NET 中的 structenum 等类型是不可继承的。

因此,我不得不将我原始库中的几个 struct 改成了类,这并没有太困扰我。

然而,枚举(enum)就比较棘手了。我所做的是引入自己的 Enumeration 类,而不是使用 enum。这是我实际上认为对我原始代码是一大改进的更改之一。我一直觉得 enum 无法通过例如显示名称(例如包含空格)来扩展,这很令人烦恼。通过引入 Enumeration 类,正好可以做到这一点。

public abstract class Enumeration : IComparable<Enumeration>
{
    private readonly string displayName;
    private readonly int value;

    protected Enumeration(int value, string displayName)
    {
        this.value = value;
        this.displayName = displayName;
    }

    public string DisplayName
    {
        get { return this.displayName; }
    }

    public int Value
    {
        get { return this.value; }
    }

    ...
}

关于使用枚举类而不是 enum 的整个讨论本身就值得写一篇单独的文章,但另一个值得一提的优点是,这种方法可以减少不可避免地伴随 enum 使用而产生的 switch 语句的数量。看看问候文本(以 Greeting 属性的形式)是如何优雅地成为问候类型的一个细节的。

public abstract class GreetingType : Enumeration
{
    ...

    protected GreetingType(int value, string displayName)
        : base(value, displayName)
    {
    }

    public abstract string Greeting { get; }

} 

现在可以定义各个问候类型,例如,一个中性问候类型。

public class GreetingTypeNeutral : GreetingType
{
    public GreetingTypeNeutral()
        : base(0, "Neutral")
    {
    }

    public override string Greeting
    {
        get { return "Hello World!"; }
    }
}

或者一个简单的问候类型。

public class GreetingTypeSilly : GreetingType
{
    public GreetingTypeSilly()
        : base(1, "Silly")
    {
    }

    public override string Greeting
    {
        get { return "Howdy World!"; }
    }
}

静态方法

GreetingType 枚举类将我们带到最后一个挑战。在 GreetingType 枚举类中,我定义了 3 个 static 方法——每个问候类型一个。

public abstract class GreetingType : Enumeration
{
    public static readonly GreetingType Casual = new GreetingTypeCasual();
    public static readonly GreetingType Neutral = new GreetingTypeNeutral();
    public static readonly GreetingType Silly = new GreetingTypeSilly();

    ...
}

但不幸的是,COM 不支持 static 方法。因此,对于 COM 接口,我必须暴露 3 个问候类型类——这里以 GreetingTypeCasual 类为例。

[Guid("b60afdd6-7f93-48eb-baf1-15196c8f779b")]
[ClassInterface(ClassInterfaceType.None)]
public class GreetingTypeCasual : ClassLibrary.GreetingTypeCasual, IGreetingType
{
}

这就是为什么我不得不将原始问候类型设为 public。如果我不想将我的程序集暴露给 COM,我会将 GreetingTypeNeutral(以及其他问候类型)设为 internal——甚至设为 GreetingType 类中的 private 类。

COM 注册

当所有挑战都克服并且 ClassLibrary.Interop 程序集准备就绪后,必须对其进行适当的注册。

在我的 ClassLibrary.Interop 项目中,我在项目的“生成”属性下勾选了“注册 COM 互操作”选项。这将在您自己的机器上完成。

如果您想将库的 COM 版本部署到其他机器,您必须使用程序集注册工具 RegAsm。如果您从与程序集本身位于同一文件夹中的 Windows 批处理文件中调用它,您可以使用以下语法:

c:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe /tlb /codebase %~dp0ClassLibrary.Interop.dll

这种方法要求程序集使用强名称签名(即使没有放入 GAC)。

我猜大多数 COM 消费者运行的是 32 位。如果您想为 64 位消费者注册,则应调用位于 c:\Windows\Microsoft.NET\Framework64 下的 64 位版本的 RegAsm

VBA 示例

最后,这里有一些使用 Visual Basic for Applications (VBA) 宏中的 COM API 的示例代码。

Public Sub testGreeter()
    Dim ConsoleMessageWriter As New ConsoleMessageWriter
    Dim Greeter As New Greeter
    Dim Greetings() As String
    
    Greeter.Initialize ConsoleMessageWriter
    
    Greeter.GreetNeutral
    Greetings = Greeter.Greetings
    MsgBox Greetings(0), vbOKOnly, "Neutral greeting"
    
    Greeter.Greet New GreetingTypeCasual
    Greetings = Greeter.Greetings
    MsgBox Greetings(1), vbOKOnly, "Casual greeting"  
End Sub

摘要

本文介绍了一种将 .NET 程序集暴露给 COM 的方法,即在专用的 ClassLibrary.Interop 程序集中处理所有 COM 特定内容,而无需影响原始 ClassLibrary 程序集。

将 .NET 程序集功能暴露给 COM 不一定是一件麻烦事。是的,确实有一些挑战需要克服,而且,当然,我个人总是更倾向于直接使用 .NET 程序集。但是,我确实看到提供一个专用的 COM API 的一些优势,该 API 可以作为一种“更高级别”的脚本 API,供非硬核 .NET 程序员使用。我有点喜欢 ClassLibrary.Interop 程序集中显式定义的 COM 接口作为完整功能的“外观”,以及例如抽象基类和接口是如何对 COM API 用户隐藏的。

© . All rights reserved.