How do we talk with COM the language of events and delegates

I wrote this blog entry from a real world customer issue and noticed that how little documentation exists on this topic.

I assume that you already know what is a source interface in COM and what are IConnectionPoint and IConnectionPointContainer interfaces. Please pick up any COM book to learn that (I would reccommend Essential COM by Don Box). Also, a familiarity with the events and delegates would help.

The example code in this article are picked up from a real world scenario given by a customer.

 

Lets assume that we have a COM server which has a source interface as defined in the following IDL.

Note: The best way to learn from this article would be to

1) create a typelibrary from the IDL given above

2) create the interop dll using tlbimp.exe

3) Use ildasm to read what is written inside the interop dll.

//Start of the idl

import "oaidl.idl";

import "ocidl.idl";

[

object,

uuid(E3AA8979-3038-4CCD-BA37-13F29F1E61F1),

dual,

nonextensible,

helpstring("ILegacyComObject Interface"),

pointer_default(unique)

]

interface ILegacyComObject : IDispatch{

[id(1), helpstring("method DoSomething")] HRESULT DoSomething(void);

};

[

uuid(CD839D63-B5CB-4236-825F-9EF2C943261B),

version(1.0),

helpstring("AtlComClient 1.0 Type Library")

]

library AtlComClientLib

{

importlib("stdole2.tlb");

[

uuid(C23B1EFE-1A27-4200-B14C-5F2019E024C5),

nonextensible,

helpstring("_ILegacyComObjectEvents Interface")

]

dispinterface _ILegacyComObjectEvents

{

properties:

methods:

[id(1) ] VARIANT_BOOL CanDoSomething();

[id(2) ] void DoneSomething();

};

[

uuid(A2272EDE-8103-43A7-A819-286EAE2C2A0A),

helpstring("LegacyComObject Class")

]

coclass LegacyComObject

{

[default] interface ILegacyComObject;

[default, source] dispinterface _ILegacyComObjectEvents;

};

};

//End of the idl

 

 

 

In common terms we also say that the com server supports two "events" CanDoSomething and DoneSomething.

NOTE: ILegacyComObject is just some implemented interface by the com server.

 

When we create an inteop dll using tlbimp.exe it will contain the following classes and interfaces.

 

1) We need two delegates for the two events defined. Our naming convention is <interfacename>_<event name>EventHandler. Also note that since delegates are types so we will have two classes defined in the interop dll by the name:

_ILegacyComObjectEvents_CanDoSomethingEventHandler

_ILegacyComObjectEvents_DoneSomethingEventHandler

 

2) We have an interface defined as _ILegacyComObjectEvents with the correct ComVisible and GUID attributes.

 

3) Tlbimp.exe also creates a second event interface, designated by the "_Event" suffix added to the name of the original interface. This second event interface has DoneSomething and CanDoSomething events as members. It also has add and remove methods for the event delegates. In this example, the interface is called _ILegacyComObjectEvents_Event.

 4) The coclass generated would implement ILegacyComObject and _ILegacyComObjectEvents_Event(Note) and be named as LegacyComObjectClass. For points 3 and 4 also msdn hassome good article https://msdn2.microsoft.com/en-us/library/k639e386.aspx

5) We generate a class <source interface name>_SinkHelper which implements the source interface methods (or in other words it implements _ILegacyComObject interface). Thus we would pass an instance of this class everytime IConnectionPoint::Advise is called. This class also stores the cookie returned from Advise.

 

Here is the pseudo code of the class

 

class _ILegacyComObjectEvents_SinkHelper : _ILegacyComObjectEvents

{

//store the cookie returned from the IConnectionPoint::Advise call

  public Int m_dwCookie;

// store the delegates from the user

public _ILegacyComObjectEvents_CanDoSomethingEventHandler m_CanDoSomethingDelegate;

 

  public _ILegacyComObjectEvents_DoneSomethingEventHandler m_DoneSomethingDelegate;

 

 ILegacyComObject_SinkHelper ()

{

  m_dwCookie = NULL;

m_CanDoSomethingDelegate= NULL;

m_DoneSomethingDelegate = NULL;

}

 

bool CanDoSomething()

{

          if (m_CanDoSomethingDelegate ! = NULL)

return m_CanDoSomethingDelegate.Invoke(); //Invoke the delegate

          return 0; // notice the default value

// See the Notes below for the return type

}

void DoneSomething()

{

          if (m_DoSomethingDelegate ! = NULL)

                return m_DoSomethingDelegate.Invoke(); //Invoke the delegate

   return;

}

}

 

6) We generate an EventProvider with name <source interface name>_EventProvider

This is the main class which provides the inter-operation between the COM server and the client. Note that it also implements the IDisposable interface.

 

Here is the pseudoCode.

class _ILegacyComObjectEvents_EventProvider: IDisposable, _ILegacyComObjectEvents_Event

