Events

Writ large:

The full IConnectionPoint protocol is too big to fit in your head. You’ll have to look in a COM book or docs. For a single client to subscribe to a single connection point:

  • The COM server implements IConnectionPointContainer and IConnectionPoint. A connection point is a source.
  • The client is implements the sink. The sink is a COM object.

image image

Publish/Subscribe

COM events support a N:M publish/subscribe model. A connection point (source) can have multiple subscribers (1:M) and a listener (sink) can subscribe to multiple COM servers (N:1).

The protocol to subscribe to an event has several steps:

  1. It is assumed that a user-defined callback interface has been registered on the system. The sink implements this interface.
  2. The client calls QueryInterface on the source to get a pointer to IConnectionPointContainer.
  3. The client calls IConnectionPointContainer::FindConnectionPoint with the IID of the callback interface, and gets a pointer to the relevant IConnectionPoint.
  4. The client calls IConnectionPoint::Advise, passing in the IUnknown of its sink object.
  5. The server calls QueryInterface on the the sink to get its interface pointer.
  6. (The source sends events to the sink’s callback interface…)
  7. To unsubscribe, the sink calls IConnectionPoint::Unadvise and calls Release on the IConnectionPoint and IConnectionPointContainer pointers.

The name IConnectionPointContainer can be a little confusing, ‘container’ refers only to the fact that it contains connection points. If you were embedding an ActiveX control in a window, the source would be the control and the sink would be the window. Both interfaces are implemented by the source, i.e. the control.

In IDL, the connection point interfaces are tagged with [source] in the coclass section. Note that these are dispatch interfaces.

coclass AConnectableObject
{
	[default] interface IAConnectableObject; 
	[default, source] dispinterface 
		_IAConnectableObjectEvents;
	[source] dispinterface _IAConnectableObjectEvents2;
};|

Build the project to create the type library. Then you can run the AppWizard. The generated class header will have a COM map and a connection map, like this:

BEGIN_COM_MAP(CAConnectableObject)
    COM_INTERFACE_ENTRY(IAConnectableObject)
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CAConnectableObject)
    CONNECTION_POINT_ENTRY(DIID__IAConnectableObjectEvents)
    CONNECTION_POINT_ENTRY(DIID__IAConnectableObjectEvents2)
END_CONNECTION_POINT_MAP()

On the sink side, the client implements a handler by deriving a class from IDispatchImpl, which is a templated class.

struct CAtlMsgTrafficEventSink :
    public IDispEventImpl<1, 
        CAtlMsgTrafficEventSink,
        &DIID__IATLMsgTrafficCtlEvents,
        &LIBID_ATLMSGTRAFFICLib, 1, 0>
{
    ...

COM does not require that the sink has a dispatch interface, this is a limitation of ATL.

Or, Just Pass The Pointer

In many situations, the full-blown publish/subscribe model is overkill. Also the setup is chatty if you’ve got an outproc server. It’s needed for compatibility with scripting clients, but there are other, simpler alternatives. Consider IDataObject, IAdviseSink, and IQuickActivate, which set up the connection in a single call. It all depends on your use case.

With IQuickActivate, the source still implements IConnectionPoint. It was designed for ActiveX, so the QACONTAINER parameter includes ambient properties of the containing window. However, IQuickActivate is not supported by ATL.

COM Interop

.NET components can be both sources and sinks of COM events (a.k.a. unmanaged events).


References