Below is a summary of terminology used here when discussing COM events in Visual C++ and ATL.
Inbound interface—This is the normal case where a COM object implements a predefined interface.
Outbound Interface—This is an interface of methods that a COM object will fire at various times. For example, the Map coclass will fire an event on the IActiveViewEvents in response to changes in the map.
Event Source—The source COM object will fire events to an outbound interface when certain actions occur. For example, the Map coclass is a source of IActiveViewEvents and will fire the IActiveViewEvents::ItemAdded event when a new layer is added to the map. The source object can have any number of clients, or event sink objects , listening to events. Also, a source object may have more than one outbound interface; for example, the Map coclass also fires events on an IMapEvents interface. An event source will typically declare its outbound interfaces in IDL with the [source] tag.
Event Sink—A COM object that listens to events is said to be a "sink" for events. The sink object implements the outbound interface; this is not always advertised in the type libraries because the sink may listen to events internally. An event sink typically uses the connection point mechanism to register its interest in the events of a source object.
Connection Point—COM objects that are the source of events typically use the connection point mechanism to allow sinks to hook up to a source. The connection point interfaces are the standard COM interfaces IConnectionPointContainer and IConnectionPoint .
Fire Event—When a source object needs to inform all the sinks of a particular action, the source is said to "fire" an event. This results in the source iterating all the sinks and making the same method call on each. For example, when a layer is added to a map, the Map coclass is said to fire the ItemAdded event. So all the objects listening to the Map's outbound IActiveViewEvents interface will be called on their implementation of the ItemAdded method.
Advise and unadvise events—To begin receiving events, a sink object is said to "advise" a source object that it needs to receive events. When events are no longer required, the sink will unadvise the source.
The ConnectionPoint Mechanism
The source object implements the IConnectionPointContainer interface to allow sinks to query a source for a specific outbound interface. The following steps are performed to begin listening to an event. ATL implements this with the AtlAdvise method.
-
The sink will QI the source object's IConnectionPointContainer and call FindConnectionPoint to supply an interface ID for outbound interfaces. To be able to receive events, the sink object must implement this interface.
-
The source may implement many outbound interfaces and will return a pointer to a specific connection point object implementing IConnectionPoint to represent one outbound interface.
-
The sink calls IConnectionPoint::Advise, passing a pointer to its own IUnknown implementation. The source will store this with any other sinks that may be listening to events. If the call to Advise was successful, the sink will be given an identifier (a simple unsigned long value, called a cookie) to give back to the source at a later point when it no longer needs to listen to events.
The connection is now complete; methods will be called on any listening sinks by the source. The sink will typically hold on to an interface pointer to the source, so when a sink has finished listening it can be released from the source object by calling IConnectionPoint::Unadvise. This is implemented with AtlUnadvise.
Connection point mechanism for hooking source to sink objects
IDispatch events versus pure COM events
An outbound interface can be a pure dispatch interface. This means instead of the source calling directly onto a method in a sink, the call is made via the IDispatch::Invoke mechanism. The IDispatch mechanism has a performance overhead to package parameters compared to a pure vtable COM call. However, there are some situations where this must be used. ActiveX controls must implement their default outbound interface as a pure IDispatch interface; for example, IMapControlEvents2 is a pure dispatch interface. Also, Microsoft Visual Basic 6 can only be a source of pure IDispatch events. The connection point mechanism is the same as for pure COM mechanisms, the main difference being in how the events are fired.
ATL provides some macros to assist with listening to IDispatch events; this is discussed on MSDN under 'Event Handling and ATL'. There are two templates available, IDispEventImpl and IDispEventSimpleImpl, that are discussed in the following sections.
Using IDispEventImpl to listen to events
The ATL template IDispEventImpl will use a type library to "crack" the IDispatch calls and process the arguments into C++ method calls. The Visual Studio Class wizard can provide this mechanism automatically when adding an ActiveX control to a dialog box. Right-click the Control and click Add Event Handler. In the Event Handler Wizard, choose choose the event, choose the sink from the class list and then click Add and Edit.
Visual Studio C++ Class Wizard. Adding event handler to an ActiveX control on a dialog box.
The following code illustrates the event handling code added by the wizard.
[VCPP] // MyDialog.h : Declaration of the CMyDialog
#pragma once
#include "resource.h" // main symbols
#include
// CMyDialog
class CMyDialog: public CAxDialogImpl < CMyDialog > , public IDispEventImpl <
IDC_MAPCONTROL1, CMyDialog >
{
public:
CMyDialog(){}
~CMyDialog(){}
enum
{
IDD=IDD_MYDIALOG
};
BEGIN_MSG_MAP(CMyDialog)MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_HANDLER(IDOK, BN_CLICKED, OnClickedOK)COMMAND_HANDLER(IDCANCEL,
BN_CLICKED, OnClickedCancel)CHAIN_MSG_MAP(CAxDialogImpl < CMyDialog > )
END_MSG_MAP()
// Handler prototypes:
// LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
// LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled);
// LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL
&bHandled)
{
CAxDialogImpl < CMyDialog > ::OnInitDialog(uMsg, wParam, lParam,
bHandled);
bHandled=TRUE;
return 1; // Let the system set the focus
}
LRESULT OnClickedOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL
&bHandled)
{
EndDialog(wID);
return 0;
}
LRESULT OnClickedCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL
&bHandled)
{
EndDialog(wID);
return 0;
}
public:
BEGIN_SINK_MAP(CMyDialog)SINK_ENTRY(IDC_MAPCONTROL1, 1,
OnMouseDownMapcontrol1)END_SINK_MAP()
public:
void __stdcall OnMouseDownMapcontrol1(long button, long shift, long x, long
y, double mapX, double mapY);
};
// MyDialog.cpp : Implementation of CMyDialog
#include "stdafx.h"
#include "MyDialog.h"
// CMyDialog
void __stdcall CMyDialog::OnMouseDownMapcontrol1(long button, long shift, long
x, long y, double mapX, double mapY)
{
// TODO: Add your message handler code here
}
Beware! The following issue with events is documented on the MSDN Knowledge Base when using IDispEventImpl. Fixes to ATL code are shown in MSDN for these issues; however, it is not always desirable to modify or copy ATL header files. In this case, IDispEventSimpleImpl can be used instead.
BUG: IDispEventImpl Event Handlers May Give Strange Values for Parameters (Q241810)
Using IDispEventSimpleImpl to Listen to Events
As the name of this template suggests, it is a simpler version of IDispEventImpl. The type library is no longer used to turn the IDispatch arguments into a C++ method call. While this may be a simpler implementation, it now requires the developer to supply a pointer to a structure describing the format of the event parameters. This structure is typically placed in the .cpp file. For example, here is the structure describing the parameters of an OnMouseDown event for the MapControl:
[VCPP] _ATL_FUNC_INFO g_ParamInfo_MapControl_OnMouseDown=
{
CC_STDCALL, // Calling convention.
VT_EMPTY, // Return type.
6, // Number of arguments.
{
VT_I4, VT_I4, VT_I4, VT_I4, VT_R8, VT_R8
} // VariantArgument types.
};
The header file now inherits from IDispEventSimpleImpl and uses a different macro, SINK_ENTRY_INFO, in the SINK_MAP. Also, the events interface ID is required; #import can be used to define this symbol. Note that a dispatch interface is normally prefixed with DIID instead of IID. See Importing ArcGIS type libraries for an explanation of #import.
[VCPP] #pragma once
#include "resource.h" // main symbols
#include <atlhost.h>
// reference to structure defining event parameters
extern _ATL_FUNC_INFO g_ParamInfo_MapControl_OnMouseDown;
/////////////////////////////////////////////////////////////////////////////
// CMyDialog2
class CMyDialog2: public CAxDialogImpl < CMyDialog2 > , public
IDispEventSimpleImpl < IDC_MAPCONTROL1, CMyDialog2, &DIID_IMapControlEvents2
>
{
public:
// Message handler code removed, it is the same as CMyDialog using IDispEventSimple
BEGIN_SINK_MAP(CMyDialog2)
// Make sure the Event Handlers have __stdcall calling convention
// The 0x1 is the Dispatch ID of the OnMouseDown method
SINK_ENTRY_INFO(IDC_MAPCONTROL1, // ID of event source
DIID_IMapControlEvents2, // interface to listen to
0x1, // dispatch ID of MouseDown
OnMapControlMouseDown, // method to call when event arrives
&g_ParamInfo_MapControl_OnMouseDown) // parameter info for method call
END_SINK_MAP()
};
Listening to more than one IDispatch event interface on a COM object
If a single COM object needs to receive events from more than one IDispatch source, then this can cause compiler issues with ambiguous definitions of the DispEventAdvise method. This is not normally a problem in a dialog box, as AtlAdviseSinkMap will handle all the connections. The ambiguity can be avoided by introducing different typedefs each time IDispEventSimpleImpl is inherited. The following example illustrates a COM object called CListen, which is a sink for dispatch events from a MapControl and a PageLayoutControl.
[VCPP] #pragma once
#include "resource.h" // main symbols
// This is the parameter information
extern _ATL_FUNC_INFO g_ParamInfo_MapControl_OnMouseDown;
extern _ATL_FUNC_INFO g_ParamInfo_PageLayoutControl_OnMouseDown;
//
// Define some typedefs of the dispatch template
//
class CListen; // forward definition
typedef IDispEventSimpleImpl < 0, CListen, &DIID_IMapControlEvents2 >
IDispEventSimpleImpl_MapControl;
typedef IDispEventSimpleImpl < 1, CListen, &DIID_IPageLayoutControlEvents >
IDispEventSimpleImpl_PageLayoutControl;
/////////////////////////////////////////////////////////////////////////////
// CListen
class ATL_NO_VTABLE CListen: public CComObjectRootEx < CComSingleThreadModel > ,
public CComCoClass < CListen, &CLSID_Listen > , public
IDispEventSimpleImpl_MapControl, public
IDispEventSimpleImpl_PageLayoutControl, public IListen
{
public:
CListen(){}
DECLARE_REGISTRY_RESOURCEID(IDR_LISTEN)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CListen)COM_INTERFACE_ENTRY(IListen)END_COM_MAP()
// Associated source and dispatchID to a method call
BEGIN_SINK_MAP(CListen)SINK_ENTRY_INFO(0, // ID of event source
DIID_IMapControlEvents2, // interface to listen to
0x1, // dispatch ID to receive
OnMapControlMouseDown, // method to call when event arrives
&g_ParamInfo_MapControl_OnMouseDown) // parameter info for method call
SINK_ENTRY_INFO(1, DIID_IPageLayoutControlEvents, 0x1,
OnPageLayoutControlMouseDown, &g_ParamInfo_PageLayoutControl_OnMouseDown)
END_SINK_MAP()
// IListen
public:
STDMETHOD(SetControls)(IUnknown *pMapControl, IUnknown *pPageLayoutControl);
STDMETHOD(Clear)();
private:
void __stdcall OnMapControlMouseDown(long button, long shift, long x, long
y, double mapX, double mapY);
void __stdcall OnPageLayoutControlMouseDown(long button, long shift, long x,
long y, double pageX, double pageY);
IUnknownPtr m_ipUnkMapControl;
IUnknownPtr m_ipUnkPageLayoutControl;
};
The implementation of CListen contains the following code to start listening to the controls; the typedef avoids the ambiguity of the DispEventAdvise implementation.
[VCPP] // Start listening to the MapControl
IUnknownPtr ipUnk=pMapControl;
HRESULT hr=IDispEventSimpleImpl_MapControl::DispEventAdvise(ipUnk);
if (SUCCEEDED(hr))
m_ipUnkMapControl=ipUnk;
// Store pointer to MapControl for Unadvise
// Start listening to the PageLayoutControl
ipUnk=pPageLayoutControl;
hr=IDispEventSimpleImpl_PageLayoutControl::DispEventAdvise(ipUnk);
if (SUCCEEDED(hr))
m_ipUnkPageLayoutControl=ipUnk;
// Store pointer to PageLayoutControl for Unadvise
The implementation of CListen also contains the following code to UnAdvise and stop listening to the controls.
[VCPP] // Stop listening to the MapControl
if (m_ipUnkMapControl != 0)
IDispEventSimpleImpl_MapControl::DispEventUnadvise(m_ipUnkMapControl);
m_ipUnkMapControl=0;
if (m_ipUnkPageLayoutControl != 0)
IDispEventSimpleImpl_PageLayoutControl::DispEventUnadvise
(m_ipUnkPageLayoutControl);
m_ipUnkPageLayoutControl=0;
Creating a COM Events Source
For an object to be a source of events, it will need to provide an implementation of IConnectionPointContainer and a mechanism to track which sinks are listening to which IConnectionPoint interfaces. ATL provides this through the IConnectionPointContainerImpl template. In addition, ATL provides a wizard to generate code to fire IDispatch events for all members of a given dispatch events interface. Below are the steps to modify an ATL COM coclass to support a connection point:
-
First ensure that your ATL coclass has been compiled at least once. This will allow the wizard to find an initial type library.
-
In Class view, right-click the COM object and click Add and then Add Connection Point…
-
Check the Project radio button to use a definition of events from the IDL in the project or check the File radio button to browse to a Typelib with another definition.
-
Select the outbound interface to be implemented in the coclass.
-
Clicking Finish will modify your ATL class and generate the proxy classes in a header file, with a name ending in CP , for firing events.
If the wizard fails to run, then use the following example, which illustrates a coclass that is a source of ITOCControlEvents, a pure dispatch interface.
[VCPP] #pragma once
#include "resource.h" // main symbols
#include "TOCControlCP.h"
// Include generated connection point class for firing events
/////////////////////////////////////////////////////////////////////////////
// CMyEventSource
class ATL_NO_VTABLE CMyEventSource: public CComObjectRootEx <
CComSingleThreadModel > , public CComCoClass < CMyEventSource,
&CLSID_MyEventSource > , public IMyEventSource, public
CProxyITOCControlEvents < CMyEventSource > ,
// Generated ConnectionPoint class
public IConnectionPointContainerImpl < CMyEventSource >
// Implementation of Connection point Container
{
public:
CMyEventSource(){}
DECLARE_REGISTRY_RESOURCEID(IDR_MYEVENTSOURCE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CMyEventSource)COM_INTERFACE_ENTRY(IMyEventSource)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
// Allow QI to this interface
END_COM_MAP()
// List of available connection points
BEGIN_CONNECTION_POINT_MAP(CMyEventSource)CONNECTION_POINT_ENTRY
(DIID_ITOCControlEvents)END_CONNECTION_POINT_MAP()
};
The connection point class (TOCControlEventsCP.h in the above example) contains code to fire an event to all sink objects on a connection point.
There is one method on the class for each event beginning "Fire". Each method will build a parameter list of variants to pass as an argument to the dispatch Invoke method. Each sink is iterated, and a pointer to the sink is stored in a vector m_vec member variable inherited from IConnectionPointContainerImpl. Note that m_vec can contain pointers to zero; this must be checked before firing the event.
[VCPP] template <class T> class CProxyITOCControlEvents: public IConnectionPointImpl<T,
&DIID_ITOCControlEvents, CComDynamicUnkArray>
{
public:
VOID Fire_OnMouseDown(LONG button, LONG shift, LONG x, LONG y)
{
// Package each of the parameters into an IDispatch argument list
T *pT=static_cast<T *> (this);
int nConnectionIndex;
CComVariant *pvars=new CComVariant[4];
int nConnections=m_vec.GetSize();
// Iterate each sink object
for (nConnectionIndex=0; nConnectionIndex<nConnections;
nConnectionIndex++)
{
pT->Lock();
CComPtr<IUnknown> sp=m_vec.GetAt(nConnectionIndex);
pT->Unlock();
IDispatch *pDispatch=reinterpret_cast<IDispatch *> (sp.p);
// Note m_vec can contain 0 entries so it is important to check for this
if (pDispatch != NULL)
{
// Build up the argument list
pvars[3]=button;
pvars[2]=shift;
pvars[1]=x;
pvars[0]=y;
DISPPARAMS disp=
{
pvars, NULL, 4, 0
};
// Fire the dispatch method, 0x1 is the DispatchId for MouseDown
pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_METHOD, &disp, NULL, NULL, NULL);
}
}
delete [] pvars; // clean up the parameter list
}
VOID Fire_OnMouseUp(LONG button, LONG shift, LONG x, LONG y)
{
// ... Other events
}
To fire an event from the source, call Fire_OnMouseDown when required.
A similar approach can be used for firing events to a pure COM (non-IDispatch) interface. The wizard will not generate the connection point class, so this must be written by hand; the following example illustrates a class that will fire an ITOCBuddyEvents::ActiveViewReplaced event; ITOCBuddyEvents is a pure COM, non-IDispatch interface. The key difference is that there is no need to package the parameters; a direct method call can be made.
[VCPP] template <class T> class CProxyTOCBuddyEvents: public IConnectionPointImpl<T,
&IID_ITOCBuddyEvents, CComDynamicUnkArray>
{
// This class based on the ATL-generated connection point class
public:
void Fire_ActiveViewReplaced(IActiveView *pNewActiveView)
{
T *pT=static_cast<T *> (this);
int nConnectionIndex;
int nConnections=this->m_vec.GetSize();
for (nConnectionIndex=0; nConnectionIndex<nConnections;
nConnectionIndex++)
{
pT->Lock();
CComPtr<IUnknown> sp=this->m_vec.GetAt(nConnectionIndex);
pT->Unlock();
ITOCBuddyEvents *pTOCBuddyEvents=reinterpret_cast<ITOCBuddyEvents
*> (sp.p);
if (pTOCBuddyEvents)
pTOCBuddyEvents->ActiveViewReplaced(pNewActiveView);
}
}
};
IDL Declarations for an Object That Supports Events
When an object is exported to a type library, the event interfaces are declared by using the [source] tag against the interface name. For example, an object that fires ITOCBuddyEvents declares:
[source] interface ITOCBuddyEvents;
If the outbound interface is a dispatch events interface, dispinterface is used instead of interface. In addition, a coclass can have a default outbound interface; this is specified with the [default] tag. Default interfaces are identified by some design environments (for example, Visual Basic 6). Following is the declaration for the default outbound events interface:
[default, source] dispinterface IMyEvents2;
Event Circular Reference Issues
After a sink has performed an advise on the source, there is typically a COM circular reference. This occurs because the source has an interface pointer to a sink to fire events, this keeps the sink alive. Similarly, a sink object has a pointer back to the source so it can perform the unadvise at a later point. This keeps the source alive. Therefore, these two objects will never be released and may cause substantial memory leaks. There are a number of ways to tackle this issue:
-
Ensure the advise and unadvise are made on a method or Windows message that is guaranteed to happen in pairs and is independent of an object's life cycle. For example, in a coclass that is also receiving Windows messages, use the Windows messages OnCreate (WM_CREATE) and OnDestroy (WM_DESTROY) to advise and unadvise.
-
If an ATL dialog box class needs to listen to events, one approach is to make the dialog box a private COM class and implement the events interface directly on the dialog box. ATL allows this without much extra coding. This approach is illustrated below. The dialog box class creates a CustomizeDialog coclass and listens to ICustomizeDialogEvents. The OnInitDialog and OnDestroy methods (corresponding to Windows messages) are used to advise and unadvise on the CustomizeDialog.
class CEngineControlsDlg: public CAxDialogImpl < CEngineControlsDlg > , public
CComObjectRoot, // Make Dialog Class a COM Object as well
public ICustomizeDialogEvents
// Implement this interface directly on this object
CEngineControlsDlg(): m_dwCustDlgCookie(0){}
// initialize cookie for event listening
// ... Event handlers and other standard dialog code has been removed ...
BEGIN_COM_MAP(CEngineControlsDlg)COM_INTERFACE_ENTRY(ICustomizeDialogEvents)
// Make sure QI works for this event interface
END_COM_MAP()
// ICustomizeDialogEvents implementation to receive events on this dialog box
STDMETHOD(OnStartDialog)();
STDMETHOD(OnCloseDialog)();
ICustomizeDialogPtr m_ipCustomizeDialog; // The source of events
DWORD m_dwCustDlgCookie; // Cookie for CustomizeDialogEvents
}
The dialog box needs to be created like a noncreateble COM object, rather than on the stack as a local variable. This allocates the object on the heap and allows it to be released through the COM reference counting mechanism.
[VCPP] // Create dialog class on the heap using ATL CComObject template
CComObject < CEngineControlsDlg > *myDlg;
CComObject < CEngineControlsDlg > ::CreateInstance(&myDlg);
myDlg->AddRef(); // Keep dialog box alive until you're done with it
myDlg->DoModal();
// Launch the dialog box; when method returns, dialog box has exited
myDlg->Release();
// typically the refcount now goes to 0 and frees the dialog object
- Implement an intermediate COM object for use by the sink; this is sometimes called a listener or event helper object. This object typically contains no implementation but simply uses C++ method calls to forward events to the sink object. The listener has its reference count incremented by the source, but the sink's reference count is unaffected. This breaks the cycle, allowing the sink's reference count to reach 0 when all other references are released. As the sink executes its destructor code, it instructs the listener to unadvise and release the source.
An alternative to using C++ pointers to communicate between listener and sink is to use an interface pointer that is a weak reference. That is , the listener contains a COM pointer to the sink but does not increment the sink's reference count. It is the responsibility of the sink to ensure that this pointer is not accessed after the sink object has been released.