Paresh Suthar's Radio Weblog
And that's all I have to say about that - Forrest Gump






Subscribe to "Paresh Suthar's Radio Weblog" in Radio UserLand.

Click to see the XML version of this web page.

Click here to send an email to the editor of this weblog.
 

 

Connection point based event handling between COM and .NET

Though Adam Nathan does an excellent job describing connection point event handling, there are some missing details that I hope to explain for other developers journeying into this realm.

We begin with the tlbimp.exe program that comes with VS.NET - this application will take an existing type library and generate a .NET assembly from it (sometimes referred to as an "Interop").  The resulting assembly contains the managed type definitions for all of the enums, interfaces and coclasses that exist in the type library, but it does not contain any implementation code.

A  VS.NET project can reference an Interop assembly to allow COM connection point events to be subscribed to via their managed counterparts, using standard managed code techniques.  This means that in a C# project, you can write code that uses the "+=" notation to establishing a subscription to a COM connection point event, and that uses the "-=" notation for removing the subscription.  There are several important points to keep in mind when sinking (subscribing to) these events:

  • Any COM interfaces that are part of the parameter list for the event handler will implicitly have a Runtime Callable Wrapper (RCW) created for them, if one does not already exist, otherwise the reference count for the existing RCW is incremented.  Note that the Common Language Runtime (CLR) uses hashtables to associate a COM interface pointer with it's RCW for subsequent usage, and the hashtable entry is cleared when the RCW's reference count becomes 0.
  • If you do not explicitly release the RCWs prior to leaving the event handler, you will have non-deterministic release of these RCWs.  Essentially this means that if require deterministic release of the COM interfaces, you should call System.Runtime.InteropServices.Marshal.ReleaseCOMObject(..) for each COM interface.
  • If the COM connection point events can be fired by multiple threads, you must make sure that any marshalling that may occur will work correctly.  When COM connection point events are fired from COM code, COM Callable Wrappers (CCW) are used to notify the .NET client(s).  CCWs live in the free threaded apartment and aggregate the free threaded marshaler, so there are usually no issues from this side.  The issues that do come up are when the RCWs that represent COM interfaces are used in the event handler.  If the apartment in which the COM object that implements the COM interface is different than the one used to access the RCW, marshalling will occur.

One approach to ensure deterministic finalization for COM interfaces is to create helper classes that manage the COM object interaction by implementing IDisposable.  You could 1) Create a helper class per COM object that uses types defined by the Interop Assemblies, or 2) Create a helper class per COM object that uses another helper class which uses raw connection points.  We'll examine both cases based on a type library generated from the IDL below, and an Interop assembly called Interop.SampleTypeLibrary.dll generated from tlbimp.exe:


[uuid(FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFF0), version(1.0)]
library SampleTypeLibrary
{
   importlib("stdole2.tlb");

   [uuid(FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFF1), version(1.0), oleautomation, dual]
   interface IPerson : IDispatch
   {
      [propget] HRESULT Address(IAddress** o_ppIAddress);
      [propput] HRESULT Address(IAddress* i_pIAddress);
   }

   [uuid(FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFF2), version(1.0), oleautomation, dual]
   interface IAddress : IDispatch
   {
      [propget] HRESULT Street (BSTR* o_pValue);
      [propput] HRESULT Street (BSTR i_Value);
      [propget] HRESULT City (BSTR* o_pValue);
      [propput] HRESULT City (BSTR i_Value);
      [propget] HRESULT State(BSTR* o_pValue);
      [propput] HRESULT State (BSTR i_Value);
   }

   [uuid(FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFF3), version(1.0)]
   dispinterface IPersonListener
   {
      properties:
      methods:
        [id(1)] void OnAddressChanged(IAddress* i_pIAddress);
   }

   [uuid(FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFF4)]
   coclass Person
   {
      [default] interface IUnknown;
      interface IPerson;
      [default, source] dispinterface IPersonListener;
   }

   [uuid(FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFF5)]
   coclass Address
   {
      [default] interface IUnknown;
      interface IAddress;
   }

};

Case 1: Create a helper class per COM object that uses types defined by the Interop Assemblies
using Interop.SampleTypeLibrary;
using System.Runtime.InteropServices;

