The Brilliant Example Problem That Produces Blinding InsightLet'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 ObjectThere are seven steps to creating a connectable object using ATL:
Adding Required Base Classes to Your Connectable ObjectFor 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.
Changes to the COM_MAP for a Connectable Object
Adding Each Connection PointA 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.
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.
Update the coclass to Support the Source Interface
Where There Are Events, There Must Be FireSo 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
The ATL Connection Point Proxy GeneratorWriting 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 boxThe 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 CodeThe 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 CodePutting 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
Going the Last Meter/Mile, Adding One Last BellThe 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() ... }; |