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.
interface ISpeaker : IDispatch {
[propget, id(1)] HRESULT Volume([out, retval] long *pVal);
[propput, id(1)] HRESULT Volume([in] long newVal);
[propget, id(2)] HRESULT Speech([out, retval] BSTR *pVal);
[propput, id(2)] HRESULT Speech([in] BSTR newVal);
[id(3)] HRESULT Speak();
};
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.
dispinterface _ISpeakerEvents {
properties:
methods:
[id(1)] void OnWhisper(BSTR bstrSpeach);
[id(2)] void OnTalk(BSTR bstrSpeach);
[id(3)] void OnYell(BSTR bstrSpeach);
};
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).
coclass Demagogue {
[default] interface IUnknown;
interface ISpeaker;
interface INamedObject;
[default, source] dispinterface _ISpeakerEvents;
};
I start with the CDemagogue class: an
ATL-based, single-threaded-apartment-resident, COM-createable
object to represent a Demagogue.
class ATL_NO_VTABLE CDemagogue :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDemagogue, &CLSID_Demagogue>,
public ISupportErrorInfo,
public IDispatchImpl<ISpeaker, &IID_ISpeaker,
&LIBID_ATLINTERNALSLib>,
...
{ ... };
Seven Steps to a
Connectable Object
There are seven steps to creating a connectable
object using ATL:
|
|
1.
|
Implement the
IConnectionPointContainer interface.
|
2.
|
Implement QueryInterface so that it
responds to requests for
IID_IConnectionPointContainer.
|
3.
|
Implement the IConnectionPoint
interface for each source interface that the connectable object
supports.
|
4.
|
Provide a connection map that is a table that
associates an IID with a connection point implementation.
|
5.
|
Update the coclass definition 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.
|
6.
|
Typically, you implement helper methods that
call the sink methods for all connected sinks.
|
7.
|
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.
class ATL_NO_VTABLE CDemagogue :
...
public IConnectionPointContainerImpl<CDemagogue>,
...
|
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:
BEGIN_COM_MAP(CDemagogue)
...
COM_INTERFACE_ENTRY(IConnectionPointContainer)
...
END_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.
class ATL_NO_VTABLE CDemagogue :
...
public IConnectionPointContainerImpl<CDemagogue>,
public IConnectionPointImpl<CDemagogue, &DIID__ISpeakerEvents>
...
|
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 to produce 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:
BEGIN_CONNECTION_POINT_MAP(CDemagogue)
CONNECTION_POINT_ENTRY(__uuidof(_ISpeakerEvents))
END_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.
coclass Demagogue
{
[default] interface IUnknown;
interface ISpeaker;
interface INamedObject;
[default, source] dispinterface _ISpeakerEvents;
};
|
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 your connectable 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.
HRESULT Fire_OnTalk(BSTR bstrSpeech)
{
CComVariant arg, varResult;
int nIndex, nConnections = m_vec.GetSize();
for (nIndex = 0; nIndex < nConnections; nIndex++) {
CComPtr<IUnknown> sp = m_vec.GetAt(nIndex);
IDispatch* pDispatch =
reinterpret_cast<IDispatch*>(sp.p);
if (pDispatch != NULL) {
VariantClear(&varResult);
arg = bstrSpeech;
DISPPARAMS disp = { &arg, NULL, 1, 0 };
pDispatch->Invoke(0x2, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_METHOD, &disp, &varResult, NULL, NULL);
}
}
return varResult.scode;
}
|
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).
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:
#pragma once
template<class T>
class CProxy_ISpeakerEvents :
public IConnectionPointImpl<T, &__uuidof(_ISpeakerEvents)> {
public:
HRESULT Fire_OnWhisper(BSTR bstrSpeech) {
HRESULT hr = S_OK;
T * pThis = static_cast<T*>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0;
iConnection < cConnections;
iConnection++)
{
pThis->Lock();
CComPtr<IUnknown> punkConnection =
m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection =
static_cast<IDispatch*>(punkConnection.p);
if (pConnection) {
CComVariant avarParams[1];
avarParams[0] = bstrSpeech;
DISPPARAMS params = { avarParams, NULL, 1, 0 };
hr = pConnection->Invoke(DISPID_ONWHISPER,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
¶ms, NULL, NULL, NULL);
}
}
return hr;
}
// Other methods similar, deleted for clarity
};
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.
class ATL_NO_VTABLE CDemagogue :
...
public IConnectionPointContainerImpl<CDemagogue>,
public CProxy_ISpeakerEvents<CDemagogue>, ... {
BEGIN_COM_MAP(CDemagogue)
...
COM_INTERFACE_ENTRY(IConnectionPointContainer)
...
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CDemagogue)
CONNECTION_POINT_ENTRY(__uuidof(_ISpeakerEvents))
END_CONNECTION_POINT_MAP()
...
};
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.
STDMETHODIMP CDemagogue::Speak() {
if (m_nVolume <= -100)
return Fire_OnWhisper(m_bstrSpeech);
if (m_nVolume >= 100)
return Fire_OnYell(m_bstrSpeech);
return Fire_OnTalk(m_bstrSpeech);
}
|
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:
template <const CLSID* pcoclsid, const IID* psrcid,
const GUID* plibid = &CAtlModule::m_libid,
WORD wMajor = 1, WORD wMinor = 0,
class tihclass = CComTypeInfoHolder>
class ATL_NO_VTABLE IProvideClassInfo2Impl
: public IProvideClassInfo2
{ ... }
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:
#define LIBRARY_MAJOR 1
#define LIBRARY_MINOR 0
class ATL_NO_VTABLE CDemagogue :
...
public IConnectionPointContainerImpl<CDemagogue>,
public CProxy_ISpeakerEvents<CDemagogue>,
public IProvideClassInfo2Impl<&CLSID_Demagogue,
&__uuidof(_ISpeakerEvents),
&LIBID_ATLINTERNALSLib, LIBRARY_MAJOR, LIBRARY_MINOR>,
...
};
You also need to update the interface map so
that QueryInterface responds to IProvideClassInfo
and IProvideClassInfo2.
BEGIN_COM_MAP(CDemagogue)
...
COM_INTERFACE_ENTRY(IProvideClassInfo2)
COM_INTERFACE_ENTRY(IProvideClassInfo)
END_COM_MAP()
Finally, here are all connectable-object-related
changes in one place:
#define LIBRARY_MAJOR 1
#define LIBRARY_MINOR 0
// Event dispinterface
dispinterface _ISpeakerEvents {
properties:
methods:
[id(1)] void OnWhisper(BSTR bstrSpeech);
[id(2)] void OnTalk(BSTR bstrSpeech);
[id(3)] void OnYell(BSTR bstrSpeech);
};
// Connectable object class
coclass Demagogue {
[default] interface IUnknown;
interface ISpeaker;
interface INamedObject;
[default, source] dispinterface _ISpeakerEvents;
};
// Implementation class for coclass Demagogue
class ATL_NO_VTABLE CDemagogue :
...
public IConnectionPointContainerImpl<CDemagogue>,
public CProxy_ISpeakerEvents<CDemagogue>,
public IProvideClassInfo2Impl<&CLSID_Demagogue,
&__uuidof(_ISpeakerEvents),
&LIBID_ATLINTERNALSLib, LIBRARY_MAJOR, LIBRARY_MINOR>,
... {
public:
BEGIN_COM_MAP(CDemagogue)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
COM_INTERFACE_ENTRY(IProvideClassInfo2)
COM_INTERFACE_ENTRY(IProvideClassInfo)
...
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CDemagogue)
CONNECTION_POINT_ENTRY(__uuidof(_ISpeakerEvents))
END_CONNECTION_POINT_MAP()
...
};
|