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 pointers
USES_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
current this 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
IEnumConnectionPoints pointer, 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
five IConnectionPoint 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 an
IConnectionPointImpl 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 the
IConnectionPointContainer 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 it
pEnum->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
of QueryInterface. 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 QueryInterface
STDMETHOD(_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
IUnknown interface, 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
CComTypeInfoHolder object:
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);
}
|