{

// store the IConnectionPoint object from the COM server here

IConnectionPoint m_ConnectionPoint;

 

// runtime will provide the implementation of this

IConnectionPointContainer m_ConnectionPointContainer;

 

//arraylist of object of type SinkHelpers

ArrayList m_aEventSinkHelpers;

 

public _ILegacyComObject_EventProvider(Object A1)

{

  m_ConnectionPointContainer = (IConnectionPointContainer)A1;

}

 

void Dispose()

{

_ILegacyComObjectEvents_EventProvider::Finalize();

GC.SuppressFinalize();

}

void Finalize()

{

                // save yourself from the threading issues

System.Threading.Monitor::Enter(this);

           try

{

for (each x in m_aEventSinkHelpers)

{

  // call unadivse on each connection made

  m_ConnectionPoint.UnAdvise(((_ILegacyComObject

Events_SinkHelper)x).m_dwCookie);

}

System.Runtime.InteropServices.Marshal::ReleaseComObject(m_ConnectionPoint);

          }//end try

      catch (Exception)

     {

 

      }//end catch

      finally

      {

System.Threading.Monitor::Exit(this);

      }//end finally

}//end of Finalize

 

// This function gets the IConnectionPoint implementation from the server for the given

// interface. Also intializes the arraylist of EventSinkHelpers.

void Init ()

{

m_ConnectionPointContainer.FindConnectionPoint(guid of _ILegacyComObject,

                                                                                    &m_ConnectionPoint);

  Initialize the arraylist m_aEventSinkHelpers;

}//end of Init

 

public void add_CanDoSomething(_ILegacyComObjectEvents_CanDoSomethingEventHandler A_1)

{

//declare some temporary variables.

_ILegacyComObjectEvents_SinkHelper V_0;

// Save yourserlf from the threading issues.

 System.Threading.Monitor::Enter(this);

try

{

                // first check whether we have IConnectionPoint with us

 if (m_ConnectionPoint==NULL)

                              Init();

V_0 = new _ILegacyComObjectEvents_SinkHelper();

//call the advise method

m_ConnectionPoint.Advise(V_0, &V_1);

// store the delegate and the cookie

V_0.m_dwCookie = V_1;

V_0.m_CanDoSomethingDelegate = A_1;

//add the EventSinkHelper to the arraylist

m_aEventSinkHelpers.Add((object) V_0);

} //end try

finally

{

   System.Threading.Monitor::Exit(this);

}//end finally

}// end add_CanDoSomething

 

// We have a similar implementation for add_DoneSomething

 

public void remove_CanDoSomething (_ILegacyComObjectEvents_CanDoSomethingEventHandler A_1)

{

                //declare some temporary variables

_ILegacyComObjectEvents_SinkHelper V_2;

System.Threading.Monitor::Enter(this);

try

{

for (each x in m_aEventSinkHelpers)

{

                                if ((_ILegacyComObjectEvents_SinkHelper) x.m_CanDoSomethingDelegate == A_1)

                                {

                                                m_aEventSinkHelpers.Remove(x);

                                                V_2 = (_ILegacyComObjectEvents_SinkHelper)x;

                                                m_ConnectionPoint.UnAdvise(V_2.m_dwCookie);

}

}

 if (m_aEventSinkHelpers is empty)

    System.Runtime.InteropServices.Marshal::ReleaseComObject(m_ConnectionPoint);

}//end of try

finally

{

System.Threading.Monitor::Exit(this);

}

}//end of remove_CanDoSomething

 

// We have a similar implementation for remove_DoneSomething

 

} //end of class _ILegacyComObject_EventProvider

 

 

EventProvider does the main work for us. In general every time a user adds a delegate

1) Get the IConnectionPointer if not available from the COM server and store it in the EvenProvider.

2) Create a new instance of EvenSinkHelper.

3) Call Advise and give the EventSinkHelper implementation to the server.

 

Here is something unexpected which the users will notice.

 

In COM, a user may create and give one implementation of _ILegacyComObject to the server. When the event occurs the server will call both _ILegacyComObject::CanDoSomething and _ILegacyComObject::DoneSomething. Client calls only one IConnectionPoint::Advise.

 

However in the managed code the user would need to do something like

 

private LegacyComObject m_legacyComObject;

m_legacyComObject = new LegacyComObject();

//add the delegate for CanDoSomething

m_legacyComObject.CanDoSomething += m_legacyComObject_CanDoSomething;

//add the delegate for DoneSomething

m_legacyComObject.DoneSomething += m_legacyComObject_DoneSomething;

 
// m_legacyComObject_CanDoSomething and m_legacyComObject_DoneSomething are two functions implemented in the user class.

 

Because we treat the two methods as two separate events we need to make two calls to _ILegacyComObjectEvents_EventProvider::add_CanDoSomething and _ILegacyComObjectEvents_EventProvider::add_DoneSomething

Looking at the pseudo code above this would lead to two
separate connections or two separate Advise calls and which is inefficient.

In this case, for example, in the first call we create _ILegacyComObjectEvents_SinkHelper instance and call IConnectionPoint::Advise on it. Also notice that _ILegacyComObjectEvents_SinkHelper::m_DoneSomethingDelegate will remain NULL. If unmanaged COM server invokes this DoneSomething event then we would simply return a default value. The delegates are allowed to return only the primitive types or the value types.