如何将COM对象包装在本机.NET类中?

16
我正在使用一个广泛存在的COM API(可能是Outlook,但不是)在.NET(C#)中。我通过在Visual Studio中添加“COM引用”来完成这个过程,所以所有的“魔法”都是在幕后完成的(即,我不必手动运行tlbimp)。
虽然现在可以从.NET轻松地使用COM API,但它并不是非常.NET友好。例如,没有泛型,事件很奇怪,像IPicture这样的特殊情况等。因此,我想创建一个本地的.NET API,使用现有的COM API进行实现。
一个简单的第一步可能是:
namespace Company.Product {
   class ComObject {
       public readonly global::Product.ComObject Handle; // the "native" COM object
       public ComObject(global::Product.ComObject handle) {
          if (handle == null) throw new ArgumentNullException("handle");
          Handle = handle;
       }

       // EDIT: suggestions from nobugz
       public override int GetHashCode() {
          return Handle.GetHashCode();
       }
       public override bool Equals(object obj) {
          return Handle.Equals(obj);
       }
   }
}

这种方法的一个直接问题是,您很容易为同一基础“本机COM”对象创建多个 ComObject 实例。例如,在进行枚举时:
IEnumerable<Company.Product.Item> Items {
   get {
      foreach (global::Item item in Handle.Items)
         yield return new Company.Product.Item(item);
   }
}

这在大多数情况下可能是意料之外的。解决这个问题可能看起来像

namespace Company.Product {
   class ComObject {
       public readonly global::Product.ComObject Handle; // the "native" COM object
       static Dictionary<global::Product.ComObject, ComObject> m_handleMap = new Dictionary<global::Product.ComObject, ComObject>();
       private ComObject(global::Product.ComObject handle) {
          Handle = handle;
          handleMap[Handle] = this;
       }
       public ComObject Create(global::Product.ComObject handle) {
          if (handle == null) throw new ArgumentNullException("handle");

          ComObject retval;
          if (!handleMap.TryGetValue(handle, out retval))
              retval = new ComObject(handle);
          return retval;             
       }
   }
}

那看起来好多了。枚举器改为调用Company.Product.Item.Create(item)。 但现在问题是Dictionary<>会保持两个对象"活着",所以它们永远不会被垃圾回收;这对COM对象可能是不利的。现在事情开始变得混乱起来...
看起来解决方案的一部分以某种方式使用WeakReference。还有建议使用IDisposable,但在每个对象上处理Dispose()似乎并不是很.NET友好。然后有各种讨论关于何时/是否应该调用ReleaseComObject。还有代码http://codeproject.com上使用延迟绑定,但我满意于一个版本相关的API。

所以,目前我不确定最好的处理方式是什么。我希望我的本地.NET API尽可能像".NET"(甚至可以使用.NET 4.0中的Interop程序集),而无需使用"两个点"规则等启发式方法。

我想尝试的一件事是创建一个ATL项目,使用/clr标志进行编译,并使用C++的编译器COM支持(由#import创建的Product::ComObjectPtr)而不是.NET RCWs。当然,我通常更喜欢用C#编程而不是C++/CLI...

4个回答

12

你自己不需要处理COM对象。在你添加了对COM二进制文件的引用后,.NET会为你生成一个门面(facade),从而简化使用COM对象的任务,使其变成了使用普通的.NET类。如果你不喜欢为你生成的接口,那么你应该创建一个现有门面的门面。你不必担心COM的复杂性,因为这已经为你完成了(可能还有一些需要注意的事情,但我认为它们是少数的)。只需将该类用作常规的.NET类,因为它确实是一个常规的.NET类,并在出现任何问题时处理它们。

编辑:你可能会遇到的问题之一是不确定性的 COM 对象销毁。在后台进行的引用计数依赖于垃圾回收,因此您无法确定对象何时被销毁。根据您的应用程序,您可能需要更确定性的 COM 对象销毁。要做到这一点,您将使用 Marshal.ReleaseComObject如果是这种情况,则您应该注意 this 引起的问题。

抱歉,我想发布更多链接,但显然我必须先获得 10 分声望才能发布超过 1 个链接。


6
我在将COM对象引入.NET时发现最大的问题是垃圾回收器运行在不同的线程上,而COM对象的最终释放通常会(总是?)从该线程调用。Microsoft故意打破了COM线程模型规则,即针对公寓线程化对象,所有方法必须从同一个线程调用。

对于一些COM库来说,这并不是什么大问题,但对于其他一些库来说,这是个巨大的问题 - 尤其是需要在它们的析构函数中释放资源的库。

这是需要注意的事情...

2

你让事情变得不必要的困难。这不是一个“句柄”,也不需要引用计数。__ComObject是一个普通的.NET类,遵循正常的垃圾回收规则。


不,第一个代码片段已经足够好了。垃圾收集器知道何时清理引用。除非知道实际 coclass 名称,否则我无法建议更好的名称。 不要使用“ComObject”。 - Hans Passant
您让我有点迷糊了。为什么不同的COM对象集合不能映射到不同的.NET对象集合?我认为您应该考虑使用类工厂。 - Hans Passant
我不明白问题所在,为什么不在创建COM对象时立即进行包装?一个COM对象对应一个.NET包装器对象。以后再进行包装没有意义。 - Hans Passant
好的,我开始看到问题了:Items是COM对象的一个属性。WeakReference太丑了,创建一个新的包装器。一定要重写GetHashCode()和Equals()方法,以便基于COM对象而不是包装器实例来确定身份。 - Hans Passant
我们绝对没有在同一页上。C++/CLI并不能解决任何问题,你仍然会面临完全相同的包装器问题。CLR的COM互操作层确实可以处理__ComObject身份映射,但这对C++/CLI也不可用。我认为你可能让这个问题变得比它本来应该的更加困难了。祝你好运! - Hans Passant
1
你开始这个任务的时候就得出结论,你正在处理的COM接口很糟糕,需要修复。我不怀疑这一点。Office接口是否同样糟糕需要使用包装器?就我个人而言,我认为他们成功地通过了八个主要版本并保持了一致性和良好的工作状态是令人惊讶的。这是一个良好的设计。如果你必须处理糟糕的COM接口,请修复它们。我相信你知道如何做到这一点! - Hans Passant

0

听起来你想要在复杂的COM API周围创建一个更简单的.NET API。正如nobugz所说,你的ComObject类是真正的本地.NET对象,内部包含对实际com对象的非托管引用。你不需要做任何奇怪的事情来管理它们...只需像普通.NET对象一样使用它们。

现在,关于向你的.NET消费者呈现“更漂亮的界面”。有一个现有的设计模式可以解决这个问题,它被称为Facade。我假设你只需要这些COM对象提供的功能的一部分。如果是这种情况,那么在你的com互操作对象周围创建一个facade层。这个层应该包含必要的类、方法和支持类型,为所有.NET客户端提供必要的功能,比com对象本身具有更友好的API。facade还负责简化奇怪的事情,比如将com对象用于事件的转换为普通的.NET事件、数据编组、简化com对象的创建、设置和拆卸等。

通常情况下,应该避免使用抽象化,因为它们往往会增加工作量和复杂性。然而有时候它们是必要的,在某些情况下可以极大地简化事情。如果一个更简单的API可以提高其他团队成员的生产力,他们需要消费非常复杂的组件对象系统提供的一些功能,那么抽象化就提供了实际价值。


正如外观设计模式所述,它充当一个简化缓冲区,位于一个系统和另一个系统之间。假设您知道这个外观的消费者需要什么功能,您将设计一个全新的API,为该功能提供干净、简单的.NET接口。一个正确设计的外观是完全自包含和独立的。它包装的系统不应泄漏到它缓冲的系统中。这意味着您需要为外观设计新的类、枚举、方法等,这些方法映射到任何com类型和方法。 - jrista
正如我在最后一段中提到的那样,抽象是必要的恶。它们之所以是恶,是因为它们在某种意义上增加了工作和复杂性。它们之所以是必要的,是因为它们在另一种意义上减少了工作和复杂性。在您的情况下,外观的好处是多方面的:简化了复杂的系统,缓冲了消费者对遗留复杂性的影响,将COM的怪异之处隐藏在.NET的正常性背后等等。您可以选择要求每个人直接使用COM对象包装器...然而,这会创建一种负耦合,可能会在未来产生严重后果... - jrista

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