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

SafeCOMWrapper - 托管可 Disposable 的强类型安全包装器, 用于晚期绑定的 COM

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (55投票s)

2005年7月4日

CPOL

10分钟阅读

viewsIcon

469211

downloadIcon

2141

创建版本无关的 COM 包装器,使用后期绑定调用,同时提供强类型化和可处置接口。一个版本无关的托管 Outlook 自动化库。

Sample Image - SafeCOMWrapper.gif

引言

从 .NET 使用 COM 有几个问题

  • 您无法通过利用“using”块来实现 Dispose 模式以安全地处置 COM 引用。
  • 您无法保证 COM 引用被最终化。没有办法为 COM 引用实现“~Destructor()”。
  • 如果调用 Marshal.ReleaseComObject 时因 Exception 而跳过,COM 引用将不会被释放。
  • 当您通过“添加引用...”向 COM 库添加引用时,该引用是版本特定的。因此,如果您添加了对 Office 2003 COM 库的引用,当部署到 Office 2000 时,它将无法正常工作。
  • 版本无关 COM 的唯一解决方案是使用后期绑定操作,但您会错过强类型化语言的所有功能。

让我们来解决所有这些问题。我们希望使用一种**托管的强类型化**方法来执行**后期绑定**的 COM 操作,并同时在 COM 对象上利用**Dispose 模式**。此处提出的解决方案适用于任何 COM 对象,无论是 Microsoft Office COM 库、IE 和 DHTML 对象,甚至是您自己的 COM 对象。每当您处理任何类型的 COM 库时,都应使用此方法。

在进入解决方案之前,让我们先了解一点关于 RealProxy 和方法拦截的背景知识。

更新

  • 2005 年 9 月 9 日:添加了 COM 事件支持。感谢 Richard Deeming 添加了 COM 事件支持以及 ByRef 参数支持。他使之成为一个完整的解决方案。

介绍 RealProxy 和方法拦截

System.Runtime.Remoting.Proxies 命名空间提供了一个名为 RealProxy 的类。您可以使用此类创建任何类的代理并提供方法拦截。方法拦截意味着您可以拦截对目标对象的任何方法调用。您可以重写 RealProxyInvoke 方法,对实际对象调用的任何方法都将被代理的 Invoke 方法拦截。在 Invoke 方法内部,您可以调用实际对象的方法,也可以调用其他方法。在 Invoke 方法中,您可以随心所欲。

图 1:代理如何工作

TransparentProxy 是另一种代理,由运行时动态生成以表示给定类型。实际的方法拦截由 TransparentProxy 完成。这是一个隐藏的机制,您永远不会在代码中看到它的存在。但正是这个代理拦截了对给定类型的任何方法/属性调用,然后将调用重定向到 RealProxyRealProxy 本身不拦截任何方法调用;TransparentProxy 实际提供了捕获方法调用并调用 RealProxyInvoke 方法的服务。

RealProxy 类提供了一个 GetTransparentProxy 方法,您可以使用它为给定类型创建透明代理。例如,如果您有兴趣拦截对 IList 接口的所有方法调用,您可以为 IList 接口创建 TransparentProxy,并且 IList 接口中声明的所有方法都将被 TransparentProxy 拦截。

您可以通过这个 MSDN 电视节目了解更多关于 RealProxy 的信息。

扩展 RealProxy

您可以扩展 RealProxy 并创建自定义拦截类,以拦截对自定义对象的 方法调用。例如,如果您有一个名为 MyObject 的常规类,您可以创建一个扩展 RealProxyMyObjectProxy,并拦截对 MyObject 实例的所有调用,提供一些自定义服务,如日志记录、安全检查、资源清理等。

让我们看一个简单的方法拦截示例

public class MyObject : MarshalByRefObject
{
        public void DoSomething()
        {
               Debug.WriteLine("DoSomething called");
        }
}

这是一个简单的对象。注意:该对象扩展了 MarhalByRefObject。这是一个问题,因为我们不想通过继承此类来破坏我们的对象模型。我们很快就会找到解决这个问题的方法。

