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_Ca
nDoSomethingDelegate = 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.
Comments
Anonymous
March 13, 2009
PingBack from http://www.tydlevidle.cz/2009/03/exposing-com-events-from-net-implementing-mapsurround-in-arcmap/Anonymous
April 21, 2010
The comment has been removedAnonymous
February 19, 2015
The comment has been removed