How It All Works: The Messy Implementation Details Classes Used by an Event Source The IConnectionPointContainerImpl Class Let's start by examining the IConnectionPointContainerImpl template class implementation of the IConnectionPointContainer interface. First, the class needs to provide a vtable that is compatible with the IConnectionPointContainer interface. This vtable must contain five methods: the three IUnknown methods and the two IConnectionPointContainer methods.
template <class T> class ATL_NO_VTABLE IConnectionPointContainerImpl : public IconnectionPointContainer { typedef CComEnum<IEnumConnectionPoints,&__uuidof(IEnumConnectionPoints), IConnectionPoint*, _CopyInterface<IConnectionPoint> >CComEnumConnectionPoints; public: STDMETHOD(EnumConnectionPoints)( IEnumConnectionPoints** ppEnum) { if (ppEnum == NULL) return E_POINTER; *ppEnum = NULL; CComEnumConnectionPoints* pEnum = NULL; ATLTRY(pEnum = new CComObject<CComEnumConnectionPoints>) if (pEnum == NULL) return E_OUTOFMEMORY; int nCPCount; const _ATL_CONNMAP_ENTRY* pEntry = T::GetConnMap(&nCPCount); // allocate an initialize a vector of connection point // object pointersUSES_ATL_SAFE_ALLOCA; if ((nCPCount < 0) || (nCPCount > (INT_MAX / sizeof(IConnectionPoint*))))return E_OUTOFMEMORY; size_t nBytes=0; HRESULT hr=S_OK; if( FAILED(hr=::ATL::AtlMultiply(&nBytes,sizeof(IConnectionPoint*), static_cast<size_t>(nCPCount)))) { return hr; } IConnectionPoint** ppCP =(IConnectionPoint**)_ATL_SAFE_ALLOCA( nBytes, _ATL_SAFE_ALLOCA_DEF_THRESHOLD); if (ppCP == NULL) { delete pEnum; return E_OUTOFMEMORY; } int i = 0; while (pEntry->dwOffset != (DWORD_PTR)-1) { if (pEntry->dwOffset == (DWORD_PTR)-2) { pEntry++; const _ATL_CONNMAP_ENTRY* (*pFunc)(int*) =(const _ATL_CONNMAP_ENTRY* (*)(int*))( pEntry->dwOffset); pEntry = pFunc(NULL); continue; }ppCP[i++] = (IConnectionPoint*)( (INT_PTR)this+pEntry->dwOffset); pEntry++; } // copy the pointers: they will AddRef this object HRESULT hRes = pEnum->Init((IConnectionPoint**)&ppCP[0],(IConnectionPoint**)&ppCP[nCPCount], reinterpret_cast<IConnectionPointContainer*>(this),AtlFlagCopy); if (FAILED(hRes)) { delete pEnum; return hRes; } hRes = pEnum->QueryInterface(__uuidof(IEnumConnectionPoints), (void**)ppEnum); if (FAILED(hRes)) delete pEnum; return hRes; }STDMETHOD(FindConnectionPoint)(REFIID riid, IConnectionPoint** ppCP) { if (ppCP == NULL) return E_POINTER; *ppCP = NULL; HRESULT hRes = CONNECT_E_NOCONNECTION; const _ATL_CONNMAP_ENTRY* pEntry = T::GetConnMap(NULL); IID iid; while (pEntry->dwOffset != (DWORD_PTR)-1) { if (pEntry->dwOffset == (DWORD_PTR)-2) { pEntry++; const _ATL_CONNMAP_ENTRY* (*pFunc)(int*) = (const _ATL_CONNMAP_ENTRY* (*)(int*))( pEntry->dwOffset); pEntry = pFunc(NULL);continue; } IConnectionPoint* pCP = (IConnectionPoint*)((INT_PTR)this+pEntry->dwOffset); if (SUCCEEDED(pCP->GetConnectionInterface(&iid)) && InlineIsEqualGUID(riid, iid)) { *ppCP = pCP; pCP->AddRef(); hRes = S_OK; break; } pEntry++; } return hRes; } };
The IUnknown methods are easy: The class doesn't implement them. You bring in the proper implementation of these three methods when you define a CComObject class parameterized on your connectable object classfor example, CCom-Object<CConnectableObject>. The CComEnumConnectionPoints typedef declares a class for a standard COM enumerator that implements the IEnumConnectionPoints interface. You use this class of enumerator to enumerate IConnectionPoint interface pointers. A template expansion of the ATL CComEnum class provides the implementation. The implementation of the EnumConnectionPoints method creates and returns an instance of this enumerator. EnumConnectionPoints begins with some basic error checking and then creates a new instance of a CComEnumConnectionPoints enumerator on the heap. The ATL enumerator implementation requires an enumerator to be initialized after instantiation. ATL enumerators are rather inflexible: To initialize an enumerator, you must pass it an array of the items the enumerator enumerates. In this particular case, the enumerator provides IConnectionPoint pointers, so the initialization array must be an array of IConnectionPoint pointers. A connectable object's connection map contains the information needed to produce the array of IConnectionPoint pointers. Each connection map entry contains the offset in the connectable object from the base of the IConnectionPointContainerImpl instance (that is, the currentthis pointer value) to the base of an IConnectionPointImpl instance. EnumConnectionPoints allocates space for the initialization array on the stack, using _ATL_SAFE_ALLOCA. It iterates through each entry of the connection map, calculates the IConnectionPoint interface pointer to each IConnectionPointImpl object, and stores the pointer in the array. Note that these pointers are not reference-counted because the lifetime of the pointers in a stack-based array is limited to the method lifetime. The call to the enumerator Init method initializes the instance. It's critical here to use the AtlFlagCopy argument. This informs the enumerator to make a proper copy of the items in the initialization array. For interface pointers, this means to AddRef the pointers when making the copy. Refer to Chapter 8, "Collections and Enumerators," for details on COM enumerator initialization. The pEnum pointer is a CComEnumConnectionPoints pointer, although it would be a bit better if it were declared as a CComObject<CComEnumConnectionPoints> pointer because that's what it actually is. Regardless, EnumConnectionPoints must return an IEnumConnectionPointspointer, not pEnum itself, so it queries the enumerator (via pEnum) for the appropriate interface pointer and returns the pointer. The FindConnectionPoint method is quite straightforward. After the usual initial error checking, FindConnectionPoint uses the connection map to calculate an IConnectionPoint interface pointer to each connection point in the connectable object. Using the interface pointer, it asks each connection point for the IID of its supported interface and compares it with the IID it's trying to find. A match causes it to return the appropriate AddRef'ed interface pointer with status S_OK; otherwise, failure returns the CONNECT_E_NOCONNECTION status code. One minor oddity is the check to see if the offset is -2 when walking through the connection point map. This is a value that appears only when using attributed ATL (discussed in Appendix D, "Attributed ATL"). Attributed ATL inserts a second connection point map into your class. The check for an offset of -2 is used to signal that it's time to jump from the standard connection point map into the attributed map. This is done by calling the function pointer in that slot to get the new starting address of the next map to walk. This has the potential to be a general-purpose chaining mechanism for connection point maps; however, the connection-point macros don't support it, so it would take a good deal of hacking. Most of the real work is left to the connection point implementation, so let's look at it next. The IConnectionPointImpl Class The IConnectionPointImpl template class implements the IConnectionPoint interface. To do that, the class needs to provide a vtable that is compatible with the IConnectionPoint interface. This vtable must contain eight methods: the three IUnknown methods and the fiveIConnectionPoint methods. The first item of note is that the IConnectionPointImpl class derives from the _ICPLocator class:
template <class T, const IID* piid, class CDV = CComDynamicUnkArray > class ATL_NO_VTABLE IConnectionPointImpl : public _ICPLocator<piid> { ... }
The _ICPLocator Class
template <const IID* piid> class ATL_NO_VTABLE _ICPLocator { public: //this method needs a different name than QueryInterface STDMETHOD(_LocCPQueryInterface)(REFIID riid, void ** ppvObject) = 0; virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0; virtual ULONG STDMETHODCALLTYPE Release(void) = 0; };
The _ICPLocator class contains the declaration of the virtual method, _LocCPQueryInterface. A virtual method occupies a slot in the vtable, so this declaration states that calls through the first entry in the vtable, the entry callers use to invoke QueryInterface, will be sent to the method _LocCPQueryInterface. The declaration is purely virtual, so a derived class needs to provide the implementation. This is important because each connection point needs to provide a unique implementation of _LocCPQueryInterface. A connection point must maintain a COM object identity that is separate from its connection point container. Therefore, a connection point needs its own implementation of QueryInterface. If you named the first virtual method in the _ICPLocator class QueryInterface, C++ multiple inheritance rules would see the name as just another reference to a single implementation of QueryInterface for the connectable object. Normally, that's exactly what you want. For example, imagine that you have a class derived from three interfaces. All three interfaces mention the virtual method QueryInterface, but you want a single implementation of the method that all base classes share. Similarly, you want a shared implementation of AddRef and Release as well. But you don't want this for a connection point in a base class. The idea here is that we want to expose two different COM identities (the connectable object and the connection point), which requires two separate implementations of QueryInterface. But we merge the remaining IUnknown implementation (AddRef and Release) because we don't want to keep a separate reference count for each connection point. ATL uses this "unique" approach to avoid having to delegate AddRef and Release calls from the connection point object to the connectable object. The IConnectionPointImpl Class's Methods The IUnknown methods are more complicated in IConnectionPointImpl than was the case in IConnectionPointContainerImpl so that a connection point can implement its own unique QueryInterface method. For a connection point, this is the _LocCPQueryInterface virtual method.
template <class T, const IID* piid, class CDV = CComDynamicUnkArray > class ATL_NO_VTABLE IConnectionPointImpl : public _ICPLocator<piid> { typedef CComEnum<IEnumConnections,&__uuidof(IEnumConnections), CONNECTDATA, _Copy<CONNECTDATA> > CComEnumConnections;typedef CDV _CDV; public: ~IConnectionPointImpl(); STDMETHOD(_LocCPQueryInterface)(REFIID riid, void ** ppvObject) { #ifndef _ATL_OLEDB_CONFORMANCE_TESTS ATLASSERT(ppvObject != NULL); #endif if (ppvObject == NULL) return E_POINTER; *ppvObject = NULL; if (InlineIsEqualGUID(riid, __uuidof(IConnectionPoint)) || InlineIsEqualUnknown(riid)) { *ppvObject = this; AddRef(); #ifdef _ATL_DEBUG_INTERFACES _AtlDebugInterfacesModule.AddThunk((IUnknown**)ppvObject,_T("IConnectionPointImpl"), riid); #endif // _ATL_DEBUG_INTERFACES return S_OK; } else return E_NOINTERFACE; } STDMETHOD(GetConnectionInterface)(IID* piid2) { if (piid2 == NULL) return E_POINTER; *piid2 = *piid; return S_OK; } STDMETHOD(GetConnectionPointContainer)(IConnectionPointContainer** ppCPC) { T* pT = static_cast<T*>(this); // No need to check ppCPC for NULL since // QI will do that for us return pT->QueryInterface( __uuidof(IConnectionPointContainer), (void**)ppCPC); } STDMETHOD(Advise)(IUnknown* pUnkSink, DWORD* pdwCookie);STDMETHOD(Unadvise)(DWORD dwCookie); STDMETHOD(EnumConnections)(IEnumConnections** ppEnum); CDV m_vec; };
The _LocCPQueryInterface Method The _LocCPQueryInterface method has the same function signature as the COM QueryInterface method, but it responds to only requests for IID_IUnknown and IID_IConnectionPoint by producing an AddRef'ed pointer to itself. This gives each base class instance of anIConnectionPointImpl object a unique COM identity. The AddRef and Release Methods As usual, you bring in the proper implementation of these two methods when you define a CComObject class parameterized on your connectable object classfor example, CComObject<CConnectableObject>. The GetConnectionInterface and GetConnectionPointContainer Methods The GetConnectionInterface interface method simply returns the source interface IID for the connection point, so the implementation is trivial. The GetConnectionPointContainer interface method is also simple, but some involved typecasting is required to request the correct interface pointer. The issue is that the current class, this particular IConnectionPointImpl expansion, doesn't support the IConnectionPointContainer interface. But the design of the template classes requires your connectable object class, represented by class T in the template, to implement theIConnectionPointContainer interface:
T* pT = static_cast<T*>(this); return pT->QueryInterface(IID_IConnectionPointContainer, (void**)ppCPC);
The typecast goes from the connection point subobject down the class hierarchy to the (deriving) connectable object class and calls that class's QueryInterface implementation to obtain the required IConnectionPointContainer interface pointer. The Advise, Unadvise, and EnumConnections Methods The Advise, Unadvise, and EnumConnections methods all need a list of active connections. Advise adds new entries to the list, Unadvise removes entries from the list, and EnumConnections returns an object that enumerates over the list. This list is of template parameter type CDV. By default, this is type CComDynamic-UnkArray, which provides a dynamically growable array implementation of the list. As I described previously, ATL provides a fixed-size list implementation and a specialized single-entry list implementation. However, it is relatively easy to provide a custom list implementation because the Advise, Unadvise, and EnumConnections implementations always access the list through its well-defined methods: Add, Remove, begin, end, GetCookie, and GetUnknown. The CComEnumConnections typedef declares a class for a standard COM enumerator that implements the IEnumConnections interface. You use this class of enumerator to enumerate CONNECTDATA structures, which contain a client sink interface pointer and its associated magic cookie-registration token. A template expansion of the ATL CComEnum class provides the implementation. The implementation of the EnumConnections interface method creates and returns an instance of this enumerator. The Advise Method
template <class T, const IID* piid, class CDV> STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise( IUnknown* pUnkSink, DWORD* pdwCookie) { T* pT = static_cast<T*>(this); IUnknown* p;HRESULT hRes = S_OK; if (pdwCookie != NULL) *pdwCookie = 0; if (pUnkSink == NULL || pdwCookie == NULL) return E_POINTER; IID iid; GetConnectionInterface(&iid); hRes = pUnkSink->QueryInterface(iid, (void**)&p); if (SUCCEEDED(hRes)) { pT->Lock(); *pdwCookie = m_vec.Add(p); hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT; pT->Unlock(); if (hRes != S_OK) p->Release(); } else if (hRes == E_NOINTERFACE) hRes = CONNECT_E_CANNOTCONNECT; if (FAILED(hRes)) *pdwCookie = 0; return hRes;}
The Advise method retrieves the sink interface IID for this connection point and queries the IUnknown pointer that the client provides for the sink interface. This ensures that the client passes an interface pointer to an object that actually implements the expected sink interface. Failure to provide the correct interface pointer produces a CONNECT_E_CANNOTCONNECT error. You don't want to keep a connection to something that can't receive the callback. Plus, by obtaining the correct interface pointer here, the connection point doesn't have to query for it during each callback. When the query succeeds, the connection point needs to add the connection to the list. So, it acquires a lock on the entire connectable object, adds the connection to the list if there's room, and releases the lock. The Unadvise Method
template <class T, const IID* piid, class CDV> STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Unadvise( DWORD dwCookie) { T* pT = static_cast<T*>(this); pT->Lock(); IUnknown* p = m_vec.GetUnknown(dwCookie); HRESULT hRes = m_vec.Remove(dwCookie) ? S_OK :CONNECT_E_NOCONNECTION; pT->Unlock(); if (hRes == S_OK && p != NULL) p->Release(); return hRes;}
The Unadvise method is relatively simple. It locks the connectable object, asks the list class to translate the provided magic cookie value into the corresponding IUnknown pointer, removes the connection identified by the cookie, unlocks the connectable object, and releases the held sink interface pointer. The EnumConnections Method EnumConnection begins with some basic error checking and then creates a new instance of a CComObject<CComEnumConnections> enumerator on the heap. As before, the ATL enumerator implementation requires that an enumerator be initialized from an array after instantiationin this particular case, a contiguous array of CONNECTDATA structures.
template <class T, const IID* piid, class CDV> STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::EnumConnections( IEnumConnections** ppEnum) { if (ppEnum == NULL) return E_POINTER;*ppEnum = NULL; CComObject<CComEnumConnections>* pEnum = NULL; ATLTRY(pEnum = new CComObject<CComEnumConnections>) if (pEnum == NULL) return E_OUTOFMEMORY; T* pT = static_cast<T*>(this); pT->Lock(); CONNECTDATA* pcd = NULL; ATLTRY(pcd = new CONNECTDATA[m_vec.end()-m_vec.begin()]) if (pcd == NULL) { delete pEnum; pT->Unlock(); return E_OUTOFMEMORY; } CONNECTDATA* pend = pcd; // Copy the valid CONNECTDATA's for (IUnknown** pp = m_vec.begin();pp<m_vec.end();pp++) { if (*pp != NULL) { (*pp)->AddRef(); pend->pUnk = *pp;pend->dwCookie = m_vec.GetCookie(pp); pend++; } } // don't copy the data, but transfer ownership to itpEnum->Init(pcd, pend, NULL, AtlFlagTakeOwnership); pT->Unlock(); HRESULT hRes = pEnum->_InternalQueryInterface( __uuidof(IEnumConnections), (void**)ppEnum); if (FAILED(hRes)) delete pEnum;return hRes; }
The connection list stores IUnknown pointers. The CONNECTDATA structure contains the interface pointer and its associated magic cookie. EnumConnections allocates space for the initialization array from the heap. It iterates through the connection list entries, copying the interface pointer and its associated cookie to the dynamically allocated CONNECTDATA array. It's important to note that any non-NULL interface pointers are AddRef'ed. This copy of the interface pointers has a lifetime greater than the EnumConnections method. The call to the enumerator Init method initializes the instance. It's critical here to use the AtlFlagTakeOwnership argument. This informs the enumerator to use the provided array directly instead of making yet another copy of it. This also means that the enumerator is responsible for correctly releasing the elements in the array as well as the array itself. The EnumConnections method now uses _InternalQueryInterface to return an IEnumConnections interface pointer on the enumerator object, which, at this point, is the only outstanding reference to the enumerator.
Classes Used by an Event Sink First, you need to understand the big picture about event sinks. Your object class might want to implement multiple event sink interfaces or the same event sink interface multiple times. All event sink interfaces need nearly identical functionality: IUnknown, IDispatch, Invoke, and the capability to look up the DISPID in a sink map and delegate the event method call to the appropriate event handler. But each implementation also needs some custom functionalityspecifically, each implementation must be a unique COM identity. ATL defines a class named _IDispEvent that implements the common functionality and, through template parameters and interface coloring, allows each derivation from this one C++ class to maintain a unique COM identity. This means that ATL implements all specialized event sink implementations using a single C++ class, _IDispEvent. The _IDispEvent Class Let's examine the _IDispEvent class. The first interesting aspect is that it is intended to be used as an abstract base class. The first three virtual methods are declared using the COM standard calling convention and are all purely virtual. The first method is _LocDEQueryInterface, and the following two are the AddRef and Release methods. This gives the _IDispEvent class the vtable of a COM object that supports the IUnknown interface. The _IDispEvent class cannot simply derive from IUnknown because it needs to provide a specialized version ofQueryInterface. A derived class needs to supply the _LocDEQueryInterface, AddRef, and Release methods.
class ATL_NO_VTABLE _IDispEvent { ... public: //this method needs a different name than QueryInterfaceSTDMETHOD(_LocDEQueryInterface)(REFIID riid, void ** ppvObject) = 0; virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0; virtual ULONG STDMETHODCALLTYPE Release(void) = 0; ... };
The class maintains five member variables, only one of which is always used, m_dwEventCookie. Each member variable is initialized by the constructor:
_IDispEvent() : m_libid(GUID_NULL), m_iid(IID_NULL), m_wMajorVerNum(0), m_wMinorVerNum(0),m_dwEventCookie(0xFEFEFEFE) { }
The m_dwEventCookie variable holds the connection point registration value returned from the source object's IConnectionPoint::Advise method until it's needed to break the connection. The class assumes that no event source will ever use the value 0xFEFEFEFE as the connection cookie because it uses that value as a flag to indicate that no connection is established.
The m_libid, m_iid, m_wMajorVerNum, and m_wMinorVerNum variables hold the type library GUID, the source interface IID, and the type library major and minor version number, respectively.
GUID m_libid; IID m_iid; unsigned short m_wMajorVerNum; unsigned short m_wMinorVerNum; DWORD m_dwEventCookie;
This _IDispEvent class provides the DispEventAdvise and DispEventUnadvise methods, which establish and break, respectively, a connection between the specified source object's (pUnk) source interface (piid) and the _DispEvent sink object.
HRESULT DispEventAdvise(IUnknown* pUnk, const IID* piid) { ATLENSURE(m_dwEventCookie == 0xFEFEFEFE); return AtlAdvise(pUnk, (IUnknown*)this, *piid, &m_dwEventCookie); } HRESULT DispEventUnadvise(IUnknown* pUnk, const IID* piid) { HRESULT hr = AtlUnadvise(pUnk, *piid, m_dwEventCookie); m_dwEventCookie = 0xFEFEFEFE; return hr; }
You can implement multiple event sinks in a single ATL COM object. In the most general case, this means that you need a unique event sink for each different source of events (source identifier). Furthermore, you need a unique event sink object for each separate connection (source interface) to a source of events. The _IDispEventLocator Class Implementing multiple event sinks requires the sink to derive indirectly from _IDispEvent multiple times. But we need to do so in a way that allows us to find a particular _IDispEvent base class instance, given a source object identifier and the source interface on that object. ATL uses the template class _IDispEventLocator to do this. Each unique _IDisp-EventLocator template invocation produces a different, addressable _IDispEvent event sink instance.
template <UINT nID, const IID* piid> class ATL_NO_VTABLE _IDispEventLocator : public _IDispEvent {public: };
The IDispEventSimpleImpl Class The IDispEventSimpleImpl class implements the IDispatch interface. It derives from the _IDispEventLocator<nID, pdiid> class to inherit the IUnknown vtable, the member variables, and the connection-point Advise and Unadvise support that the _IDispEvent base class provides.
template <UINT nID, class T, const IID* pdiid> class ATL_NO_VTABLE IDispEventSimpleImpl : public _IDispEventLocator<nID, pdiid> { // Abbreviated for clarity STDMETHOD(_LocDEQueryInterface)(REFIID riid, void ** ppvObject); virtual ULONG STDMETHODCALLTYPE AddRef(); virtual ULONG STDMETHODCALLTYPE Release(); STDMETHOD(GetTypeInfoCount)(UINT* pctinfo);STDMETHOD(GetTypeInfo)(UINT itinfo, LCID lcid, ITypeInfo** pptinfo); STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid); STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr); };
Notice that the IDispEventSimpleImpl class provides an implementation of the _LocDEQueryInterface, AddRef, and Release methods that it inherited from its _IDispEvent base class. Notice also that the next four virtual methods are the standard IDispatch interface methods. The IDispEventSimpleImpl class now has the proper vtable to support IDispatch. It cannot simply derive from IDispatch to obtain the vtable because the class needs to provide a specialized version of QueryInterface. The IDispEventSimpleImpl class implements the _LocDEQueryInterface method so that each event sink is a separate COM identity from that of the deriving class. The event sink object is supposed to respond positively to requests for its source dispatch interface ID, the IUnknowninterface, the IDispatch interface, and the GUID contained in the m_iid member variable.
STDMETHOD(_LocDEQueryInterface)(REFIID riid, void ** ppvObject) { ATLASSERT(ppvObject != NULL); if (ppvObject == NULL) return E_POINTER; *ppvObject = NULL; if (InlineIsEqualGUID(riid, IID_NULL)) return E_NOINTERFACE; if (InlineIsEqualGUID(riid, *pdiid) || InlineIsEqualUnknown(riid) || InlineIsEqualGUID(riid, __uuidof(IDispatch)) || InlineIsEqualGUID(riid, m_iid)) { *ppvObject = this; AddRef(); #ifdef _ATL_DEBUG_INTERFACES _AtlDebugInterfacesModule.AddThunk( (IUnknown**)ppvObject, _T("IDispEventImpl"), riid); #endif // _ATL_DEBUG_INTERFACES return S_OK; } else return E_NOINTERFACE;}
The IDispEventSimpleImpl class also provides a simple implementation of the AddRef and Release methods. This permits the class to be used directly as a COM object.
template <UINT nID, class T, const IID* pdiid> class ATL_NO_VTABLE IDispEventSimpleImpl : ... { ... virtual ULONG STDMETHODCALLTYPE AddRef() { return 1; } virtual ULONG STDMETHODCALLTYPE Release() { return 1; } ... };
However, when you compose the class into a more complex ATL-based COM object, the AddRef and Release methods in the deriving class are used. In other words, an AddRef to the event sink of a typical ATL COM object calls the deriving object's CComObject::AddRef method (or whatever your most-derived class is). Watch out for reference-counting cycles due to this. A client holds a reference to the event source, which holds a reference to (nominally) the event sink but is actually to the client itself. The IDispEventSimpleImpl class implements the GetTypeInfoCount, GetTypeInfo, and GetIDsOfNames methods by returning the error E_NOTIMPL. An event-dispatch interface is required only to support the IUnknown methods and the Invoke method.
STDMETHOD(GetTypeInfoCount)(UINT*){ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetTypeInfoCount"));} STDMETHOD(GetTypeInfo)(UINT, LCID, ITypeInfo**) {ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetTypeInfo"));}STDMETHOD(GetIDsOfNames)(REFIID, LPOLESTR*, UINT, LCID, DISPID*){ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetIDsOfNames"));} STDMETHOD(Invoke)(DISPID dispidMember, REFIID, LCID lcid, WORD /*wFlags*/, DISPPARAMS* pdispparams, VARIANT* pvarResult,EXCEPINFO* /*pexcepinfo*/, UINT* /*puArgErr*/);
The Invoke method searches the deriving class's event sink map for the appropriate event handler for the current event. It finds the appropriate sink map by calling _GetSinkMap, which is a static member function defined in the deriving class by the BEGIN_SINK_MAP macro (described later in this section). The proper event handler is the entry that has the matching event source ID (nID) as the template invocation, the same source interface IID (pdiid) as the template invocation, and the same DISPID as the argument to Invoke. When the matching event sink entry specifies an _ATL_FUNC_INFO structure (meaning that the event sink entry was defined using the SINK_ENTRY_INFO macro), Invoke uses the structure to call the handler. Otherwise, Invoke calls the GetFuncInfoFromId virtual function to obtain the required structure. When the GetFuncInfoFromId function fails, Invoke silently returns S_OK. This is as it should be because an event handler must respond with S_OK to events the handler doesn't recognize. You must override the GetFuncInfoFromId method when using the SINK_ENTRY_EX macro with the IDispEventSimpleImpl class. The default implementation silently fails:
virtual HRESULT GetFuncInfoFromId(const IID&, DISPID, LCID, _ATL_FUNC_INFO&) { ATLTRACE(_T("TODO: Classes using IDispEventSimpleImpl should " "override this method\n")); ATLASSERT(0);ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetFuncInfoFromId")); }
This means that if you use the IDispEventSimpleImpl class directly, and you specify an event hander using the SINK_ENTRY_EX macro, and you forget to override the GetFuncInfoFromId method or you implement it incorrectly, everything compiles cleanly but your event handler will never be called. The IDispEventSimpleImpl class provides some overloaded helper methods for establishing and breaking a connection to an event source. The following two methods establish and break a connection between the current sink and the specified event interface (piid) on the specified event source (pUnk):
// Helpers for sinking events on random IUnknown* HRESULT DispEventAdvise(IUnknown* pUnk, const IID* piid) { ATLENSURE(m_dwEventCookie == 0xFEFEFEFE); return AtlAdvise(pUnk, (IUnknown*)this, *piid, &m_dwEventCookie); } HRESULT DispEventUnadvise(IUnknown* pUnk, const IID* piid) { HRESULT hr = AtlUnadvise(pUnk, *piid, m_dwEventCookie); m_dwEventCookie = 0xFEFEFEFE; return hr; }
The next two methods establish and break a connection between the current sink and the specified event source using the sink's dispatch interface:
HRESULT DispEventAdvise(IUnknown* pUnk) { return _IDispEvent::DispEventAdvise(pUnk, pdiid); }HRESULT DispEventUnadvise(IUnknown* pUnk) { return _IDispEvent::DispEventUnadvise(pUnk, pdiid); }
The Sink Map: Associated Structure, Macros, and the _GetSinkMap Method The sink map is an array of _ATL_EVENT_ENTRY structures. The structure contains the following fields:
nControlID | The event source identifier; control ID for contained controls | piid | The source dispatch interface IID | nOffset | The offset of the event sink implementation from the deriving class | dispid | The event callback dispatch ID | pfn | The member function pointer of the event handler to invoke | pInfo | The _ATL_FUNC_INFO structure used for the event-handler call |
template <class T> struct _ATL_EVENT_ENTRY { UINT nControlID; // ID identifying object instance const IID* piid; // dispinterface IID int nOffset; // offset of dispinterface from this pointer DISPID dispid; // DISPID of method/property void (__stdcall T::*pfn)(); // method to invoke _ATL_FUNC_INFO* pInfo; // pointer to info structure };
When you use the BEGIN_SINK_MAP macro, you define a static member function in your class called _GetSinkMap. It returns the address of the array of _ATL_EVENT_ENTRY structures.
#define BEGIN_SINK_MAP(_class)\ typedef _class _GetSinkMapFinder;\ static const ATL::_ATL_EVENT_ENTRY<_class>* _GetSinkMap() {\ PTM_WARNING_DISABLE \ typedef _class _atl_event_classtype;\ static const ATL::_ATL_EVENT_ENTRY<_class> map[] = {
Each SINK_ENTRY_INFO macro adds one _ATL_EVENT_ENTRY structure to the array.
#define SINK_ENTRY_INFO(id, iid, dispid, fn, info) {id, &iid, (int)(INT_PTR)(static_cast<ATL::_IDispEventLocator< id, &iid>*>((_atl_event_classtype*)8))-8, dispid, (void (__stdcall _atl_event_classtype::*)())fn, info},
Two aspects of the macro are a little unusual. The following expression computes the offset of the _IDispEventLocator<id, &iid> base class with respect to your deriving class (the class containing the sink map). This enables us to find the appropriate event sink referenced by the sink map entry.
(int)(INT_PTR)(static_cast<_IDispEventLocator< id, &iid>*>((_atl_event_classtype*)8))-8
The following cast saves the event-handler function address as a pointer to a member function:
(void (__stdcall _atl_event_classtype::*)()) fn
The SINK_ENTRY_EX macro is the same as the SINK_ENTRY_INFO macro with a NULL pointer for the function information structure. The SINK_ENTRY macro is the same as the SINK_ENTRY_INFO macro with IID_NULL for the dispatch interface and a NULL pointer for the function information structure.
#define SINK_ENTRY_EX(id, iid, dispid, fn) \ SINK_ENTRY_INFO(id, iid, dispid, fn, NULL) #define SINK_ENTRY(id, dispid, fn) \ SINK_ENTRY_EX(id, IID_NULL, dispid, fn)
The END_SINK_MAP macro ends the array and completes the _GetSinkMap function implementation:
#define END_SINK_MAP() {0, NULL, 0, 0, NULL, NULL} }; \ return map;\
The IDispEventImpl Class Finally, we come to the IDispEventImpl class. This is the class that the code-generation wizards use. It derives from the IDispEventSimpleImpl class and, therefore, inherits all the functionality previously discussed. The additional template parameters specify a type library that describes the source dispatch interface for the event sink.
template <UINT nID, class T, const IID* pdiid = &IID_NULL, const GUID* plibid = &GUID_NULL, WORD wMajor = 0, WORD wMinor = 0, class tihclass = CComTypeInfoHolder> class ATL_NO_VTABLE IDispEventImpl : public IDispEventSimpleImpl<nID, T, pdiid> { ... }
The main feature of the IDispEventImpl class is that it uses the type information to implement functionality that is missing in the base class. The class implements the GetTypeInfoCount, GetTypeInfo, and GetIDsOfNames methods using the type library via a CComTypeInfoHolderobject:
STDMETHOD(GetTypeInfoCount)(UINT* pctinfo) { *pctinfo = 1; return S_OK; } STDMETHOD(GetTypeInfo)(UINT itinfo, LCID lcid, ITypeInfo** pptinfo) { return _tih.GetTypeInfo(itinfo, lcid, pptinfo); }STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid) { return _tih.GetIDsOfNames(riid, rgszNames, cNames, lcid, rgdispid); }
It also overrides the GetFuncInfoFromId method and initializes an _ATL_FUNC_INFO structure using the information provided in the type library:
HRESULT GetFuncInfoFromId(const IID& iid, DISPID dispidMember, LCID lcid, ATL_FUNC_INFO& info) {CComPtr<ITypeInfo> spTypeInfo; if (InlineIsEqualGUID(*_tih.m_plibid, GUID_NULL)) { m_InnerLibid = m_libid; m_InnerIid = m_iid; _tih.m_plibid = &m_InnerLibid; _tih.m_pguid = &m_InnerIid; _tih.m_wMajor = m_wMajorVerNum; _tih.m_wMinor = m_wMinorVerNum; } HRESULT hr = _tih.GetTI(lcid, &spTypeInfo); if (FAILED(hr)) return hr; return AtlGetFuncInfoFromId(spTypeInfo, iid, dispidMember, lcid, info); }
|