现在我们将创建代理类

public class MyObjectProxy : RealProxy
{
        private MyObject _ActualObject;

        public static MyObject Create()
        {
               MyObject obj = new MyObject();
               MyObjectProxy proxy = new MyObjectProxy( obj );
               return proxy.GetTransparentProxy() as MyObject;
        }

        public MyObjectProxy(MyObject obj) : base( typeof(MyObject) )
        {
               _ActualObject = obj;
        }

在这里,我们创建了一个记住实际对象引用的代理对象。当我们拦截时调用实际方法时,我们将需要此引用。创建代理时,我们会生成一个动态透明代理,并返回代理的引用而不是实际对象。

图 2:客户端看到的是对象的接口,但实际上是代理的接口。

这是我们使用该类的方式

[STAThread]
public static void Main()
{
        MyObject obj = MyObjectProxy.Create();
        obj.DoSomething();
}

现在让我们看看执行实际工作的 Invoke 方法

public override IMessage Invoke(IMessage msg)
{
        Debug.WriteLine(" -- Intercepted -- ");
        
        // We are assuming it is a method call
        IMethodCallMessage callMessage = msg as IMethodCallMessage;

        // Do a lot of things here. Log the method call. You will get all the
        // arguments from the msg. Do security checks. Validate arguments.
        // Anything you can do here. The world is yours.

        // Call the actual method
        this._ActualObject.DoSomething();

        // Construct a return message which contains the return value 
        ReturnMessage returnMessage = new ReturnMessage( null, null, 
               0, callMessage.LogicalCallContext,
               callMessage );

        return returnMessage;
}

让我们看看这个方法中发生了什么

  • 对于任何方法或属性访问,都会调用 Invoke 方法。
  • 调用信息可在 IMessage 消息中获得。
  • IMessage 被转换为 IMethodCallMessage,其中包含有关方法调用的所有信息。
  • 在调用实际对象的方法**之前和之后**,我们可以在此方法中编写任何我们想要的内容。
  • 最后,构造一个返回消息,其中包含有关返回值(void 函数的返回值为 null)和所有 out 参数的信息。

不再需要 MarshalByRef

我们不希望我们的对象继承自 MarshalByRef 对象,但仍然希望使用 RealProxy。解决方案是为我们要拦截的类创建一个接口。因此,对于我们简单的类,我们将创建一个接口来声明所有公共方法和属性。

public interface IMyObject
{
    void DoSomething();
}
public class MyObject : IMyObject
{
    public void DoSomething()
    {
        Debug.WriteLine("DoSomething called");
    }
}

看,不再需要 MarshalByRef

现在我们需要稍微修改一下 Real Proxy

public class MyObjectProxy : RealProxy
{
    private IMyObject _ActualObject;

    public static IMyObject Create()
    {
        MyObject obj = new MyObject();
        MyObjectProxy proxy = new MyObjectProxy( obj );
        return proxy.GetTransparentProxy() as IMyObject;
    }

    public MyObjectProxy(IMyObject obj) : base( typeof(IMyObject) )
    {
        _ActualObject = obj;
    }

更改非常简单。我们将所有 MyObject 替换为 IMyObject

所以,使用方式也将如此改变

IMyObject obj = MyObjectProxy.Create();
obj.DoSomething();

尽管为每个具体类引入接口似乎是个坏主意,但实际上,总是为您的具体类提供接口并针对接口而不是具体类编写代码是一个非常好的主意。所有设计模式书籍、面向对象纯粹主义者都会告诉你同样的道理。与针对具体类相比,针对接口编写代码有很多好处。可以举出数百种场景来支持这个想法。但是,这不属于我们讨论的主题。

创建托管的、可处置的、强类型化的但后期绑定的 COM 包装器

现在我将介绍一个可能难以理解但却非常简单的有趣概念。

我们将为 COM 对象创建自己的手动编写的接口。

我们不会从 Visual Studio 添加 COM 对象的引用,而是自己创建一个类似的接口。因此,假设我们想使用 Outlook 的 COM 接口。我们不会添加 Outlook COM 库的引用,而是为我们想要使用的方法和属性创建自己的接口。难以理解?举个例子会更容易。

让我们为 Outlook.Application 对象创建一个接口

public interface Application : IDisposable
{
   string Name { get; }
   void Quit();
}

我们正在创建一个实际复杂的 Outlook.Application 对象的子集。这样做有三个原因:

  • 我们不感兴趣使用 COM 对象暴露的所有功能。
  • 我们想为 Outlook 创建一个版本无关的接口。此接口中将只包含我们在所有版本的 Outlook 中预期的所有方法和属性。
  • 我们需要 IDisposable 接口,以便我们可以使用 using 构造。如果我们添加了 Outlook 库的引用,我们就无法修改它并从 IDisposable 接口扩展接口。

现在到了伟大的 DisposableCOMProxy。它有四项职责:

  • 创建 COM 对象。
  • 为 COM 提供可处置接口,以便您可以在 using 块中使用 COM 对象。
  • 安全地处置 COM 引用。
  • 为 COM 类型提供强类型化接口,但仍然对实际 COM 对象执行后期绑定调用。
public class DisposableCOMProxy : RealProxy
{
        public object COM;

        /// <summary>
        /// We will be using late bound COM operations. The COM object
        /// is created from the Prog ID instead of CLSID which makes it
        /// a version independent approach to instantiate COM.
        /// </summary>
        /// <param name="progID">Prog ID e.g. Outlook.Application</param>
        /// <returns></returns>
        private static object CreateCOM( string progID )
        {
               // Instantiate the COM object using late bound
               Type comType = Type.GetTypeFromProgID( progID, true );
               return Activator.CreateInstance( comType );
        }

        public static IDisposable Create( string progID, Type interfaceType )
        {       
               object theCOM = CreateCOM( progID );
               DisposableCOMProxy wrapper = 
                  new DisposableCOMProxy( theCOM, interfaceType );
               return wrapper.GetTransparentProxy() as IDisposable;
        }

        public DisposableCOMProxy( object theCOM, 
               Type interfaceType ) :base( interfaceType )
        {
               this.COM = theCOM;
        }

Create 静态方法接受 ProgID 和接口类型(例如 Application),该接口定义了 COM 的方法和属性。然后它创建一个包装代理,该代理持有 COM 引用。

现在,我们需要拦截对 Application 接口的所有方法调用,并将调用委托给实际的 COM 对象。

Invoke 方法执行以下操作:

  • 检查方法名称。如果方法名称是 Dispose,则通过调用 Marshal.RelaseComObject 来释放 COM 引用并退出。
  • 对于任何其他方法,它会将调用重定向到实际的 COM 对象。

这是 Invoke 方法的代码

public override IMessage Invoke(IMessage msg)
{
        IMethodCallMessage callMessage = msg as IMethodCallMessage;

        object returnValue = null;

        MethodInfo method = callMessage.MethodBase as MethodInfo;
        
        // We intercept all method calls on the interface and delegate the method
        // call to the COM reference.
        // Only exception is for "Dispose" which needs to be called on this class
        // in order to release the COM reference.
        // COM reference does not have Dispose method
        if( method.Name == "Dispose" )
        {
               this.Release();
        }
        else
        {
               object invokeObject = this.COM;
               Type invokeType = this.COM.GetType();

               // Get Property called: Retrieve property value
               if( method.Name.StartsWith("get_") )
               {
                       string propertyName = method.Name.Substring(4);
                       returnValue = invokeType.InvokeMember( propertyName, 
                               BindingFlags.GetProperty, null,
                               invokeObject, callMessage.InArgs );
               }
                       // Set Property Called: Set the property value
               else if( method.Name.StartsWith("set_") )
               {
                       string propertyName = method.Name.Substring(4);
                       returnValue = invokeType.InvokeMember( propertyName, 
                                     BindingFlags.SetProperty, null,
                                     invokeObject, callMessage.InArgs );
               }
                       // Regular method call
               else
               {
                       returnValue = invokeType.InvokeMember( method.Name, 
                                     BindingFlags.InvokeMethod, null,
                                     invokeObject, callMessage.Args );
               }
        }

        // Construct a return message which contains the return value 
        ReturnMessage returnMessage = new ReturnMessage( returnValue, null, 
               0, callMessage.LogicalCallContext,
               callMessage );

        return returnMessage;
}

请记住,所有属性调用也是方法调用。.NET 运行时会动态生成代理上的“get_PropertyName”和“set_PropertyName”方法,以拦截属性调用。

这样,我们就有了可处置的 COM 包装器,现在我们可以愉快地使用 COM 对象,而无需担心内存泄漏。

public static void Main()
{
        // Instantiate Outlook.Application and wrap the COM with the Application
        // interface so that we have a strongly type managed wrapper to late bound 
        // COM
        using( Application app = 
             ( Application)DisposableCOMProxy.Create( "Outlook.Application", 
              typeof( Application ) ) )
        {
               Debug.WriteLine( app.Name );
        }
}

COM 引用将被确保在两种方式下释放:

