previous page
next page

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).

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:

#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,
                    &params, 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()
...
};


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)