namespace SampleNamespace
{
   public class PersonHelperClass :
      Interop.SampleTypeLibrary.IPerson,
      Interop.SampleTypeLibrary.IPersonListener,
      IDisposable
   {
      private Person m_Person;

      // ; Declare delegate and event that consumers will use instead of
      // ; those provided by Interop Assembly
      public delegate void AddressChangedEventHandler(IAddress i_pIAddress);
      public event AddressChangedEvent;

      public PersonHelperClass(Person i_Person)
      {
         // ; Save reference to Interop Assembly object
         m_Person = i_Person;
   
         //  Subscribe for events defined by Interop Assembly
         m_Person.OnAddressChanged += PersonListener_OnAddressChangedEventHandler(OnAddressChangedEventHandler);
      }

      public OnAddressChangedEventHandler (IAddress i_pIAddress)
      {
         try
         {
            //   Rebroadcast event
            AddressChangedEvent(i_pIAddress);
         }
         catch
         {
         }

         //   Decrement RCW count
         Marshal.ReleaseComObject(i_pIAddress);
      }

      //   Implementation of IDisposable
      public new void Dispose()
      {
         if (m_Person != null)
         {
            //  Unsubscribe for events defined by Interop Assembly
            m_Person.OnAddressChanged -= PersonListener_OnAddressChangedEventHandler(OnAddressChangedEventHandler); 

            //   Decrement RCW count and set reference to null
            Marshal.ReleaseComObject(m_Person);
            m_Person = null;
         }
      }
   }
}

Case 2:  Create a helper class per COM object that uses another helper class which uses raw connection points
using Interop.SampleTypeLibrary;
using System.Runtime.InteropServices;

namespace SampleNamespace2
{
   public class PersonHelperClass2 :
      Interop.SampleTypeLibrary.IPerson,
      Interop.SampleTypeLibrary.IPersonListener,
      IDisposable
   {
      private Person m_Person;

      //   Declare instance of helper class for managing connection point
      //   based public event AddressChangedEvent;
      private ConnectionPointHelperClass m_PersonListenerConnectionPoint = null;

     //   Declare event that will be used for rebroadcast purposes
      protected event AddressChangedEventHandler InternalAddressChangedEvent;

      //   Declare delegate and event that consumers will use instead of
      //   those provided by Interop Assembly
      public delegate void AddressChangedEventHandler(IAddress i_pIAddress);
      public event AddressChangedEvent
      {
         //   Override default behavior for adding event subscribers
         add
         {
            lock(this)
            {
               //   Create instance of connection point helper class if necessary
               if (m_PersonListenerConnectionPoint == null)
               {
                  Guid PersonListenerIID = typeof(Interop.SampleTypeLibrary.IPersonListener).GUID;
                  m_PersonListenerConnectionPoint = new ConnectionPointHelperClass(ref PersonListenerIID);
               }
      
               //   Subscribe for connection point based events
               object Source = m_Person;
               object Sink = this;
               m_PersonListenerConnectionPoint.Advise(ref Source, ref Sink);

               //   Add subscriber to internal event
               InternalAddressChangedEvent += value;
            }

         //   Override default behavior for removing event subscribers
         remove
         {
            lock(this)
            {
               //   Unsubscribe for connection point based events
               m_PersonListenerConnectionPoint.Unadvise();

               //   Remove subscriber to internal event
               InternalAddressChangedEvent -= value;
            }
      }

      public PersonHelperClass2 (Person i_Person)
      {
         // Save reference to Interop Assembly object
         m_Person = i_Person;
      }

      public OnAddressChangedEventHandler (IAddress i_pIAddress)
      {
         try
         {
            //   Rebroadcast event
            if (InternalAddressChangedEvent != null)
               InternalAddressChangedEvent(i_pIAddress);
         }
         catch
         {
         }

         //   Decrement RCW count
         Marshal.ReleaseComObject(i_pIAddress);
      }

      //   Implementation of IDisposable
      public new void Dispose ()
      {
         if (m_Person != null)
         {
            //  Decrement RCW count and set reference to null
            Marshal.ReleaseComObject(m_Person);
            m_Person = null;
         }
      }
   }

   public class ConnectionPointHelperClass
   {
      private UCOMIConnectionPoint m_ConnectionPoint = null;
      private int m_Cookie = 0;
      private int m_Count = 0;
      private Guid m_IID = Guid.Empty;

      public ConnectionHelperClass (ref Guid i_IID)
      {
         m_IID = i_IID;
      }

      public void Advise (ref object i_Source, ref object i_Sink)
      {
         //   Late bind connection point subscription
         if (++m_Count == 1)
         {
           //   Subscribe for connection point event
            UCOMIConnectionPointContainer ConnectionPointContainer = (UCOMIConnectionPointContainer)i_Source;
            ConnectionPointContainer.FindConnectionPoint(ref m_IID, out m_ConnectionPoint);
            m_ConnectionPoint.Advise(i_Sink, out m_Cookie);
         }
      }

      public void Unadvise ()
      {
         //   Unsubscribe for connection point event upon last unadvise
         if (--m_Count == 0)
         {
            m_ConnectionPoint.Unadvise(m_Cookie);
            m_Cookie = 0;

            //   Decrement RCW count and set reference to null
            Marshal.ReleaseComObject(m_ConnectionPoint);
            m_ConnectionPoint = null;
         }
      }
}


Click here to visit the Radio UserLand website. © Copyright 2005 Paresh Suthar.
Last update: 8/19/2005; 3:25:53 PM.
This theme is based on the SoundWaves (blue) Manila theme.