  • COM 包装器的 Dispose 方法会释放 COM 引用。
  • COM 包装器的析构函数会释放 COM 引用。即使您忘记调用 Dispose,当对象被垃圾回收器最终化时,引用也会被正确释放。
/// <summary>
/// Safely release the COM object
/// </summary>
private void Release()
{
        if( null == this.COM ) return;

        Marshal.ReleaseComObject( this.COM );
        this.COM = null;

        Debug.WriteLine( "COM released successfully" );
}

~DisposableCOMProxy()
{
        this.Release();
}

因此,通过完成所有这些,我们实际上是在为 COM 对象模拟一个 IDisposable 接口。

现在您拥有了一个版本无关的托管 Outlook 包装器,它可以在所有版本的 Outlook 上运行。尝试在 Outlook 2000、2002、XP 和 2003 上运行它。

然而,这个微小的应用程序接口实际上并没有提供任何有用的东西。您需要一个功能齐全的 Outlook 库接口集合。此外,我们还需要解决另一个问题:

如何处理 COM 对象返回的任何属性或方法返回的对象?返回的对象是纯 COM 对象,没有任何包装器。例如,如果您调用 Application 对象上的 ActiveExplorer() 方法,它会返回正在运行的 Explorer 的实例。我们需要为返回的对象提供 COM 包装器,以便编写强类型代码并实现 Dispose 模式。

解决方案很简单。在 Invoke 方法中,我们分析 returnValue 中的内容。如果它是 object,那么它肯定是一个 COM 对象。因此,我们需要对所有返回类型为 object 的类型执行以下操作:

  • 从代理正在拦截的接口类型(例如 Application)获取方法定义。
  • 找出被调用的方法或属性的返回类型定义,例如 Explorer ActiveExplorer()
  • 如果返回类型是 interface,那么我们就使用接口类型对返回的对象创建一个 COM 包装器。

因此,Invoke 方法将获得以下附加代码:

// Now check if the method return value is also an interface. if it is an 
// interface, then we are interested to intercept that too
if( method.ReturnType.IsInterface && null != returnValue )
{       
        // Return a intercepting wrapper for the com object
        DisposableCOMProxy proxy = 
            new DisposableCOMProxy( returnValue, method.ReturnType );
        returnValue = proxy.GetTransparentProxy();
}

我们还将修改我们定义的接口,以引入 Explorer 类型。

public interface Application : IDisposable
{
        string Name { get; }
        Explorer ActiveExplorer();
        void Quit();
}
public interface Explorer : IDisposable
{
        string Caption { get; }
        void Close();
        void Display();
        void Activate();
}

这样,我们就可以以安全、**托管**、**强类型化**、**可处置**的方式使用 Outlook 的更多功能,这些功能仍然可以通过对 COM 的**后期绑定**调用来工作,从而使其更加安全。

public static void Main()
{
        using( Application app = 
             ( Application)DisposableCOMProxy.Create( "Outlook.Application", 
               typeof( Application ) ) )
        {
               Debug.WriteLine( app.Name );

               using( Explorer explorer = app.ActiveExplorer() )
               {
                       Debug.WriteLine( explorer.Caption );
               }
        }
}

源代码包含什么

您将获得一个完整的 Outlook 库接口集合。我已经反编译了从 Outlook 生成的整个互操作程序集,并清除了每一个方法和属性,以摆脱那些讨厌的属性。

所以,而不是这样:

[ComImport, TypeLibType((short) 4160), 
            Guid("00063001-0000-0000-C000-000000000046")]
public interface _Application
{
      [DispId(61440)]
      Application Application { [return: MarshalAs(UnmanagedType.Interface)] 
                  [MethodImpl(MethodImplOptions.InternalCall, 
                  MethodCodeType=MethodCodeType.Runtime), DispId(61440)] get; }
      [DispId(61450)]
      OlObjectClass Class { [MethodImpl(MethodImplOptions.InternalCall, 
                  MethodCodeType=MethodCodeType.Runtime), DispId(61450)] get; }
      [DispId(0xf00b)]
      NameSpace Session { [return: MarshalAs(UnmanagedType.Interface)] 
                  [MethodImpl(MethodImplOptions.InternalCall, 
                  MethodCodeType=MethodCodeType.Runtime), DispId(0xf00b)] get; }
      [DispId(0xf001)]
      object Parent { [return: MarshalAs(UnmanagedType.IDispatch)] 
                  [MethodImpl(MethodImplOptions.InternalCall, 
                  MethodCodeType=MethodCodeType.Runtime), DispId(0xf001)] get; }
      [DispId(0x114)]
      Assistant Assistant { [return: MarshalAs(UnmanagedType.Interface)] 
                  [MethodImpl(MethodImplOptions.InternalCall, 
                  MethodCodeType=MethodCodeType.Runtime), DispId(0x114)] get; }

您将在提供的源代码中找到以下内容:

public interface Common : IDisposable
{
        Application Application { get; }
        NameSpace Session { get; }
        object Parent { get; }
        OlObjectClass Class { get; }
}

public interface Application : Common
{
        string Name { get; }

        Explorer ActiveExplorer();
        Explorers Explorers{ get; }

        string Version { get; }
        Inspector ActiveInspector();
        object CreateItem(OlItemType ItemType);
        object CreateItemFromTemplate(string TemplatePath, 
                                         object InFolder);
        object CreateObject(string ObjectName);
        NameSpace GetNamespace(string Type);
        
        void Quit();
}

一整套整洁干净的接口。

我还修改了接口,以提供某种通用性。对象模型(或者您也可以说是接口模型)如下:

在源代码中,您会找到以下内容:

  • COMWrapper.cs - 这是您将使用的最终包装器。
  • TestOutlook.cs – 对 Outlook 的各种操作的完整测试。
  • MyObject.cs – 方法拦截的简单示例。
  • OfficeWrappers.cs – 几乎完整的 Outlook 2003 接口集。
  • SimpleOutlookWrapper.cs – 本文中到目前为止您看到的所有代码。

如何创建您自己的 COM 包装器

以下是创建您自己的 COM 包装器的步骤:

  • 创建一个接口,该接口定义 COM 对象的方法和属性签名,例如:Application
  • 创建所有其他相关的接口和枚举,例如 ExplorerolItemType
  • 使用 DisposableCOMProxy.Create 方法创建第一个 COM 对象。
  • using( ... ) 块中使用所有对象,以确保它们被正确处置。

结论

COMWrapper 代理是一个通用的 COM 包装器。它不依赖于 Office。Office 包装器仅作为示例。您可以将此包装器用于任何 COM 对象,包括 IE 浏览器控件引用、DHTML 对象库、任何 Office 应用程序的 COM 库,甚至您自己的 COM 库。每当您处理 COM 对象时,都应该到处使用它。不要相信您的直觉,认为您永远不会忘记调用 Marshal.RelaseComObject。对 COM 对象使用经过验证的 Dispose 模式。

© . All rights reserved.