Chapter 9. Connection Points
A Review of Connection Points
An object implements one or more interfaces to expose its functionality. The term connection points refers to a logically inverse mechanism that allows an object to expose its capability to call one or more specific interfaces.
Another perspective is that QueryInterface allows a client to retrieve from an object a pointer to an
interface that the object implements. Connection points allow a
client to give an object a pointer
to an interface that the client implements. In the first case, the
client uses the retrieved interface pointer to call methods
provided by the object. In the second case, the object uses the
provided interface pointer to call methods provided by the
client.
A slightly closer inspection of the two
mechanisms reveals that QueryInterface allows a client to
retrieve from an object only interfaces that the client knows how
to call. Connection points allow a client to provide to an object
only interfaces that the object knows how to call.
A connection has two parts: the object making calls to the methods of a specific interface, called the source or, alternatively, the connection point; and the object implementing the interface (receiving the calls), called the sink object (see Figure 9.1) Using my terminology from earlier paragraphs, the object is the source and makes calls to the sink interface methods. The client is the sink and implements the sink interface. One additional complexity is that a particular source object can have connections to multiple sink objects.
Figure 9.1. A connection to two sinks
The IConnectionPoint Interface
A client uses the source
object’s implementation of the IConnectionPoint interface
to establish a connection. Here is the definition of the IConnectionPoint interface:
1 interface IConnectionPoint : IUnknown {
2 HRESULT GetConnectionInterface ([out] IID* pIID);
3 HRESULT GetConnectionPointContainer (
4 [out] IConnectionPointContainer** ppCPC);
5 HRESULT Advise ([in] IUnknown* pUnkSink,
6 [out] DWORD* pdwCookie);
7 HRESULT Unadvise ([in] DWORD dwCookie);
8 HRESULT EnumConnections ([out] IEnumConnections** ppEnum);
9 }
The GetConnectionInterface method
returns the interface identifier (IID) of the sink interface for
which a connection point makes calls. Using the previous example,
calling GetConnectionInterface returns IID_ISomeSink. A client calls the Advise method
to establish a connection. The client provides the appropriate sink
interface pointer for the connection point and receives a magic
cookie (token) that represents the connection. A client can later
call the Unadvise method, specifying the magic cookie to
break the connection. The EnumConnections method returns a
standard COM enumeration object that a client uses to enumerate all
the current connections that a connection point holds. The last
method is GetConnectionPointContainer, which introduces a
new complexity.
So far, this design allows a source object to make calls on only one specific interface. The source object maintains a list of clients that want to receive calls on that specific interface. When the source object determines that it should call one of the methods of its sink interface, the source iterates through its list of sink objects, calling that method for each sink object. What the design (again, as described so far) doesn’t include is the capability for an object to originate calls on multiple different interfaces using this mechanism. Alternatively, to present the question directly, we have a design in which an object can support multiple connections to a single connection point, but how can an object support multiple different connection points?
The solution is to demote the source object to
subobject status and have an encapsulating object, called the
connectable object (see Figure9.2), act as a container of these source subobjects. A client
uses the source object’s GetConnectionPointContainer method to retrieve a pointer to the connectable object. A
connectable object implements the IConnectionPointContainer interface.
Figure 9.2. A connectable object
Implementing IConnectionPointContainer indicates that a COM object
supports connection points and, more specifically, that it can
provide a connection point (a source subobject) for each sink
interface the connectable object knows how to call. Clients then
use the connection point as described previously to establish the
connection.
The IConnectionPointContainer Interface
Here is the definition of the IConnectionPointContainer interface:
1 interface IConnectionPointContainer : IUnknown {
2 HRESULT EnumConnectionPoints (
3 [out] IEnumConnectionPoints ** ppEnum);
4 HRESULT FindConnectionPoint ([in] REFIID riid,
5 [out] IConnectionPoint** ppCP);
6 }
You call the FindConnectionPoint method
to retrieve an IConnectionPoint interface pointer to a
source subobject that originates calls on the interface specified
by the riid parameter. The EnumConnectionPoints method returns a standard COM enumerator subobject that implements
the IEnumConnectionPoints interface. You can use this
enumerator interface to retrieve an IConnectionPoint interface pointer for each connection point that the connectable
object supports.
Most often, a client that wants to establish a connection to a connectable object does the following (with error checking removed for clarity):
1 CComPtr<IUnknown>
2 spSource = /* Set to the source of the events */ ;
3 CComPtr<_ISpeakerEvents>
4 spSink = /* Sink to receive the events */ ;
5 DWORD dwCookie ;
6
7 CComPtr<IConnectionPointContainer> spcpc;
8 HRESULT hr = spSource.QueryInterface (&spcpc);
9
10 CComPtr<IConnectionPoint> spcp ;
11 hr = spcpc->FindConnectionPoint(__uuidof(_ISpeakerEvents),
12 &spcp);
13 hr = spcp->Advise (spSink, &dwCookie) ;// Establish connection
14 // Time goes by, callbacks occur...
15 hr = spcp->Unadvise (dwCookie) ; // Break connection
In fact, ATL provides two useful functions that
make and break the connection between a source and a sink object.
The AtlAdvise function makes the connection between a
connectable object’s connection point (specified by the pUnkCP and iid parameters) and a sink interface
implementation (specified by the pUnk parameter), and
returns a registration code for the connection in the pdw location. The AtlUnadvise function requests the
connectable object’s connection point to break the connection
identified by the dw parameter.
1 ATLAPI AtlAdvise(IUnknown* pUnkCP, IUnknown* pUnk,
2 const IID& iid, LPDWORD pdw);
3 ATLAPI AtlUnadvise(IUnknown* pUnkCP, const IID& iid, DWORD dw);
You use these functions like this:
1 DWORD dwCookie;
2 // Make a connection
3 hr = AtlAdvise(spSource, spSink, __uuidof(_ISpeakerEvents),
4 &dwCookie);
5 // ... Receive callbacks ...
6 // Break the connection
7 hr = AtlUnadvise(spSource, __uuidof(_ISpeakerEvents), dwCookie);
In summary, to establish a connection, you need an interface pointer for the connectable object, an interface pointer to an object that implements the sink interface, and the sink interface ID.
You commonly find connection points used by ActiveX controls. An ActiveX control is often a source of events. An event source implements event callbacks asmethod calls on a specified event sink interface. Typically, an ActiveX control container implements an event sink interface so that it can receive specific events from its contained controls. Using the connection points protocol, the control container establishes a connection from the source of events in the control (the connection point) to the event sink in the container. When an event occurs, the connection point calls the appropriate method of the sink interface for each sink connected to the connection point.
I should point out that the connection points design is deliberately a very general mechanism, which means that using connection points isn’t terribly efficient in some cases. [1] Connection points are most useful when an unknown number of clients might want to establish callbacks to a variety of different sink interfaces. In addition, the connection points protocol is well known; therefore, objects with no custom knowledge of each other can use it to establish connections. If you are writing both the source and sink objects, you might want to invent a custom protocol that is easier to use than the connection point protocol, to trade interface pointers.
Creating an ATL-Based Connectable Object
The Brilliant Example Problem That Produces Blinding Insight
Let’s create a Demagogue COM object that
represents a public speaker. The ATL-based CDemagogue class implements the ISpeaker interface. When asked to Speak, a speaker can whisper, talk, or yell his speech,
depending on the value of Volume.
1interface ISpeaker : IDispatch {
2 [propget, id(1)] HRESULT Volume([out, retval] long *pVal);
3 [propput, id(1)] HRESULT Volume([in] long newVal);
4 [propget, id(2)] HRESULT Speech([out, retval] BSTR *pVal);
5 [propput, id(2)] HRESULT Speech([in] BSTR newVal);
6 [id(3)] HRESULT Speak();
7};
Whispering, talking, and yelling generate event
notifications on the _ISpeakerEvents source dispatch
interface, and the recipients of the events hear the speech. Many
client components can receive event notifications only when the
source interface is a dispatch interface.
1dispinterface _ISpeakerEvents {
2properties:
3methods:
4 [id(1)] void OnWhisper(BSTR bstrSpeach);
5 [id(2)] void OnTalk(BSTR bstrSpeach);
6 [id(3)] void OnYell(BSTR bstrSpeach);
7};
The underscore prefix is a naming convention that causes many type library browsers to not display the interface name. Because an event interface is an implementation detail, typically you don’t want such interfaces displayed to the authors of scripting languages when they use your component.
Note that the Microsoft Interface Definition
Language (MIDL) compiler prefixes DIID_ to the name of a dispinterface when it generates its named globally unique
identifier (GUID). So DIID__ISpeakerEvents is the named
GUID for this interface.
Therefore, the following coclass definition describes a Demagogue. I’ve added an interface that lets
me name a particular Demagogue if I don’t like the default (which
is Demosthenes).
1coclass Demagogue {
2 [default] interface IUnknown;
3 interface ISpeaker;
4 interface INamedObject;
5 [default, source] dispinterface _ISpeakerEvents;
6};
I start with the CDemagogue class: an
ATL-based, single-threaded-apartment-resident, COM-createable
object to represent a Demagogue.
1class ATL_NO_VTABLE CDemagogue :
2 public CComObjectRootEx<CComSingleThreadModel>,
3 public CComCoClass<CDemagogue, &CLSID_Demagogue>,
4 public ISupportErrorInfo,
5 public IDispatchImpl<ISpeaker, &IID_ISpeaker,
6 &LIBID_ATLINTERNALSLib>,
7 ...
8{ ... };
Seven Steps to a Connectable Object
There are seven steps to creating a connectable object using ATL:
Implement the
IConnectionPointContainerinterface.Implement
QueryInterfaceso that it responds to requests forIID_IConnectionPointContainer.Implement the
IConnectionPointinterface for each source interface that the connectable object supports.Provide a connection map that is a table that associates an IID with a connection point implementation.
Update the
coclassdefinition for the connectable object class in your IDL file to specify each source interface. Each source interface must have the[source]attribute. The primary source interface should have the[default, source]attributes.Typically, you implement helper methods that call the sink methods for all connected sinks.
You must call the helper methods at the appropriate times.
Adding Required Base Classes to Your Connectable Object
For an object to fire events using the
connection points protocol, the object must be a connectable
object. This means that the object must implement the IConnectionPointContainer interface. You can use an
ATL-provided implementation of IConnectionPointContainer and IConnectionPoint by deriving the connectable object
class from the appropriate base classes.
Step 1. Derive the CDemagogue connectable
object class from the ATL template class IConnectionPointContainerImpl. This template class
requires one parameter, the name of your derived class. This
derivation provides the connectable object with an implementation
of the IConnectionPointContainer interface.
1class ATL_NO_VTABLE CDemagogue :
2 ...
3 public IConnectionPointContainerImpl<CDemagogue>,
4 ...
Changes to the COM_MAP for a Connectable Object
Step 2. Any time you add a new interface
implementation to a class, you should immediately add support for
the interface to your QueryInterface method. ATL
implements QueryInterface for an object by searching the
object’s COM_MAP for an entry matching the requested IID.
To indicate that the object supports the IConnectionPointContainer interface, add a COM_INTERFACE_ENTRY macro for the interface:
1BEGIN_COM_MAP(CDemagogue)
2...
3 COM_INTERFACE_ENTRY(IConnectionPointContainer)
4...
5END_COM_MAP ()
Adding Each Connection Point
A
connection point container needs a collection of connection points
to contain (otherwise, the container is somewhat boring as well as
misleading). For each source interface that the connectable object
supports, you need a connection point subobject. A connection point
subobject is logically a separate object (that is, its COM object
identity is unique) that implements the IConnectionPoint interface.
Step 3. To create connection point subobjects, you
derive your connectable object class from the template class IConnectionPointImpl one or more timesonce for each source
interface supported by the connectable object. This derivation
provides the connectable object with one or more implementations of
the IConnectionPoint interface on separately
reference-counted subobjects. The IConnectionPointImpl class requires three template parameters: the name of your
connectable object class, the IID of the connection point’s source
interface, and, optionally, the name of a class that manages the
connections.
1class ATL_NO_VTABLE CDemagogue :
2 ...
3 public IConnectionPointContainerImpl<CDemagogue>,
4 public IConnectionPointImpl<CDemagogue, &DIID__ISpeakerEvents>
5 ...
Where, O Where Are the Connection Points? Where, O Where Can They Be?
Any implementation of IConnectionPointContainer needs some fundamental
information: a list of connection point objects and the IID that
each connection point object supports. The ATL implementation uses
a table called a connection point
map in which you provide the required information. You
define a connection point map in your connectable object’s class
declaration using three ATL macros.
The BEGIN_CONNECTION_POINT_MAP macro
specifies the beginning of the table. The only parameter is the
class name of the connectable object. Each CONNECTION_POINT_ENTRY macro places an entry in the table
and represents one connection point. The macro’s only parameter is
the IID of the interface that the connection point supports.
Note that the CONNECTION_POINT_ENTRY macro requires you to specify an IID, whereas the COM_INTERFACE_ENTRY macro needs an interface class name.
Historically, you could always prepend an IID_ prefix to
an interface class name toproduce the name of the GUID for the interface.
Earlier versions of ATL’s COM_INTERFACE_ENTRY macro
actually did this to produce the appropriate IID.
However, source interfaces have no such regular
naming convention. Various versions of MFC, MKTYPLIB, and MIDL have
generated different prefixes to a dispinterface. The CONNECTION_POINT_ENTRY macro couldn’t assume a prefix, so
you had to specify the IID explicitly. By default, ATL uses the __uuidof keyword to obtain the IID for a class.
The END_CONNECTION_MAP macro generates
an end-of-table marker and code that returns the address of the
connection map as well as its size.
Step 4. Here’s the connection map for the CDemagogue class:
1BEGIN_CONNECTION_POINT_MAP(CDemagogue)
2 CONNECTION_POINT_ENTRY(__uuidof(_ISpeakerEvents))
3END_CONNECTION_POINT_MAP()
Update the coclass to Support the Source Interface
Step 5. Clients often read the type library, which
describes an object that is a source of events, to determine
certain implementation details, such as the object’s source
interface(s). You need to ensure that the source object’s coclass description is up-to-date by adding an entry to
describe the source interface.
1coclass Demagogue
2{
3 [default] interface IUnknown;
4 interface ISpeaker;
5 interface INamedObject;
6 [default, source] dispinterface _ISpeakerEvents;
7};
Where There Are Events, There Must Be Fire
So far, we have a Demagogue connectable object that is a container of connection points and one connection point. The implementation, as presented up to now, permits a client to register a callback interface with a connection point. All the enumerators will work. The client can even disconnect. However, the connectable object never issues any callbacks. This isn’t terribly useful and has been a bit of work for no significant gain, so we’d better finish things. A connectable object needs to call the sink interface methods, otherwise known as firing the events.
To fire an event, you call the appropriate event method of the sink interface for each sink interface pointer registered with a connection point. This task is complex enough that you’ll generally find it useful to add event-firing helper methods to yourconnectable object class. You have one helper method in your connectable object class for each method in each of your connection points’ supported interfaces.
You fire an event by calling the associated event method of a particular sink interface. You do this for each sink interface registered with the connection point. This means you need to iterate through a connection point’s list of sink interfaces and call the event method for each interface pointer. “How and where does a connection point maintain this list?” you ask. Good timing, I was about to get to that.
Each IConnectionPointImpl base class
object (which means each connection point) contains a member
variable m_vec that ATL declares as a vector of IUnknown pointers. However, you don’t need to call QueryInterface to get the appropriate sink interfaces out
of this collection; ATL’s implementation of IConnectionPointImpl::Advise has already performed this
query for you. For example, the vector in the connection point
associated with DIID_ISpeakerEvents actually contains _ISpeakerEvents pointers.
By default, m_vec is a CComDynamicUnkArray object, which is a dynamically
allocated array of IUnknown pointers, each a client sink
interface pointer for the connection point. The CComDynamicUnkArray class grows the vector as required, so
the default implementation provides an unlimited number of
connections.
Alternatively, when you declare the IConnectionPointImpl base class, you can specify that m_vec is a CComUnkArray object that holds a fixed
number of sink interface pointers. Use the CComUnkArray class when you want to support a fixed maximum number of
connections. ATL also provides an explicit template, CComUnkArray<1>, that is specialized for a single
connection
Step 6. To fire an event, you iterate through the
array and, for each
non- NULL entry,
call the sink interface method associated with the event you want
to fire. Here’s a simple helper method that fires the OnTalk event of the _ISpeakerEvents interface.Note that m_vec is unambiguous only when you have a single
connection point.
1HRESULT Fire_OnTalk(BSTR bstrSpeech)
2{
3 CComVariant arg, varResult;
4 int nIndex, nConnections = m_vec.GetSize();
5
6 for (nIndex = 0; nIndex < nConnections; nIndex++) {
7 CComPtr<IUnknown> sp = m_vec.GetAt(nIndex);
8 IDispatch* pDispatch =
9 reinterpret_cast<IDispatch*>(sp.p);
10 if (pDispatch != NULL) {
11 VariantClear(&varResult);
12 arg = bstrSpeech;
13 DISPPARAMS disp = { &arg, NULL, 1, 0 };
14 pDispatch->Invoke(0x2, IID_NULL, LOCALE_USER_DEFAULT,
15 DISPATCH_METHOD, &disp, &varResult, NULL, NULL);
16 }
17 }
18 return varResult.scode;
19}
The ATL Connection Point Proxy Generator
Writing the helper methods
that call a connection point interface method is tedious and prone
to errors. An additional complexity is that a sink interface can be
a custom COM interface or a dispinterface. Considerably
more work is involved in making a dispinterface callback
(that is, using IDispatch::Invoke) than making a vtable callback. Unfortunately, the dispinterface callback is the most frequent case because it’s the only event
mechanism that scripting languages, Internet Explorer, and most
ActiveX control containers support.
The Visual Studio 2005 IDE, however, provides a source codegeneration tool that generates a connection point class that contains all the necessary helper methods for making callbacks on a specific connection point interface. In the Visual Studio 2005 Class View pane, right-click on the C++ class that you want to be a source of events. Select the Add Connection Point menu item from the context menu. The Implement Connection Point Wizard appears (see Figure 9.3).
Figure 9.3. The Implement Connection Point dialog box
The
Implement Connection Point Wizard creates one or more classes
(declared and defined in the specified header file) that represent
the specified interface(s) and their methods. To use the code
generator, you must have a type library that describes the desired
event interface. The code generator reads the type library
description of an interface and generates a class, derived from IConnectionPointImpl, that contains an event-firing helper
function for each interface method. You specify the generated class
name as one of your connectable object’s base classes. This base
class implements a specific connection point and contains all
necessary event-firing helper methods.
The Implement Connection Point Proxy-Generated Code
The proxy generator produces a template class
with a name in the form CProxy_<SinkInterfaceName>.
This proxy class requires one parameter: your connectable object’s
class name. The proxy class derives from an IConnectionPointImpl template instantiation that specifies
your source interface.
Here is the code that the Implement Connection
Point Wizard generates for the previously described _ISpeakerEvents interface:
1#pragma once
2
3template<class T>
4class CProxy_ISpeakerEvents :
5 public IConnectionPointImpl<T, &__uuidof(_ISpeakerEvents)>
6{
7public:
8 HRESULT Fire_OnWhisper(BSTR bstrSpeech) {
9 HRESULT hr = S_OK;
10 T * pThis = static_cast<T*>(this);
11 int cConnections = m_vec.GetSize();
12
13 for (int iConnection = 0;
14 iConnection < cConnections;
15 iConnection++)
16 {
17 pThis->Lock();
18 CComPtr<IUnknown> punkConnection =
19 m_vec.GetAt(iConnection);
20 pThis->Unlock();
21
22 IDispatch * pConnection =
23 static_cast<IDispatch*>(punkConnection.p);
24
25 if (pConnection) {
26 CComVariant avarParams[1];
27 avarParams[0] = bstrSpeech;
28 DISPPARAMS params = { avarParams, NULL, 1, 0 };
29 hr = pConnection->Invoke(DISPID_ONWHISPER,
30 IID_NULL,
31 LOCALE_USER_DEFAULT,
32 DISPATCH_METHOD,
33 ¶ms, NULL, NULL, NULL);
34 }
35 }
36 return hr;
37 }
38
39// Other methods similar, deleted for clarity
40
41};
Using the Connection Point Proxy Code
Putting everything together
so far, here are the pertinent parts of the CDemagogue connectable object declaration. The only change from previous
examples is the use of the generated connection point proxy class, CProxy_ISpeakerEvents<CDemagogue>, as a base class
for the connection point instead of the more generic IConnectionPointImpl class.
1class ATL_NO_VTABLE CDemagogue :
2...
3 public IConnectionPointContainerImpl<CDemagogue>,
4 public CProxy_ISpeakerEvents<CDemagogue>, ... {
5
6BEGIN_COM_MAP(CDemagogue)
7...
8 COM_INTERFACE_ENTRY(IConnectionPointContainer)
9...
10END_COM_MAP()
11
12BEGIN_CONNECTION_POINT_MAP(CDemagogue)
13 CONNECTION_POINT_ENTRY(__uuidof(_ISpeakerEvents))
14END_CONNECTION_POINT_MAP()
15...
16};
Firing the Events
Step 7. The final step to make everything work is to fire each event at the appropriate time. When to do this is application-specific, but here is one example.
The CDemagogue object makes its speech when a client calls the Speak method. The Speak method, based on the current volume
property, either whispers, talks, or yells. It does this by calling
the OnWhisper, OnTalk, or OnYell event
methods, as appropriate, of all clients listening to the
demagogue’s _ISpeakerEvents interface.
1STDMETHODIMP CDemagogue::Speak() {
2 if (m_nVolume <= -100)
3 return Fire_OnWhisper(m_bstrSpeech);
4
5 if (m_nVolume >= 100)
6 return Fire_OnYell(m_bstrSpeech);
7
8 return Fire_OnTalk(m_bstrSpeech);
9}
Going the Last Meter/Mile, Adding One Last Bell
The changes described so far provide a complete implementation of the connection point protocol. However, one last change makes your connectable object easier for clients to use. A connectable object should provide convenient client access to information about the interfaces that the object supports.
More specifically, many clients that want to
receive events from a connectable object can ask the object for its IProvideClassInfo2 interface. Microsoft Internet Explorer,
Visual Basic, and ATL-based ActiveX control containers do this, for example. The client calls the GetGUID method of this interface with the parameter GUIDKIND_DEFAULT_SOURCE_DISP_IID to retrieve the IID of
the primary event disp-interface that the connectable
object supports. This is the IID of the dispinterface listed in the connectable object’s coclass description
with the [default, source] attributes.
Supporting IProvideClassInfo2 gives
arbitrary clients a convenient mechanism for determining the
primary event source IID and then using the IID to establish a
connection. Note that the IID returned by this call to GetGUID must be a dispinterface; it cannot be a
standard IUnknown-derived (vtable) interface.
When a connectable object fails the query for IProvideClassInfo2, some clients ask for IProvideClassInfo. A client can use this interface to
retrieve an ITypeInfo pointer about the connectable
object’s class. With a considerable bit of effort, a client can use
this ITypeInfo pointer and determine the default source
interface that the connectable object supports. The IProvideClassInfo2 interface derives from the IProvideClassInfo interface, so by implementing the first
interface, you’ve already implemented the second one.
Because most connectable objects should
implement the IProvideClassInfo2 interface, ATL provides a
template class for the implementation, IProvideClassInfo2Impl, which provides a default
implementation of all the IProvideClassInfo and IProvideClassInfo2 methods. The declaration of the class
looks like this:
1template <const CLSID* pcoclsid, const IID* psrcid,
2 const GUID* plibid = &CAtlModule::m_libid,
3 WORD wMajor = 1, WORD wMinor = 0,
4 class tihclass = CComTypeInfoHolder>
5class ATL_NO_VTABLE IProvideClassInfo2Impl
6 : public IProvideClassInfo2
7{ ... }
To use this implementation in your connectable
object, you must derive the connectable object class from the IProvideClassInfo2Impl class. The last two template
parameters in the following example are the major and minor version
numbers of the component’s type library. They default to 1 and 0,
respectively, so I didn’t need to list them. However, when you
change the type library’s version number, you also need to change
the numbers in the template invocation. You won’t get a compile
error if you fail to make the change, but things won’t work
correctly.
By always listing the version number explicitly, I remember to make this change more often:
1#define LIBRARY_MAJOR 1
2#define LIBRARY_MINOR 0
3
4class ATL_NO_VTABLE CDemagogue :
5...
6 public IConnectionPointContainerImpl<CDemagogue>,
7 public CProxy_ISpeakerEvents<CDemagogue>,
8 public IProvideClassInfo2Impl<&CLSID_Demagogue,
9 &__uuidof(_ISpeakerEvents),
10 &LIBID_ATLINTERNALSLib, LIBRARY_MAJOR, LIBRARY_MINOR>,
11...
12};
You also need to update the interface map so
that QueryInterface responds to IProvideClassInfo and IProvideClassInfo2.
1BEGIN_COM_MAP(CDemagogue)
2...
3 COM_INTERFACE_ENTRY(IProvideClassInfo2)
4 COM_INTERFACE_ENTRY(IProvideClassInfo)
5END_COM_MAP()
Finally, here are all connectable-object-related changes in one place:
1#define LIBRARY_MAJOR 1
2#define LIBRARY_MINOR 0
3
4// Event dispinterface
5dispinterface _ISpeakerEvents {
6properties:
7methods:
8 [id(1)] void OnWhisper(BSTR bstrSpeech);
9 [id(2)] void OnTalk(BSTR bstrSpeech);
10 [id(3)] void OnYell(BSTR bstrSpeech);
11};
12
13// Connectable object class
14coclass Demagogue {
15 [default] interface IUnknown;
16 interface ISpeaker;
17 interface INamedObject;
18 [default, source] dispinterface _ISpeakerEvents;
19};
20
21// Implementation class for coclass Demagogue
22class ATL_NO_VTABLE CDemagogue :
23...
24 public IConnectionPointContainerImpl<CDemagogue>,
25 public CProxy_ISpeakerEvents<CDemagogue>,
26 public IProvideClassInfo2Impl<&CLSID_Demagogue,
27 &__uuidof(_ISpeakerEvents),
28 &LIBID_ATLINTERNALSLib, LIBRARY_MAJOR, LIBRARY_MINOR>,
29 ... {
30public:
31BEGIN_COM_MAP(CDemagogue)
32 COM_INTERFACE_ENTRY(IConnectionPointContainer)
33 COM_INTERFACE_ENTRY(IProvideClassInfo2)
34 COM_INTERFACE_ENTRY(IProvideClassInfo)
35...
36END_COM_MAP()
37
38BEGIN_CONNECTION_POINT_MAP(CDemagogue)
39 CONNECTION_POINT_ENTRY(__uuidof(_ISpeakerEvents))
40END_CONNECTION_POINT_MAP()
41...
42};
Creating an Object That Is an Event Recipient
It is quite easy, in
theory, to implement an object that receives events on a single
interface. You define a class that implements the interface and
connect the object to the event source. We have a Demagogue class
that generates events on the _ISpeakerEvents dispatch
interface. Let’s define a CEarPolitic class (clearly one
ear of the body politic) that implements _ISpeakerEvents.
1coclass EarPolitic {
2 [default] dispinterface _ISpeakerEvents;
3};
Now, implement the class using ATL as the CEarPolitic class.
1class ATL_NO_VTABLE CEarPolitic :
2 ...
3 public _ISpeakerEvents,
4 ... {
5public:
6 ...
7BEGIN_COM_MAP(CEarPolitic)
8 COM_INTERFACE_ENTRY(IDispatch)
9 COM_INTERFACE_ENTRY(_ISpeakerEvents)
10...
11END_COM_MAP()
12
13// _ISpeakerEvents
14 STDMETHOD(GetTypeInfoCount)(UINT* pctinfo);
15 STDMETHOD(GetTypeInfo)(UINT itinfo, LCID lcid,
16 ITypeInfo** pptinfo);
17 STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames,
18 UINT cNames, LCID lcid, DISPID* rgdispid);
19 STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid, LCID lcid,
20 WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
21 EXCEPINFO* pexcepinfo, UINT* puArgErr);
22};
Unfortunately, an event interface is typically a dispinterface, so the normal interface-implementation
techniques don’t work. When you run the MIDL compiler, all you get
for the generated _ISpeakerEvents interface is this:
1MIDL_INTERFACE("A924C9DE-797F-430d-913D-93158AD2D801")
2_ISpeakerEvents : public IDispatch
3{
4};
Instead of being able to simply implement the
methods of the interface as regular C++ member functions, we must
implement the IDispatch interface and support, at a
minimum, the Invoke method. Invoke is a tedious
method to write for any nontrivial event interface.
Another alternative is to use the IDispatch interface implementation that the IDispatchImpl template class provides. Unfortunately, the
template class requires parameters describing a dual interface, not
a dispinterface. To use IDispatchImpl, you need
to define a dummy dual interface that has the same dispatch
methods, dispatch identifiers, and function signatures as the event dispinterface.
This has more implications than are usually
apparent. A dispinterface is not immutable, unlike a
regular COM interface. If you don’t control the definition of the dispinterface, it might change from release to release.
(It’s not that likely to change, but it is possible.) This means
your dual interface needs to change as well. This implies that you
cannot document the dual interface because, after it is published,
it is immutable because some client may implement it. Because you
should not describe the dual interface in a type library (because
that documents it), you cannot use the universal type
library-driven-marshaler and need a remoting proxy/stub for the
dual interface. These are all theoretical issues because the dual
interface, in this case, is an implementation detail that is
specific to the implementation class, but the issues give us
motivation enough to look for another solution.
On a slightly different note, what if you want
to receive the same events from more than one event source and you
want to know which source fired the event? For example, let’s say
you want to implement an EarPolitic class that acts as a
judge listening to _ISpeakerEvents from both a Defendant object and a Plaintiff object. Each
object is a source of OnWhisper, OnTalk, and OnYell events, but the judge needs to keep track of who is
saying what.
This requires you to implement the _ISpeakerEvents interface multiple times: once for each
event source. Providing separate implementations of any interface
multiple times in a class requires each implementation to be in a
separate COM identity. Two typical solutions to this problem are
member-wise composition (in which each implementation is in a
nested class) and something similar to tear-off interfaces (in
which each implementation is in a separate class).
The IDispEventImpl and IDispEventSimpleImpl Classes
To avoid these issues, ATL provides two template
classes, IDispEventImpl and IDispEventSimpleImpl,
that provide an implementation of the IDispatch interface
for an ATL COM object. Typically, you use one of these classes in
an object that wants to receive event callbacks. Both classes
implement the dispatch interface on a nested class object that
maintains a separate COM identity from the deriving class. This
means that you can derive from these classes multiple times when
you need to implement multiple source-dispatch interfaces.
The IDispEventImpl class requires a
type library that describes the dispatch interface. The class uses
the typeinfo at runtime to map the VARIANT parameters received in the Invoke method call to the
appropriate types and stack frame layout necessary for calling the
event-handler member function.
1template <UINT nID, class T, const IID* pdiid = &IID_NULL,
2 const GUID* plibid = &GUID_NULL,
3 WORD wMajor = 0, WORD wMinor = 0,
4 class tihclass = CComTypeInfoHolder>
5class ATL_NO_VTABLE IDispEventImpl :
6 public IDispEventSimpleImpl<nID, T, pdiid>
7{ ... }
The IDispEventSimpleImpl class doesn’t
use a type library, so it’s a more lightweight class. You use it
when you don’t have a type library or when you want to be more
efficient at runtime.
1template <UINT nID, class T, const IID* pdiid>
2class ATL_NO_VTABLE IDispEventSimpleImpl :
3 public _IDispEventLocator<nID, pdiid>
4{ ... }
When using the IDispEventSimpleImpl class, you must provide an _ATL_FUNC_INFO structure
containing information that describes the expected parameters for
the event handler.
1struct _ATL_FUNC_INFO {
2 CALLCONV cc; // Calling convention
3 VARTYPE vtReturn; // VARIANT type for return value
4 SHORT nParams; // Number of parameters
5 VARTYPE pVarTypes[_ATL_MAX_VARTYPES]; // Array of parameter
6 // VARIANT type
7};
Notice that IDispEventImpl derives from
the IDispEventSimpleImpl class. The IDispEventSimpleImpl class calls the event handler based
on the information in an _ATL_FUNC_INFO structure. You can
provide the structure statically (at compile time) by referencing
the structure in the sink map (described later in this
chapter).
When you provide no structure reference, the IDispEventSimpleImpl class calls the virtual method GetFuncInfoFromId to get an _ATL_FUNC_INFO structure for the event handler associated with the specified DISPID. You provide the structure dynamically by
overriding GetFuncInfoFromId and returning the appropriatestructure when called. You
must use GetFuncInfoFromId when you want to call different
event methods based on the locale provided by the event source.
Here’s the default implementation provided by
the IDispEventSimpleImpl class:
1//Helper for finding the function index for a DISPID
2virtual HRESULT GetFuncInfoFromId(const IID& iid,
3 DISPID dispidMember,
4 LCID lcid,
5 _ATL_FUNC_INFO& info) {
6 ATLTRACE(_T("TODO: Classes using IDispEventSimpleImpl "
7 "should override this method\n"));
8 ATLASSERT(0);
9 ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetFuncInfoFromId"));
10}
The IDispEventImpl class overrides this
virtual method to create the structure from the typeinfo in the specified type library.
Implementing an Event Sink
The easiest way
to implement one or more event sinks in a nonattributed ATL object
is to derive the object one or more times from IDispEventImpl once for each unique event interface coming
from each unique event source. Here’s the template class
specification once more:
1template <UINT nID, class T, const IID* pdiid = &IID_NULL,
2 const GUID* plibid = &GUID_NULL,
3 WORD wMajor = 0, WORD wMinor = 0,
4 class tihclass = CComTypeInfoHolder>
5class ATL_NO_VTABLE IDispEventImpl :
6 public IDispEventSimpleImpl<nID, T, pdiid>
7{ ... }
The nID parameter specifies an
identifier for the event source that is unique to the deriving
class T. You’ll see in Chapter 11, “ActiveX Controls,” that when the
event source is a contained control and the event recipient is a
composite control, the identifier is the contained control’s child
window identifier.
A composite control can default all other IDispEventImpl template parameters, but an arbitrary COM
object must specify all parameters except the last. The pdiid parameter specifies the GUID for the event dispinterface that this class implements. The dispinterface must be described in the type library
specified by the plibid parameter and the type library
major and minor version numbers, wMajor and wMinor. The tihclass parameter specifies the
class to manage the type information for the deriving class T. The default CComTypeInfoHolder class is
generally acceptable.
The more efficient way to implement one or more event
sinks in an ATL object uses the IDispEventSimpleImpl class
and needs no type library at runtime. However, you must provide the
necessary _ATL_FUNC_INFO structure, as described
previously. When using the IDispEventSimpleImpl class, you
need to specify only the nID event source identifier, the
deriving class T, and the pdiid GUID for the
event dispinterface:
1template <UINT nID, class T, const IID* pdiid>
2class ATL_NO_VTABLE IDispEventSimpleImpl :
3 public _IDispEventLocator<nID, pdiid>
4{ ... }
Using the easier technique, let’s redefine the CEarPolitic class to implement the _ISpeakerEvents dispatch interface twice: once for a
Demagogue acting as a Defendant and once for a different Demagogue
acting as a Plaintiff. We have a type library, so we use the IDispEventImpl class to implement the sink for the
Defendant object’s _ISpeakerEvents callbacks. We use the IDispEventSimpleImpl class for the Plaintiff object’s _ISpeakerEvents callbacks to demonstrate the alternative
implementation. We typically introduce a typedef for each event
interface implementation, to minimize typing and mistakes.
1static const int DEFENDANT_SOURCE_ID = 0 ;
2static const int PLAINTIFF_SOURCE_ID = 1 ;
3
4class CEarPolitic;
5
6typedef IDispEventImpl<DEFENDANT_SOURCE_ID,
7 CEarPolitic,
8 &__uuidof(_ISpeakerEvents),
9 &LIBID_ATLINTERNALSLib,
10 LIBRARY_MAJOR, LIBRARY_MINOR> DefendantEventImpl;
11
12typedef IDispEventSimpleImpl<PLAINTIFF_SOURCE_ID,
13 CEarPolitic, &__uuidof(_ISpeakerEvents)> PlaintiffEventImpl;
In this example, we arbitrarily chose 0 and 1
for the event source identifiers. The identifiers could have been
any numbers. Now, we need to derive the CEarPolitic class
from the two event-implementation classes:
1class ATL_NO_VTABLE CEarPolitic :
2 ...
3 public DefendantEventImpl,
4 public PlaintiffEventImpl {
5
6// Event sink map required in here
7
8};
The Event Sink Map
The IDispEventSimpleImpl class’s implementation of the Invoke method receives the event callbacks. When the event
source calls the Invoke method, it specifies the event
that has occurred using the DISPID parameter. The IDispEventSimpleImpl implementation searches an event sink
table for the function to call when event DISPID occurs on
dispatch interface DIID from event source identifier SOURCE.
You specify the beginning of the event sink map
using the BEGIN_SINK_MAP macro within the declaration of
the class that derives from IDispEventImpl or IDispEventSimpleImpl. You map each unique SOURCE/DIID/DISPID TRiple to the proper event-handling
method using the SINK_ENTRY, SINK_ENTRY_EX, and SINK_ENTRY_INFO macros.
SINK_ENTRY(SOURCE, DISPID, func). Use this macro in a composite control to name the handler for the specified event in the specified contained control’s default source interface. This macro assumes the use ofIDispEventImpland assumes that you call theAtlAdviseSinkMapfunction to establish the connection. TheAtlAdviseSinkMapfunction assumes that your class is derived fromCWindow. All in all, theSINK_ENTRYmacro isn’t very useful for nonuser-interface (UI) objects that want to receive events.SINK_ENTRY_EX(SOURCE, DIID, DISPID, func). Use this macro to indicate the handler for the specified event in the specified object’s specified source interface. This macro is most useful for non-UI objects that want to receive events and for composite controls that want to receive events from a contained control’s nondefault source interface.SINK_ENTRY_INFO(SOURCE, DIID, DISPID, func, info). This macro is similar to theSINK_ENTRY_EXmacro, with the addition that you can specify the_ATL_FUNC_INFOstructure to be used when calling the event handler. You typically use this macro when using theIDispEventSimpleImplclass.
You end the event sink map using the END_SINK_MAP macro.
The general form of the table is as follows:
1BEGIN_SINK_MAP(CEarPolitic)
2 SINK_ENTRY_EX(SOURCE, DIID, DISPID, EventHandlerFunc)
3 SINK_ENTRY_INFO(SOURCE, DIID, DISPID, EventHandlerFunc, &info)
4...
5END_SINK_MAP()
In the CEarPolitic example, there are
three events, all from the same dispatch interface but coming from
two different event sources. Therefore, we need six event sink map
entries. We can use the SINK_ENTRY_EX macro to identify
the event handlers for the Defendant event source. We
don’t need to specify the _ATL_FUNC_INFO structure because
the IDispEventImpl base class uses the type library to
provide the appropriate structure at runtime. We need to use the SINK_ENTRY_INFO macro for the Plaintiff object.
Because we used the IDisp-EventSimpleImpl base class, we
need to provide the function information structure describing each
event method.
Each function information structure describes
one event method. The structure contains the calling convention,
the VARIANT type of the return value of the method, the
number of parameters, and the VARIANT types of each of the
parameters. The calling convention should be set to CC_STDCALL because that’s what the IDispEventSimpleImpl class expects the event handlers to
use.
Here are the function prototypes for the Plaintiff’s three event methods and their information
structures (all identical in this example). Event handlers
typically do not return a value – that is, they are void functions. Note: The proper vtreturn value in the _ATL_FUNC_INFO structure to
represent a void function return is VT_EMPTY, not VT_VOID.
1void __stdcall OnHearPlaintiffWhisper(BSTR bstrText);
2void __stdcall OnHearPlaintiffTalk(BSTR bstrText);
3void __stdcall OnHearPlaintiffYell(BSTR bstrText);
4
5static const int DISPID_WHISPER = 1 ;
6static const int DISPID_TALK = 2 ;
7static const int DISPID_YELL = 3 ;
8
9_ATL_FUNC_INFO OnHearWhisperInfo = {
10 CC_STDCALL, VT_EMPTY, 1, { VT_BSTR }};
11_ATL_FUNC_INFO OnHearTalkInfo = {
12 CC_STDCALL, VT_EMPTY, 1, { VT_BSTR }};
13_ATL_FUNC_INFO OnHearYellInfo = {
14 CC_STDCALL, VT_EMPTY, 1, { VT_BSTR }};
Here’s the event sink map for the CEarPolitic object:
1BEGIN_SINK_MAP(CEarPolitic)
2 SINK_ENTRY_EX(DEFENDANT_SOURCE_ID, __uuidof(_ISpeakerEvents),
3 DISPID_WHISPER, OnHearDefendantWhisper)
4 SINK_ENTRY_EX(DEFENDANT_SOURCE_ID, __uuidof(_ISpeakerEvents),
5 DISPID_TALK, OnHearDefendantTalk)
6 SINK_ENTRY_EX(DEFENDANT_SOURCE_ID, __uuidof(_ISpeakerEvents),
7 DISPID_YELL, OnHearDefendantYell)
8 SINK_ENTRY_INFO(PLAINTIFF_SOURCE_ID, __uuidof(_ISpeakerEvents),
9 DISPID_WHISPER, OnHearPlaintiffWhisper, &OnHearWhisperInfo)
10 SINK_ENTRY_INFO(PLAINTIFF_SOURCE_ID, __uuidof(_ISpeakerEvents),
11 DISPID_TALK, OnHearPlaintiffTalk, &OnHearTalkInfo)
12 SINK_ENTRY_INFO(PLAINTIFF_SOURCE_ID, __uuidof(_ISpeakerEvents),
13 DISPID_YELL, OnHearPlaintiffYell, &OnHearYellInfo)
14END_SINK_MAP()
One caution: The sink map contains a hard-coded DISPID for each event.
This means that the sink map technically is specific to a
particular dispinterface version. COM allows the DISPID s in a dispinterface to change from version
to version. Now, this isn’t something that often happensand a
control vendor that does such a thing is just asking for tech
support calls and angry customers. But it is allowed.
The only absolutely correct way to obtain a DISPID is to ask the object implementing the dispinterface
at startup for the DISPID corresponding to an event. For a
source interface, this is impossible because when you build the
sink, you’re implementing the source interface, so there’s no
object to query. The only realistic option is to read the object’s
type library at runtime. ATL, Visual Basic, and MFC don’t support
this. They all assume that the DISPID s in a dispinterface will never change, as a questionable
performance optimization.
The Callback Methods
The callback method specified by the sink entry
macros must use the __stdcall calling convention. The
parameters for each callback method specified in the sink map must
agree in type and number with the corresponding event method as
described in the type library. Here are the Defendant’s
event methods; they are identical to the Plaintiff’s, as
expected:
1void __stdcall OnHearDefendantWhisper(BSTR bstrText);
2void __stdcall OnHearDefendantTalk(BSTR bstrText);
3void __stdcall OnHearDefendantYell(BSTR bstrText);
After two remaining steps, the CEarPolitic class will be complete: Implement the callback
methods and establish the connection between a CEarPolitic instance and a Demagogue (which invokes the _ISpeakerEvents methods).
We use the following simple implementation for each of the event handlers:
1void __stdcall CEarPolitic::OnHearDefendantTalk(BSTR bstrText) {
2 CComBSTR title ;
3 CreateText(title, OLESTR("defendant"),
4 OLESTR("talking"), m_defendant);
5
6 MessageBox(NULL, COLE2CT(bstrText), COLE2CT(title), MB_OK);
7}
8
9void CEarPolitic::CreateText(CComBSTR& bstrText,
10 LPCOLESTR strRole,
11 LPCOLESTR strAction,
12 LPUNKNOWN punk) {
13 bstrText = m_bstrName;
14 bstrText += OLESTR(" hears the ");
15 bstrText += strRole;
16 bstrText += OLESTR(" (");
17
18 CComQIPtr<INamedObject> spno = punk;
19 CComBSTR bstrName;
20 HRESULT hr = spno->get_Name (&bstrName) ;
21
22 bstrText.AppendBSTR(bstrName);
23 bstrText += OLESTR(") ");
24
25 bstrText += strAction;
26}
Connecting the Event Sink to an Event Source
When your class is a composite control, you
should use the AtlAdviseSinkMap function to establish and
remove the connections between all the source interfaces of the
contained controls listed in the sink map and your IDispEventImpl implementation(s). This method uses the
event source identifier as a child window identifier. Using the CWindow::GetDlgItem method, AtlAdviseSinkMap navigates to a child window handle and, from there, to the
contained control’s IUnknown interface. From the IUnknown interface, it gets the IConnectionPointContainer interface and the appropriate
connection point, and calls its Advise method.
1template <class T>
2inline HRESULT AtlAdviseSinkMap(T* pT, bool bAdvise);
You must use the AtlAdviseSinkMap function to establish the connections any
time you use the IDispEventImpl class and you specify only
the first two template parameters, using default values for the
rest. Not specifying the source interface implies using the default
source interface for the event source. The AtlAdvise-SinkMap method determines the default source
interface, if unspecified, for each event source and establishes
the connection point to that interface.
When your class isn’t a composite control, as in
the ongoing example, you must explicitly call the DispEventAdvise method of each of your IDispEventSimpleImpl (or derived) base classes to connect
each event source to each event sink implementation. The pUnk parameter to the DispEventAdvise method is
any interface on the event source; the piid parameter is
the desired source dispatch interface GUID. The DispEventUnadvise method breaks the connection.
1template <UINT nID, class T, const IID* pdiid>
2class ATL_NO_VTABLE IDispEventSimpleImpl : ... {
3...
4 HRESULT DispEventAdvise(IUnknown* pUnk, const IID* piid);
5 HRESULT DispEventUnadvise(IUnknown* pUnk, const IID* piid);
6...
7}
Here is the IListener interface. We
added it to the EarPolitic coclass to provide a means of
determining whether a COM object can listen to a Defendant and a Plaintiff. It also provides the ListenTo and StopListening methods to establish and break the
connection point between a Speaker event source and the Defendant or Plaintiff event sink.
1interface IListener : IDispatch {
2 typedef enum SpeakerRole { Defendant, Plaintiff } SpeakerRole;
3
4 [id(1)] HRESULT ListenTo(SpeakerRole role, IUnknown* pSpeaker);
5 [id(2)] HRESULT StopListening(SpeakerRole role);
6};
The implementation of these methods is
straightforward. ListenTo calls the DispEventAdvise method on the appropriate event sink to
establish the connection:
1STDMETHODIMP CEarPolitic::ListenTo(SpeakerRole role,
2 IUnknown *pSpeaker) {
3 HRESULT hr = StopListening(role) ; // Validates role
4 if (FAILED(hr)) return hr ;
5
6 switch (role) {
7 case Defendant:
8 hr = DefendantEventImpl::DispEventAdvise (pSpeaker,
9 &DIID__ISpeakerEvents);
10 if (SUCCEEDED(hr))
11 m_defendant = pSpeaker;
12 else
13 Error(OLESTR("The defendant does not support listening"),
14 __uuidof(IListener), hr);
15 break;
16
17 case Plaintiff:
18 hr = PlaintiffEventImpl::DispEventAdvise (pSpeaker,
19 &DIID__ISpeakerEvents);
20 if (SUCCEEDED(hr))
21 m_plaintiff = pSpeaker;
22 else
23 Error(OLESTR("The Plaintiff does not support listening"),
24 __uuidof(IListener), hr);
25 break;
26 }
27 return hr;
28}
The StopListening method calls DispEventUnadvise to
break the connection:
1STDMETHODIMP CEarPolitic::StopListening(SpeakerRole role) {
2 HRESULT hr = S_OK;
3 switch (role) {
4 case Defendant:
5 if (m_defendant)
6 hr = DefendantEventImpl::DispEventUnadvise (m_defendant,
7 &DIID__ISpeakerEvents);
8
9 if (FAILED(hr))
10 Error (OLESTR("Unexpected error trying to stop listening "
11 "to the defendant"), __uuidof(IListener), hr);
12
13 m_defendant = NULL;
14 break;
15
16 case Plaintiff:
17 if (m_plaintiff)
18 hr = PlaintiffEventImpl::DispEventUnadvise(m_plaintiff,
19 &DIID__ISpeakerEvents);
20 if (FAILED(hr))
21 Error(OLESTR("Unexpected error trying to stop listening "
22 "to the Plaintiff"), __uuidof(IListener), hr);
23
24 m_plaintiff = NULL;
25 break;
26
27 default:
28 hr = E_INVALIDARG;
29 break;
30 }
31
32 return hr;
33}
In
summary, use the IDispEventImpl and IDispEventSimpleImpl classes to implement an event sink
for a dispatch interface. Call the DispEventAdvise and DispEventUnadvise methods of each class to establish and
break the connection.
Derive your class directly from the source
interface when the source is a simple COM interface. Call the AtlAdvise and AtlUnadvise global functions to
establish and break the connection. When you need to implement the
same source interface multiple times, use one of the various
standard techniques (nested composition, method coloring, separate
classes, or intermediate base classes) to avoid name
collisions.
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.
1template <class T>
2class ATL_NO_VTABLE IConnectionPointContainerImpl
3 : public IconnectionPointContainer {
4 typedef CComEnum<IEnumConnectionPoints,
5 &__uuidof(IEnumConnectionPoints), IConnectionPoint*,
6 _CopyInterface<IConnectionPoint> >
7 CComEnumConnectionPoints;
8public:
9 STDMETHOD(EnumConnectionPoints)(
10 IEnumConnectionPoints** ppEnum) {
11 if (ppEnum == NULL) return E_POINTER;
12
13 *ppEnum = NULL;
14 CComEnumConnectionPoints* pEnum = NULL;
15 ATLTRY(pEnum = new CComObject<CComEnumConnectionPoints>)
16 if (pEnum == NULL) return E_OUTOFMEMORY;
17
18 int nCPCount;
19 const _ATL_CONNMAP_ENTRY* pEntry = T::GetConnMap(&nCPCount);
20
21 // allocate an initialize a vector of connection point
22 // object pointers
23 USES_ATL_SAFE_ALLOCA;
24 if ((nCPCount < 0) || (nCPCount >
25 (INT_MAX / sizeof(IConnectionPoint*))))
26 return E_OUTOFMEMORY;
27 size_t nBytes=0;
28 HRESULT hr=S_OK;
29 if( FAILED(hr=::ATL::AtlMultiply(&nBytes,
30 sizeof(IConnectionPoint*),
31 static_cast<size_t>(nCPCount)))) {
32 return hr;
33 }
34 IConnectionPoint** ppCP =
35 (IConnectionPoint**)_ATL_SAFE_ALLOCA(
36 nBytes, _ATL_SAFE_ALLOCA_DEF_THRESHOLD);
37 if (ppCP == NULL) {
38 delete pEnum;
39 return E_OUTOFMEMORY;
40 }
41
42 int i = 0;
43 while (pEntry->dwOffset != (DWORD_PTR)-1) {
44 if (pEntry->dwOffset == (DWORD_PTR)-2) {
45 pEntry++;
46 const _ATL_CONNMAP_ENTRY* (*pFunc)(int*) =
47 (const _ATL_CONNMAP_ENTRY* (*)(int*))(
48 pEntry->dwOffset);
49 pEntry = pFunc(NULL);
50 continue;
51 }
52 ppCP[i++] = (IConnectionPoint*)(
53 (INT_PTR)this+pEntry->dwOffset);
54 pEntry++;
55 }
56
57 // copy the pointers: they will AddRef this object
58 HRESULT hRes = pEnum->Init((IConnectionPoint**)&ppCP[0],
59 (IConnectionPoint**)&ppCP[nCPCount],
60 reinterpret_cast<IConnectionPointContainer*>(this),
61 AtlFlagCopy);
62 if (FAILED(hRes)) {
63 delete pEnum;
64 return hRes;
65 }
66 hRes = pEnum->QueryInterface(
67 __uuidof(IEnumConnectionPoints),
68 (void**)ppEnum);
69 if (FAILED(hRes)) delete pEnum;
70 return hRes;
71 }
72
73 STDMETHOD(FindConnectionPoint)(REFIID riid,
74 IConnectionPoint** ppCP) {
75 if (ppCP == NULL) return E_POINTER;
76 *ppCP = NULL;
77 HRESULT hRes = CONNECT_E_NOCONNECTION;
78 const _ATL_CONNMAP_ENTRY* pEntry = T::GetConnMap(NULL);
79 IID iid;
80 while (pEntry->dwOffset != (DWORD_PTR)-1) {
81 if (pEntry->dwOffset == (DWORD_PTR)-2) {
82 pEntry++;
83 const _ATL_CONNMAP_ENTRY* (*pFunc)(int*) =
84 (const _ATL_CONNMAP_ENTRY* (*)(int*))(
85 pEntry->dwOffset);
86 pEntry = pFunc(NULL);
87 continue;
88 }
89 IConnectionPoint* pCP =
90 (IConnectionPoint*)((INT_PTR)this+pEntry->dwOffset);
91 if (SUCCEEDED(pCP->GetConnectionInterface(&iid)) &&
92 InlineIsEqualGUID(riid, iid)) {
93 *ppCP = pCP;
94 pCP->AddRef();
95 hRes = S_OK;
96 break;
97 }
98 pEntry++;
99 }
100 return hRes;
101 }
102};
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
class – for 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> pointerbecause 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 AppendixD, “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:
1template <class T, const IID* piid,
2 class CDV = CComDynamicUnkArray >
3class ATL_NO_VTABLE IConnectionPointImpl
4 : public _ICPLocator<piid>
5{ ... }
The _ICPLocator Class
1template <const IID* piid>
2class ATL_NO_VTABLE _ICPLocator {
3public:
4 //this method needs a different name than QueryInterface
5 STDMETHOD(_LocCPQueryInterface)(REFIID riid,
6 void ** ppvObject) = 0;
7 virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
8 virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
9};
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.
1template <class T, const IID* piid,
2 class CDV = CComDynamicUnkArray >
3class ATL_NO_VTABLE IConnectionPointImpl
4 : public _ICPLocator<piid> {
5 typedef CComEnum<IEnumConnections,
6 &__uuidof(IEnumConnections), CONNECTDATA,
7 _Copy<CONNECTDATA> > CComEnumConnections;
8 typedef CDV _CDV;
9public:
10 ~IConnectionPointImpl();
11 STDMETHOD(_LocCPQueryInterface)(REFIID riid, void ** ppvObject) {
12#ifndef _ATL_OLEDB_CONFORMANCE_TESTS
13 ATLASSERT(ppvObject != NULL);
14#endif
15 if (ppvObject == NULL)
16 return E_POINTER;
17 *ppvObject = NULL;
18
19 if (InlineIsEqualGUID(riid, __uuidof(IConnectionPoint)) ||
20 InlineIsEqualUnknown(riid)) {
21 *ppvObject = this;
22 AddRef();
23#ifdef _ATL_DEBUG_INTERFACES
24 _AtlDebugInterfacesModule.AddThunk((IUnknown**)ppvObject,
25 _T("IConnectionPointImpl"), riid);
26#endif // _ATL_DEBUG_INTERFACES
27 return S_OK;
28 }
29 else
30 return E_NOINTERFACE;
31 }
32
33 STDMETHOD(GetConnectionInterface)(IID* piid2) {
34 if (piid2 == NULL)
35 return E_POINTER;
36 *piid2 = *piid;
37 return S_OK;
38 }
39 STDMETHOD(GetConnectionPointContainer)(
40 IConnectionPointContainer** ppCPC) {
41 T* pT = static_cast<T*>(this);
42 // No need to check ppCPC for NULL since
43 // QI will do that for us
44 return pT->QueryInterface(
45 __uuidof(IConnectionPointContainer), (void**)ppCPC);
46 }
47
48 STDMETHOD(Advise)(IUnknown* pUnkSink, DWORD* pdwCookie);
49 STDMETHOD(Unadvise)(DWORD dwCookie);
50 STDMETHOD(EnumConnections)(IEnumConnections** ppEnum);
51 CDV m_vec;
52};
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 class – for 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:
1T* pT = static_cast<T*>(this);
2return pT->QueryInterface(IID_IConnectionPointContainer,
3 (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
1template <class T, const IID* piid, class CDV>
2STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(
3 IUnknown* pUnkSink,
4 DWORD* pdwCookie) {
5 T* pT = static_cast<T*>(this);
6 IUnknown* p;
7 HRESULT hRes = S_OK;
8 if (pdwCookie != NULL)
9 *pdwCookie = 0;
10 if (pUnkSink == NULL || pdwCookie == NULL)
11 return E_POINTER;
12 IID iid;
13 GetConnectionInterface(&iid);
14 hRes = pUnkSink->QueryInterface(iid, (void**)&p);
15 if (SUCCEEDED(hRes)) {
16 pT->Lock();
17 *pdwCookie = m_vec.Add(p);
18 hRes = (*pdwCookie != NULL) ? S_OK :
19 CONNECT_E_ADVISELIMIT;
20 pT->Unlock();
21 if (hRes != S_OK)
22 p->Release();
23 }
24 else if (hRes == E_NOINTERFACE)
25 hRes = CONNECT_E_CANNOTCONNECT;
26 if (FAILED(hRes))
27 *pdwCookie = 0;
28 return hRes;
29}
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
1template <class T, const IID* piid, class CDV>
2STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Unadvise(
3 DWORD dwCookie) {
4 T* pT = static_cast<T*>(this);
5 pT->Lock();
6 IUnknown* p = m_vec.GetUnknown(dwCookie);
7 HRESULT hRes = m_vec.Remove(dwCookie) ? S_OK :
8 CONNECT_E_NOCONNECTION;
9 pT->Unlock();
10 if (hRes == S_OK && p != NULL)
11 p->Release();
12 return hRes;
13}
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.
1template <class T, const IID* piid, class CDV>
2STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::EnumConnections(
3 IEnumConnections** ppEnum) {
4 if (ppEnum == NULL)
5 return E_POINTER;
6 *ppEnum = NULL;
7 CComObject<CComEnumConnections>* pEnum = NULL;
8 ATLTRY(pEnum = new CComObject<CComEnumConnections>)
9 if (pEnum == NULL)
10 return E_OUTOFMEMORY;
11 T* pT = static_cast<T*>(this);
12 pT->Lock();
13 CONNECTDATA* pcd = NULL;
14 ATLTRY(pcd = new CONNECTDATA[m_vec.end()-m_vec.begin()])
15 if (pcd == NULL) {
16 delete pEnum;
17 pT->Unlock();
18 return E_OUTOFMEMORY;
19 }
20 CONNECTDATA* pend = pcd;
21 // Copy the valid CONNECTDATA's
22 for (IUnknown** pp = m_vec.begin();pp<m_vec.end();pp++) {
23 if (*pp != NULL)
24 {
25 (*pp)->AddRef();
26 pend->pUnk = *pp;
27 pend->dwCookie = m_vec.GetCookie(pp);
28 pend++;
29 }
30 }
31 // don't copy the data, but transfer ownership to it
32 pEnum->Init(pcd, pend, NULL, AtlFlagTakeOwnership);
33 pT->Unlock();
34 HRESULT hRes = pEnum->_InternalQueryInterface(
35 __uuidof(IEnumConnections), (void**)ppEnum);
36 if (FAILED(hRes))
37 delete pEnum;
38 return hRes;
39}
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. [2]
EnumConnections uses _InternalQueryInterface instead of IUnknown::QueryInterface because the latter
results in a virtual function call, whereas the former is a more
efficient direct call.
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.
1class ATL_NO_VTABLE _IDispEvent {
2...
3public:
4 //this method needs a different name than QueryInterface
5 STDMETHOD(_LocDEQueryInterface)(REFIID riid,
6 void ** ppvObject) = 0;
7 virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
8 virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
9...
10};
The class maintains five member variables, only
one of which is always used, m_dwEventCookie. Each member
variable is initialized by the constructor:
1_IDispEvent() :
2 m_libid(GUID_NULL),
3 m_iid(IID_NULL),
4 m_wMajorVerNum(0),
5 m_wMinorVerNum(0),
6 m_dwEventCookie(0xFEFEFEFE)
7{ }
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. [3]
Note
that this places a constraint on a connection list implementation
(that is, the CDV template parameter class)namely, that it
never provides the value 0xFEFEFEFE as a connection
cookie.
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.
1GUID m_libid;
2IID m_iid;
3unsigned short m_wMajorVerNum;
4unsigned short m_wMinorVerNum;
5DWORD 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.
1HRESULT DispEventAdvise(IUnknown* pUnk, const IID* piid) {
2 ATLENSURE(m_dwEventCookie == 0xFEFEFEFE);
3 return AtlAdvise(pUnk, (IUnknown*)this, *piid, &m_dwEventCookie);
4}
5
6HRESULT DispEventUnadvise(IUnknown* pUnk, const IID* piid) {
7 HRESULT hr = AtlUnadvise(pUnk, *piid, m_dwEventCookie);
8 m_dwEventCookie = 0xFEFEFEFE;
9 return hr;
10}
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. [4]
Keith
Brown pointed out that it would have been more appropriate to call
these Locator classes COM-Identity classes – for example, _IDispEventCOMIdentity and IConnectionPointCOMIdentity. Their fundamental purpose is
to provide a unique base class instance for each required COM
identity. Yes, you need to locate the appropriate base class when
needed, but the sole purpose in renaming the QueryInterface method is to implement a separate
identity.
1template <UINT nID, const IID* piid>
2class ATL_NO_VTABLE _IDispEventLocator : public _IDispEvent {
3public:
4};
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.
1template <UINT nID, class T, const IID* pdiid>
2class ATL_NO_VTABLE IDispEventSimpleImpl :
3 public _IDispEventLocator<nID, pdiid> {
4// Abbreviated for clarity
5 STDMETHOD(_LocDEQueryInterface)(REFIID riid,
6 void ** ppvObject);
7 virtual ULONG STDMETHODCALLTYPE AddRef();
8 virtual ULONG STDMETHODCALLTYPE Release();
9 STDMETHOD(GetTypeInfoCount)(UINT* pctinfo);
10 STDMETHOD(GetTypeInfo)(UINT itinfo, LCID lcid,
11 ITypeInfo** pptinfo);
12 STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames,
13 UINT cNames, LCID lcid, DISPID* rgdispid);
14 STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid,
15 LCID lcid, WORD wFlags, DISPPARAMS* pdispparams,
16 VARIANT* pvarResult, EXCEPINFO* pexcepinfo,
17 UINT* puArgErr);
18};
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.
1 STDMETHOD(_LocDEQueryInterface)(REFIID riid,
2 void ** ppvObject) {
3 ATLASSERT(ppvObject != NULL);
4 if (ppvObject == NULL)
5 return E_POINTER;
6 *ppvObject = NULL;
7
8 if (InlineIsEqualGUID(riid, IID_NULL))
9 return E_NOINTERFACE;
10
11 if (InlineIsEqualGUID(riid, *pdiid) ||
12 InlineIsEqualUnknown(riid) ||
13 InlineIsEqualGUID(riid, __uuidof(IDispatch)) ||
14 InlineIsEqualGUID(riid, m_iid)) {
15
16 *ppvObject = this;
17 AddRef();
18#ifdef _ATL_DEBUG_INTERFACES
19 _AtlDebugInterfacesModule.AddThunk(
20 (IUnknown**)ppvObject, _T("IDispEventImpl"),
21 riid);
22#endif // _ATL_DEBUG_INTERFACES
23 return S_OK;
24 }
25 else
26 return E_NOINTERFACE;
27 }
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.
1template <UINT nID, class T, const IID* pdiid>
2class ATL_NO_VTABLE IDispEventSimpleImpl : ... {
3...
4 virtual ULONG STDMETHODCALLTYPE AddRef() { return 1; }
5 virtual ULONG STDMETHODCALLTYPE Release() { return 1; }
6...
7};
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.
1STDMETHOD(GetTypeInfoCount)(UINT*)
2{ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetTypeInfoCount"));}
3
4STDMETHOD(GetTypeInfo)(UINT, LCID, ITypeInfo**)
5{ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetTypeInfo"));}
6
7STDMETHOD(GetIDsOfNames)(REFIID, LPOLESTR*, UINT, LCID, DISPID*)
8{ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetIDsOfNames"));}
9
10STDMETHOD(Invoke)(DISPID dispidMember, REFIID, LCID lcid, WORD /*wFlags*/,
11 DISPPARAMS* pdispparams, VARIANT* pvarResult,
12 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:
1virtual HRESULT GetFuncInfoFromId(const IID&, DISPID, LCID,
2 _ATL_FUNC_INFO&) {
3 ATLTRACE(_T("TODO: Classes using IDispEventSimpleImpl should "
4 "override this method\n"));
5 ATLASSERT(0);
6 ATLTRACENOTIMPL(_T("IDispEventSimpleImpl::GetFuncInfoFromId"));
7}
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):
1// Helpers for sinking events on random IUnknown*
2 HRESULT DispEventAdvise(IUnknown* pUnk, const IID* piid) {
3 ATLENSURE(m_dwEventCookie == 0xFEFEFEFE);
4 return AtlAdvise(pUnk, (IUnknown*)this, *piid, &m_dwEventCookie);
5 }
6 HRESULT DispEventUnadvise(IUnknown* pUnk, const IID* piid) {
7 HRESULT hr = AtlUnadvise(pUnk, *piid, m_dwEventCookie);
8 m_dwEventCookie = 0xFEFEFEFE;
9 return hr;
10 }
The next two methods establish and break a connection between the current sink and the specified event source using the sink’s dispatch interface:
1HRESULT DispEventAdvise(IUnknown* pUnk) {
2 return _IDispEvent::DispEventAdvise(pUnk, pdiid);
3}
4HRESULT DispEventUnadvise(IUnknown* pUnk) {
5 return _IDispEvent::DispEventUnadvise(pUnk, pdiid);
6}
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:
|
The event source identifier; control ID for contained controls |
|
The source dispatch interface IID |
|
The offset of the event sink implementation from the deriving class |
|
The event callback dispatch ID |
|
The member function pointer of the event handler to invoke [5] |
|
The |
If
you’ve been wondering why your event-handler functions have to use
the __stdcall calling convention; the declaration of this
pointer specifies it.
1template <class T>
2struct _ATL_EVENT_ENTRY {
3 UINT nControlID; // ID identifying object instance
4 const IID* piid; // dispinterface IID
5 int nOffset; // offset of dispinterface from this pointer
6 DISPID dispid; // DISPID of method/property
7 void (__stdcall T::*pfn)(); // method to invoke
8 _ATL_FUNC_INFO* pInfo; // pointer to info structure
9};
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.
1#define BEGIN_SINK_MAP(_class)\
2 typedef _class _GetSinkMapFinder;\
3 static const ATL::_ATL_EVENT_ENTRY<_class>* _GetSinkMap() {\
4 PTM_WARNING_DISABLE \
5 typedef _class _atl_event_classtype;\
6 static const ATL::_ATL_EVENT_ENTRY<_class> map[] = {
Each SINK_ENTRY_INFO macro adds one _ATL_EVENT_ENTRY structure to the array.
1#define SINK_ENTRY_INFO(id, iid, dispid, fn, info) {id, &iid,
2 (int)(INT_PTR)(static_cast<ATL::_IDispEventLocator<
3 id, &iid>*>((_atl_event_classtype*)8))-8,
4 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.
1(int)(INT_PTR)(static_cast<_IDispEventLocator<
2 id, &iid>*>((_atl_event_classtype*)8))-8
The following cast saves the event-handler function address as a pointer to a member function:
1(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 thesame as the SINK_ENTRY_INFO macro with IID_NULL for the dispatch interface and a NULL pointer for the function information structure.
1#define SINK_ENTRY_EX(id, iid, dispid, fn) \
2 SINK_ENTRY_INFO(id, iid, dispid, fn, NULL)
3#define SINK_ENTRY(id, dispid, fn) \
4 SINK_ENTRY_EX(id, IID_NULL, dispid, fn)
The END_SINK_MAP macro ends the array
and completes the _GetSinkMap function implementation:
1#define END_SINK_MAP() {0, NULL, 0, 0, NULL, NULL} }; \
2 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.
1template <UINT nID, class T, const IID* pdiid = &IID_NULL,
2 const GUID* plibid = &GUID_NULL,
3 WORD wMajor = 0, WORD wMinor = 0,
4 class tihclass = CComTypeInfoHolder>
5class ATL_NO_VTABLE IDispEventImpl :
6 public IDispEventSimpleImpl<nID, T, pdiid>
7{ ... }
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:
1STDMETHOD(GetTypeInfoCount)(UINT* pctinfo) {
2 *pctinfo = 1; return S_OK; }
3STDMETHOD(GetTypeInfo)(UINT itinfo, LCID lcid, ITypeInfo** pptinfo)
4{ return _tih.GetTypeInfo(itinfo, lcid, pptinfo); }
5STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames,
6 UINT cNames, LCID lcid, DISPID* rgdispid) {
7 return _tih.GetIDsOfNames(riid, rgszNames, cNames,
8 lcid, rgdispid);
9}
It also overrides the GetFuncInfoFromId method and initializes an _ATL_FUNC_INFO structure using the information provided in
the type library:
1HRESULT GetFuncInfoFromId(const IID& iid,
2 DISPID dispidMember, LCID lcid,
3 ATL_FUNC_INFO& info) {
4 CComPtr<ITypeInfo> spTypeInfo;
5
6 if (InlineIsEqualGUID(*_tih.m_plibid, GUID_NULL))
7 {
8 m_InnerLibid = m_libid;
9 m_InnerIid = m_iid;
10 _tih.m_plibid = &m_InnerLibid;
11 _tih.m_pguid = &m_InnerIid;
12 _tih.m_wMajor = m_wMajorVerNum;
13 _tih.m_wMinor = m_wMinorVerNum;
14 }
15 HRESULT hr = _tih.GetTI(lcid, &spTypeInfo);
16 if (FAILED(hr))
17 return hr;
18 return AtlGetFuncInfoFromId(spTypeInfo, iid,
19 dispidMember, lcid, info);
20}
Summary
The connection point protocol defines a mechanism for a client interested in receiving event callbacks to pass its event sink interface pointer to an event source. Neither the client nor the event source needs to be written with knowledge of each other. Objects hosted on a web page or, more generally, objects used by scripting languages must use the connection points protocol to fire events to the scripting engine. Also, ActiveX controls fire their events using the connection points protocol. Although this protocol is acceptable for intra-apartment use, it is inefficient (when considering round-trips) for use across an apartment boundary.
ATL provides the IDispEvent and IDispEventSimple classes for a client object to use to
receive event callbacks. Additionally, ATL provides the Implement
Connection Point Wizard so you can easily generate a class that
manages a connection point and contains helper methods to fire the
events to all connected clients.