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; } } }
|
© Copyright
2005
Paresh Suthar.
Last update:
8/19/2005; 3:25:53 PM.
This theme is based on the SoundWaves
(blue) Manila theme. |
|
|