Developers Workshop to COM and ATL 3.0
| < Free Open Study > |
|
COM's Connectable Object Architecture
The callback interface is a simple and efficient way to enable two COM objects to communicate with each other. The only problem with the callbacks we have just examined is that this approach does not provide a generic mechanism for bidirectional communication. The details of a callback interface are unique across projects, developers, and coclasses. For example, the methods used to set up and terminate the connection (such as Advise() and Unadvise()) may very well change among developers (such as Connect(), Disconnect(), ShutUp(), ad infinitum).
Given this, every object is different and we as COM developers have no way to treat COM objects with outbound interfaces in a polymorphic manner. Furthermore, late bound clients (such as IE) have no way to implement these ad hoc callbacks. Just as the mythical IEnumXXXX interface provides a way for COM collections to have a similar look and feel across the board, COM provides a number of standard interfaces that establish a standard protocol for bidirectional communication.
A connectable object refers to a COM class that contains any number of subobjects, each of which represent a possible and unique connection to an outbound interface. We will call each subobject a source of events (also called a connection point or source object). The general relationship between these key players can be seen in Figure 12-6:
To begin analyzing this complexity, we begin with the coclass itself. CSomeCoClass is a COM object that implements the IConnectionPointContainer interface and most likely other domain-specific custom interfaces, such as ISomeCustomInterface.
IConnectionPointContainer allows the client to discover if the coclass has any source objects to establish an advisory relationship with. CSomeCoClass contains two source objects, CPOne and CPTwo. Each of these objects supports the standard IConnectionPoint interface. It is important to note that a given connection point object understands how to interact with a specific type of sink (e.g., the source object must be able to identify the GUID of the outbound interface the sink implements). In Figure 12-6, CPOne is able to call methods of the _IOutBound interface. Assume that CPTwo is able to communicate with sinks implementing another outbound interface (not shown here).
Understanding IConnectionPointContainer and IEnumConnectionPoints
The methods of IConnectionPointContainer allow the client to iterate over each source subobject or ask for a particular one by name. This level of indirection allows a coclass to support a number of outbound connections. Here is the formal definition of IConnection- PointContainer, as defined in <ocidl.idl>:
// This standard COM interface allows a client to examine all the connection point objects // maintained by the coclass (a.k.a. connection point container). [ object, uuid(B196B284-BAB4-101A-B69C-00AA00341D07) ] interface IConnectionPointContainer : IUnknown { // Give me an enumerator to look over all the connection point objects. HRESULT EnumConnectionPoints([out] IEnumConnectionPoints ** ppEnum); // Do you have a connection pointed named <guid>? HRESULT FindConnectionPoint([in] REFIID riid, [out] IConnectionPoint ** ppCP); };
Of the two methods of IConnectionPointContainer, FindConnectionPoint() is the most straightforward: A client is interested in a specific connection point identified by name. If the container supports that source object, the client receives an IConnectionPoint interface from that source object to establish communications.
EnumConnectionPoints() returns a standard COM enumerator (IEnumConnection- Points), which allows the client to iterate over all connection objects held in the container. If the client happens to find a source object it is interested in, it can make the appropriate connection using the fetched IConnectionPoint interface.
Like every IEnumXXXX interface, IEnumConnectionPoints defines the Next(), Skip(), Reset(), and Clone() methods. Given what you already know about COM enumerations, the use of this interface should be of little surprise. Here's the IDL:
// Using EnumConnectionPoints(), a client is able to receive an IEnumConnectionPoints // interface. [ object, uuid(B196B285-BAB4-101A-B69C-00AA00341D07) ] interface IEnumConnectionPoints : IUnknown { HRESULT Next([in] ULONG cConnections, [out, size_is(cConnections), length_is(*pcFetched)] LPCONNECTIONPOINT * ppCP, [out] ULONG * pcFetched); HRESULT Skip([in] ULONG cConnections); HRESULT Reset(); HRESULT Clone([out] IEnumConnectionPoints ** ppEnum); };
To be honest, you will seldom (if ever) need to obtain a reference to IEnumConnection- Points, as the client will more than likely know the GUID of the outbound interfaces it is interested in listening to.
Furthermore, some COM language mappings (such as Visual Basic and VBScript) can only respond to events defined in the [default] source interface. Nevertheless, COM's connection point architecture is flexible enough to allow you to obtain an enumeration of connection points (provided that your language mapping can get you there).
Understanding IConnectionPoint and IEnumConnections
So then, once the client has obtained a pointer to IConnectionPointContainer, it will ultimately end up with an IConnectionPoint interface using FindConnectionPoint() or IEnumConnectionPoints(). With the acquired IConnectionPoint interface, the client may then call Advise() and Unadvise() to set up or destroy the connection (much like our previous callback example). Here is the IDL definition of IConnectionPoint, also defined in <ocidl.idl>:
// Each connectable object in the container implements this interface. [ object, uuid(B196B286-BAB4-101A-B69C-00AA00341D07) ] interface IConnectionPoint : IUnknown { // Get the GUID of the source interface. HRESULT GetConnectionInterface( [out] IID * pIID); // Get the container maintaining the connection point. HRESULT GetConnectionPointContainer( [out] IConnectionPointContainer ** ppCPC); // Hook me up! HRESULT Advise( [in] IUnknown * pUnkSink, [out] DWORD * pdwCookie); // Shut up already! HRESULT Unadvise( [in] DWORD dwCookie); // What other clients are listening to this connection? HRESULT EnumConnections( [out] IEnumConnections ** ppEnum); };
It was no odd twist of fate that our previous callback interface used methods named Advise() and Unadvise(). IConnectionPoint uses these methods for the very same reason as our ad hoc IEstablishCommunications: to establish and terminate a connection to a source object. The one small difference is that a connectable object returns a token ID to the client, which is used to determine which connection to terminate. It is very possible for multiple clients to be listening to the same connection, and therefore each client must be given a unique ID in order for the coclass to understand which client is disconnecting.
If a given client wants to know who else is listening to the events of a given source object, it may call EnumConnections(). This method of IConnectionPoint returns another standard COM enumerator, IEnumConnections, which returns information about the current active connections:
// Who else is out there? [ object, uuid(B196B287-BAB4-101A-B69C-00AA00341D07)] interface IEnumConnections : IUnknown { typedef struct tagCONNECTDATA { IUnknown * pUnk; DWORD dwCookie; } CONNECTDATA; HRESULT Next([in] ULONG cConnections, [out, size_is(cConnections), length_is(*pcFetched)] LPCONNECTDATA rgcd, [out] ULONG * pcFetched); HRESULT Skip([in] ULONG cConnections); HRESULT Reset(); HRESULT Clone([out] IEnumConnections ** ppEnum); };
Notice that what we are enumerating over is not a COM interface, but a structure named CONNECTDATA, which holds the IUnknown pointer of each client sink, as well as the assigned cookie ID:
// CONNECTDATA holds information for a client's sink and assigned cookie. typedef struct tagCONNECTDATA { IUnknown **pUnk; DWORD dwCookie; }CONNECTDATA;
The final methods of IConnectionPoint allow you to obtain the GUID of the outbound interface (GetConnectionInterface()), as well as the interface of the container holding this connection point (GetConnectionPointContainer()). The later method is helpful as it allows you to navigate back to the original coclass from a source subobject. From here you can obtain additional inbound interfaces, as well as discover additional outbound interfaces (ponder that one for a moment).
So all in all, the standard connectable object protocol used by COM revolves around four standard interfaces. As you can see, this is far more complex than the callback pattern we created in the first part of this chapter. Fear not: ATL provides implementations for each of these standard interfaces. The following table summarizes the set of standard COM interfaces used to configure connections between two interested parties:
COM Connection Interface | Meaning in Life |
---|---|
IConnectionPoint | Used to allow a client to connect and disconnect to a source object, as well as discover any other clients that are listening to this source object. |
IConnectionPointContainer | This interface allows a client to discover all the source objects the container object maintains. A single container may have numerous source objects under its management. |
IEnumConnectionPoints | A standard COM enumerator interface which allows a client to iterate over all of the source objects held by the container. |
IEnumConnections | Another standard COM enumerator interface which allows a client to iterate over the set of currently connected clients. |
Building a Connectable Object in C++
To give a flavor of what these interfaces look like when implemented, let's examine what it would take to create the core pieces of a connectable object in C++ (to keep things simple, we will not be implementing the details of the enumerator interfaces).
On the CD Your companion CD includes the RawConnObjServer example, which illustrates the key parts of a connection point container developed in raw C++.
To begin the journey, we need to define some source objects. As we will see, the underlying code for any connection point is very boilerplate. Given this, we will only focus on CPOne and the _IOutBound interface. CPOne supports IConnectionPoint, and like any COM object must implement the three methods of IUnknown. QueryInterface() will return pointers to IUnknown and IConnectionPoint, but nothing else. Keep in mind that when you use COM's standard connection protocol, you typically want each connectable object to have the flexibility to support any number of connected clients (rather than a single client, as in our callback example).
CPOne will maintain a list of all connected clients represented by an array (or STL vector) of IUnknown pointers. For our purposes, we will say no more than ten clients can be listening to any given connection point (recall that CPOne will receive these pointers via IConnectionPoint::Advise()). Here is the initial definition of CPOne:
// CPOne represents a connection to a single outbound interface. class CPOne : public IConnectionPoint { public: ... // IUnknown as usual // IConnectionPoint STDMETHODIMP GetConnectionInterface(IID *pIID); STDMETHODIMP GetConnectionPointContainer(IConnectionPointContainer**ppCPC); STDMETHODIMP Advise(IUnknown *pUnkSink, DWORD *pdwCookie); STDMETHODIMP Unadvise(DWORD dwCookie); STDMETHODIMP EnumConnections(IEnumConnections**ppEnum); private: ULONG m_ref; IUnknown* m_unkArray[MAX]; // MAX == 10. ULONG m_cookie; // Client ID. int m_position; // Index in the array. };
Recall that a given connection point understands how to call methods for a particular outbound interface. For the current discussion, we will assume a very simple outbound interface named _IOutBound, which supports a single method named Test():
// This is implemented by the sink and called by the // connectable object (CPOne). [uuid(FA9D1721-6879-11d3-B929-0020781238D4), object] interface _IOutBound : IUnknown { HRESULT Test(); };
Note | A common practice used when developing outbound interfaces is to prefix an underscore to the interface name (e.g., _IOutBound). This is a cue to many high-level object-browsing tools to hide the interface from view. |
Advising and Unadvising
Advise() is called by a client when it wishes to establish a connection with the source object. Again, to be as generic as possible, the client sends in the IUnknown pointer to the connection object. A connectable object will use this IUnknown interface to determine if the sink supports the correct outbound interface. Once this has been determined, the coclass adds the interface to its internal array (or vector) of interfaces and assigns a cookie (unique identifier) to the client:
// The client has implemented _IOutBound and wants to listen in... // FYI, these new HRESULT values are defined in <olectl.h>. STDMETHODIMP CPOne::Advise(IUnknown *pUnkSink, DWORD *pdwCookie) { _IOutBound *pOutBound = NULL; // Do I have room for any more connections? if(m_position < MAX) { // Query the client for the _IOutBound interface // and store it for our use. if(SUCCEEDED(pUnkSink->QueryInterface(IID__IOutBound, (void**)&pOutBound))) { m_unkArray[m_position] = pOutBound; m_cookie = m_position; // Give back a connection ID. *pdwCookie = m_cookie; m_position++; return S_OK; } else { // Some client sent me a sink I can't use! return CONNECT_E_NOCONNECTION; } } return CONNECT_E_ADVISELIMIT; }
Most Advise() methods will look about the same. First, if our array of IUnknown pointers is full (we have handed out our maximum number of connections) we return CONNECT_E_ADVISELIMIT immediately. Recall that a given connectable object is able to call methods (a.k.a. fire events) for a specific outbound interface. CPOne only knows how to talk to sink objects supporting _IOutBound, and thus the initial call to QueryInterface().
If this call is successful, we can store a pointer to this sink into our array, advance the position in the array by one, and assign the cookie. If this call was not successful, the client is attempting to communicate with a source object which does not "understand" the sink's outbound interface and should return CONNECT_E_NOCONNECTION.
Implementing Unadvise() is simply a matter of removing the specific sink pointer from the array based off the assigned cookie:
// A client named dwCookie wants to break a connection to the _IOutBound interface. STDMETHODIMP CPOne::Unadvise(DWORD dwCookie) { m_unkArray[dwCookie]->Release(); m_unkArray[dwCookie] = NULL; return S_OK; }
Finishing Up the Source Object
The next two methods every IConnectionPoint-enabled coclass must contend with are GetConnectionInterface() and EnumConnections(). The former is responsible for returning the interface identifier of the outbound interface this connection point is making calls to. As mentioned, EnumConnections() allows a client to obtain a COM enumerator to walk the list of connected clients. For our purpose, we will return E_NOTIMPL from within EnumConnections() to help ease the pain (we will wait for ATL to help us out with this one):
// Which sink interface can I talk to? STDMETHODIMP CPOne::GetConnectionInterface(IID *pIID) { *pIID = IID__IOutBound; return S_OK; } // I won't let you see my connections... STDMETHODIMP CPOne::EnumConnections(IEnumConnections**ppEnum) { return E_NOTIMPL; }
The final method of IConnectionPoint allows a client to return to the container itself. As mentioned, this allows a client to return to the coclass from a connection point subobject. The problem is, CPOne does not know who created it! To rectify this, let's give CPOne an overloaded constructor allowing the container to pass a pointer to itself. We can hold onto this pointer and use it within our implementation of GetConnectionPointContainer():
// The overloaded constructor. CPOne::CPOne(IConnectionPointContainer* pCont) { // Prep work... m_ref = 0; m_cookie = 0; m_position = 0; m_pCont = pCont; // Fill the array of IUnknown pointers with NULL. for(int i = 0; i < MAX; i++) m_unkArray[i] = NULL; } // Return pointer to the container. STDMETHODIMP CPOne::GetConnectionPointContainer(IConnectionPointContainer**ppCPC) { *ppCPC = m_pCont; (*ppCPC)->AddRef(); // Release() called when finished. return S_OK; }
Firing the Event
The final bit of code relevant to this discussion is the actual call to the Test() method implemented by the client-side sink. Once a client has set up an advisory connection to CPOne, it is patiently waiting for us to call the Test() method at some point. To do so, here is a public helper function named Fire_Test(), which will be called by the container at some point. Here, we need to recall that any number of clients (up to ten) might be hooked into this event, and call each sink:
// Call the Test() method for each connected client. HRESULT CPOne::Fire_Test() { // For each active connection... for (int nConnectionIndex = 0; nConnectionIndex < MAX; nConnectionIndex++) { // Grab an IUnk from the array. IUnknown* pUnk = m_unkArray[nConnectionIndex]; // Dynamically change it into a _IOutBound... _IOutBound* pOut = reinterpret_cast<_IOutBound*>(pUnk); if (pOut != NULL) { pOut->Test(); // Call client's implementation of Test(). } } return S_OK; }
If _IOutBound defined a number of events we would write similar Fire_ methods for each. In this way, a given source object is self sufficient, in that it has the ability to communicate with the sink.
Implementing a Connection Point Container in C++
So then, CPOne is complete (except for the enumerators). We now have to create the container for this connection point. CoSomeCoClass will need to add support for IConnectionPointContainer and define the details behind FindConnectionPoint() and EnumConnectionPoints(). To keep life sane, we will return E_NOTIMPL from the EnumConnectionPoints() method and focus on FindConnectionPoint():
// Our container will not support enumeration of our connection points. STDMETHODIMP CoSomeObject::EnumConnectionPoints(IEnumConnectionPoints **ppEnum) { // In reality the COM spec disallows returning E_NOTIMPL, but hey... return E_NOTIMPL; }
FindConnectionPoint() allows a client to ask for a connection to a specific outbound interface. To begin, CoSomeObject must create any inner source objects, which it does in the constructor (which would be cleaned up in the destructor):
// Create a new CPOne object, and inform it of its parent. CoSomeObject::CoSomeObject() : m_ref(0) { // Send in parent pointer for use in GetConnectionPointContainer() m_pconnOne = new CPOne((IConnectionPointContainer*)this); m_pconnOne->AddRef(); g_objCount++; }
We can now implement FindConnectionPoint() quite easily; just test the incoming IID and fill the client's IConnectionPoint variable to our CPOne object. Do note that FindConnec- tionPoint() has a look and feel similar to QueryInterface(). The difference is the QueryInterface() hands out pointers to inbound interfaces, while FindConnectionPoint() hands out pointers to outbound interfaces:
// Hand out our connection. STDMETHODIMP CoSomeObject::FindConnectionPoint(REFIID riid, IConnectionPoint **ppCP) { // We only supply one CP right now... if(riid == IID__IOutBound) *ppCP = m_pconnOne; else { *ppCP = NULL; return CONNECT_E_NOCONNECTION; } (*ppCP)->AddRef(); return S_OK; }
Beyond adding a QueryInterface() provision for IID_IConnectionPointContainer, CoSomeObject() is just about complete. All we need to do is actually fire the Fire_Test() method back to the clients.
Assume our single inbound interface (ISomeInterface) defines the TriggerEvent() method, which simply turns around and calls the client-side sink:
// Fire at will! STDMETHODIMP CoSomeObject::TriggerEvent() { // Here we go! This will force CPOne to fire the test method to all // connected clients. m_pconnOne->Fire_Test(); return S_OK; }
IDL and Outbound Interfaces
Last but not least, we can now modify the project's IDL definition to indicate it has a coclass supporting an outbound interface. IDL provides the [source] attribute to mark outbound interfaces, and as mentioned many languages will only accept events from the [default] source interface. If you were to develop a coclass supporting a number of connectable objects, you would simply list multiple [source] interfaces, specifying one as the [default]:
// This coclass has two outbound interfaces, but only one is the default. [uuid(FA9D1722-6879-11d3-B929-0020781238D4)] coclass CoSomeObject { [default]interface ISomeInterface; [default, source] interface _IOutBound; // VB(Script) can get here. [source] interface _IAnotherOutBound; // VB(Script) can't get here! };
Using a Connectable Object from Visual Basic
Visual Basic makes the consumption of source objects completely painless. Unlike our previous callback example, we have no need to implement any interfaces on our end. Rather, we make use of the WithEvents keyword when declaring an instance of the coclass, and VB dynamically synthesizes a sink on our behalf. When you open the VB Object Browser, you will see that any event found in the [default, source] interface will be represented by a lightning bolt icon (Figure 12-7).
All we need to do is write the code executed as a result of the firing of the Test method. Once you have set a reference to the type library, you will create an instance of the coclass in the [General][Declarations] namespace using the WithEvents keyword. Here is the complete code:
Note | You cannot use the VB New operator when declaring a variable with events. You must first declare the variable, and create a new instance after the fact. |
' [General][Declarations] ' Private WithEvents rawObj As RawConnServer.CoSomeObject Private Sub btnMakeObject_Click() ' Make the object. Set rawObj = New CoSomeObject End Sub Private Sub btnTrigger_Click() ' This will send the event. rawObj.TriggerEvent End Sub ' Sink. Private Sub rawObj_Test() MsgBox "The object just called me!", , "Test complete..." End Sub
As you can guess, we have more work to do as a C++ client, but not that much.
Using a Connectable Object from C++
Now we need to examine how a C++ client can rig into our connection object. Much like the callback example, we need a sink object to implement _IOutBound. Here is the definition:
// The sink for _IOutBound. #include "rawconnobjserver.h" // MIDL-generated file. class CCoSink : public _IOutBound { public: CCoSink(); virtual ~CCoSink(); // IUnknown STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); STDMETHODIMP QueryInterface(REFIID riid, void** ppv); // _IOutBound STDMETHODIMP Test(); // Do something interesting here... };
With this detail aside for the time being, here is the basic set of steps a C++ client will make to set up and tear down a connection:
// Here are the guts of a C++ connection (VB takes care of this internally). CCoSink g_sink; void main(int argc, char* argv[]) { ... // 1) Get IConnectionPointContainer. IConnectionPointContainer* pICPC = NULL; CoCreateInstance(CLSID_CoSomeObject, NULL, CLSCTX_SERVER, IID_IConnectionPointContainer, (void**)&pICPC); // 2) Find a connection point. IConnectionPoint* pCP = NULL; pICPC->FindConnectionPoint(IID__IOutBound, &pCP); // 3) Make a connection to this source object. ULONG cookie; pCP->Advise(&g_sink, &cookie); // 4) Get the default inbound ISomeInterface. ISomeInterface* pISomeIntf = NULL; pICPC->QueryInterface(IID_ISomeInterface, (void**)&pISomeIntf); // 5) Do something to trigger the event (this will call g_sink::Test()) pISomeIntf->TriggerEvent(); // 6) Release connection. pCP->Unadvise(cookie); ... }
So as you may agree, that is a heck of a lot of work to do just to get a connection between two COM objects (and that is without the enumerator interfaces!). If we had no other choice but straight C++ to build standard connectable objects, I'd bet we would find a lot of objects that were the "strong silent type." Of course, ATL makes this whole interaction nearly invisible using wizards, templates, and a few magical macros.
Building Connectable Objects with ATL
Having examined what it would take to get a connectable scenario up and running in straight C++, you have likely come to the following conclusions:
-
I will never write a connectable object in straight C++.
-
This is far too painful, and we did not even contend with the enumerators.
-
ATL better help out with this one.
Yes, connectable objects are a huge pain to get right without framework support, and the great news is that ATL does indeed make connection point development (just about) as easy as selecting the correct option in the Object Wizard. Using two core templates and a handful of helper classes, your ATL coclasses will be perfectly able to send out any number of events with minimal fuss and bother. Rest assured that the prefabricated ATL code is taking care of all the gory details we have just examined. To show you how easy things can be with a good framework on your side, let's re-create the previous example in ATL.
To begin, assume we have used the ATL COM AppWizard to create a new DLL workspace called ATLConnObjServer. We will now use the ATL Object Wizard to insert a new Simple Object named CoSomeObject which supports a [default] interface named ISomeInterface. Before you click that OK button, however, you will need to be sure that you select the option indicated in Figure 12-8.
When you enable this option for an ATL coclass, you are saying that you wish this object to work as a connection point container, and thus support IConnectionPointContainer. As far as the generated code is concerned, you will first see that the coclass is derived from a further ATL template named IConnectionPointContainerImpl<> together with a corresponding COM_MAP entry. As the name suggests, this ATL template provides a full implementation for FindConnection- Point() and EnumConnectionPoints().
In order for IConnectionPointContainerImpl<> to get its work done, it must be able to discover and identify each individual source object it maintains. The fact of the matter is, your coclass currently does not have any objects supporting IConnectionPoint (e.g., we don't have CPOne or CPTwo, but will insert these source objects in just a moment).
What you will find, however, is an empty ATL "connection point map" defined in your coclass. As you bring in more and more source objects into your container, this map will be populated with a number of CONNECTION_POINT_ENTRY macros. Here is the initial code:
// When you select the Support Connection Point option, your coclass // is modified with the following. // Therefore, if you forget to check this option, you will need to add the following // by hand. class ATL_NO_VTABLE CCoSomeObject : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoSomeObject, &CLSID_CoSomeObject>, public IConnectionPointContainerImpl<CCoSomeObject>, public IDispatchImpl<ISomeObject, &IID_ISomeObject, &LIBID_ATLCONNOBJSERVERLib> { public: CCoSomeObject() { } DECLARE_REGISTRY_RESOURCEID(IDR_COSOMEOBJECT) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CCoSomeObject) COM_INTERFACE_ENTRY(ISomeObject) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IConnectionPointContainer) END_COM_MAP() // This map will hold all connection points (currently none). BEGIN_CONNECTION_POINT_MAP(CCoSomeObject) END_CONNECTION_POINT_MAP() };
You will also see that the ATL Object Wizard has generated the first [default, source] interface in your IDL file's library statement. By default, ATL will name this initial outbound interface based on the initial inbound interface. Also note that ATL automatically creates a raw dispinterface to represent outbound interfaces. The reason? To enable communications with late bound clients. If you are not interested in supporting vTable-challenged clients, you can replace the definition to meet your design. Here is the initial IDL code:
// The Object Wizard will define a single outbound interface. [ uuid(9788BFA3-6941-11D3-B929-0020781238D4), version(1.0), helpstring("ATLConnObjServer 1.0 Type Library") ] library ATLCONNOBJSERVERLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [ uuid(9788BFB1-6941-11D3-B929-0020781238D4), helpstring("_ISomeObjectEvents Interface") ] dispinterface _ISomeObjectEvents { properties: methods: }; [ uuid(9788BFB0-6941-11D3-B929-0020781238D4), helpstring("CoSomeObject Class") ] coclass CoSomeObject { [default] interface ISomeObject; [default, source] dispinterface _ISomeObjectEvents; }; };
Now, you are free to compile the server as things now stand; however, you still have no subobjects implementing IConnectionPoint, and therefore have no events to fire. Before we build the source object, we first need to add methods into the outbound interface. As we are in ATL territory, we can populate our [source] interfaces using the Add Method Wizard just as we would with a standard inbound interface. Assume we have added an event named Test() to the [source] interface, which this time takes a single [in] BSTR parameter:
// The BSTR is [in] given that the coclass is calling the sink! dispinterface _ISomeObjectEvents { properties: methods: [id(1), helpstring("method Test")] HRESULT Test([in] BSTR bstrTest); };
Bringing in the Connections: Another ATL Wizard
Once you have added some events to your [source] interface (and recompiled to refresh the type information) you are now ready to use the ATL Implement Connection Point Wizard. To activate this CASE tool, right-click on the coclass that you wish to hold the connection (in our case CoSomeObject) and select this option in the pop-up menu item. What you will see is a one-step tool which lists each and every unaccounted for [source] interface in your IDL file (see Figure 12-9).
Notice that the tool will name the new file using a "CP" suffix. ATLConnObjServerCP.h is the name of our new header file that defines the connection class responsible for connecting interested clients to the _ISomeObjectEvents interface. The ATL-generated class will be marked with a "CProxy_" prefix before the name of the [source] interface (in this case CProxy_ISomeObjectEvents). Here is a look into that generated proxy class:
// The wizard generates similar code for each connection in your code. template <class T> class CProxy_ISomeObjectEvents : public IConnectionPointImpl<T, &DIID__ISomeObjectEvents, CComDynamicUnkArray> { public: HRESULT Fire_Test(BSTR bstrTest) { CComVariant varResult; T* pT = static_cast<T*>(this); int nConnectionIndex; CComVariant* pvars = new CComVariant[1]; int nConnections = m_vec.GetSize(); for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++) { pT->Lock(); CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex); pT->Unlock(); IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p); if (pDispatch != NULL) { VariantClear(&varResult); pvars[0] = bstrTest; DISPPARAMS disp = { pvars, NULL, 1, 0 }; pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, &varResult, NULL, NULL); } } delete[] pvars; return varResult.scode; } };
CProxy_ISomeObjectEvents is equivalent to the CPOne class we wrote by hand in the raw C++ example. This class implements each method of IConnectionPoint, with help from the IConnectionPointImpl<> template. More importantly, this proxy class has implemented a method called Fire_Test(). This is the name of the method we call from within our CoSomeObject logic whenever we wish to send out the Test() event to all listening clients. As you can see, the generated code will iterate over the active connections, and call the Test() method (via Invoke() with a DISPID of [id(1)]) on each client sink. Furthermore, our BSTR parameter is packaged up into a VARIANT using the ATL CComVariant helper class. Not bad for a mouse click!
Note | If you add, delete, or change any methods on your [source] interfaces, you will need to rerun the Implement Connection Point Wizard to refresh the generated code. |
Modifications in the CoSomeObject Coclass
Now that we have a real connection object for the container, we will see that the Implement Connection Point Wizard has made the following change back in the CoSomeObject coclass. First, the proxy object representing the connection has been added into our inheritance chain. Therefore, our class can call Fire_Test() whenever it wishes to send this event to connected clients. As well, you will most likely find that your connection map now has an (incorrect) entry. Yes, a bug. Here is what the code looks like before the fix (the erroneous line is in your connection map):
// The wizard has a bug! class ATL_NO_VTABLE CCoSomeObject : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoSomeObject, &CLSID_CoSomeObject>, public IConnectionPointContainerImpl<CCoSomeObject>, public IDispatchImpl<ISomeObject, &IID_ISomeObject, &LIBID_ATLCONNOBJSERVERLib>, public CProxy_ISomeObjectEvents< CCoSomeObject > { ... BEGIN_CONNECTION_POINT_MAP(CCoSomeObject) CONNECTION_POINT_ENTRY(IID__ISomeObjectEvents) END_CONNECTION_POINT_MAP() ... };
The MIDL compiler automatically creates a GUID constant marked with a "DIID_" prefix for all dispinterfaces. Note that the Implement Connection Point Wizard failed to take this into consideration, and listed our connection point entry with an "IID_" prefix (oops). This is annoying, but not a big deal to fix; add a "D" and move on:
// Fix the Wizard generated code by adding a 'D' to the connection entry. BEGIN_CONNECTION_POINT_MAP(CCoSomeObject) CONNECTION_POINT_ENTRY(DIID__ISomeObjectEvents) END_CONNECTION_POINT_MAP()
All you have to do at this point is call Fire_Test() from somewhere in your code and ATL will ensure each connected client gets your event. Assume our [default] interface has a method called TriggerTheEvent():
// As always, you will need to decide the conditions to fire your events. STDMETHODIMP CCoSomeObject::TriggerTheEvent() { // Do other stuff in this function... // Now fire the event. CComBSTR msg = "Take that!"; Fire_Test(msg.Copy()); return S_OK; }
Digging Deeper into ATL's Support for Connection Points
Now that we have seen just how easy ATL makes connection points, we will take a deeper look into how the framework is doing its magic. As you know, a client can ask an object if it supports IConnectionPointContainer. If so, the object is in effect saying, "I have some number of subobjects which you can listen to." The client discovers these connection points by calling either EnumConnectionPoints() or FindConnectionPoint(). The IConnectionPointContainerImpl<> template of ATL takes care of all the grunge work. To get the job done, IConnectionPointContainerImpl<> must make use of the derived class's connection point map.
The BEGIN_CONNECTION_POINT_MAP() macro expands to define an array of _ATL_CONNMAP_ENTRY structures named "_entries." This same macro also defines a helper function named GetConnMap() to return the _ATL_CONNMAP_ENTRY array. Here is the definition in <atlcom.h>:
// This ATL map defines an array of _ATL_CONNMAP_ENTRY structures. #define BEGIN_CONNECTION_POINT_MAP(x)\ typedef x _atl_conn_classtype;\ static const _ATL_CONNMAP_ENTRY* GetConnMap(int* pnEntries) {\ static const _ATL_CONNMAP_ENTRY _entries[] = {
So then, what is _ATL_CONNMAP_ENTRY? I can assure you it is far simpler than the _ATL_INTMAP_ENTRY structure defined by the COM_MAP. _ATL_CONNMAP_ENTRY holds a single field, which contains the offset to a given connection point from the IConnectionPointContainer interface:
// The connection point map is an array of these guys. struct _ATL_CONNMAP_ENTRY { DWORD dwOffset; };
To fill the array, we make use of the CONNECTION_POINT_ENTRY macro which computes the offset of the correct vPtr, using the offsetofclass() macro:
// CONNECTION_POINT_ENTRY computes the offset of the connection point to the // IConnectionPointContainer interface. #define CONNECTION_POINT_ENTRY(iid){offsetofclass(_ICPLocator<&iid>, \ _atl_conn_classtype)-\ offsetofclass(IConnectionPointContainerImpl<_atl_conn_classtype>, \ _atl_conn_classtype)},
Finally, the END_CONNECTION_POINT_MAP() macro terminates the array:
// Terminate the array. #define END_CONNECTION_POINT_MAP() {(DWORD)-1} };
Once we have a connection map in place, IConnectionPointContainerImpl<> can call GetConnMap() when it needs to find a given connection point for the client. <atlcom.h> defines IConnectionPointContainerImpl<>::FindConnectionPoint() as so:
// The client sends in a specific connection point IID, and we search the connection map // for the correct entry. STDMETHOD(FindConnectionPoint)(REFIID riid, IConnectionPoint** ppCP) { if (ppCP == NULL) return E_POINTER; *ppCP = NULL; HRESULT hRes = CONNECT_E_NOCONNECTION; // Get the map of connections. const _ATL_CONNMAP_ENTRY* pEntry = T::GetConnMap(NULL); IID iid; // Search the map until we reach the end. while (pEntry->dwOffset != (DWORD)-1) { // Get one of the entries. IConnectionPoint* pCP = (IConnectionPoint*)((int)this+pEntry->dwOffset); // Is it the same as what the client wants? if (SUCCEEDED(pCP->GetConnectionInterface(&iid)) && InlineIsEqualGUID(riid, iid)) { *ppCP = pCP; // Ah-ha! Found ya! pCP->AddRef(); hRes = S_OK; break; } pEntry++; } return hRes; }
EnumConnectionPoints() also makes use of the connection map to get its work done. However, it does not iterate over the map looking for a given entry, but to fill a COM enumerator for the client. The CComEnumConnectionPoints typedef defines the enumerator, and as you already have a good grasp of how ATL provides support for COM enumerators, I'd bet this does not look too offensive to your eyes at this point:
// This typedef is a COM enumerator for IConnectionPoint interfaces. typedef CComEnum<IEnumConnectionPoints, &IID_IEnumConnectionPoints, IConnectionPoint*, _CopyInterface<IConnectionPoint> > CComEnumConnectionPoints; // Return an enumerator of all the connections to the client. // (error checking code removed for clarity) STDMETHOD(EnumConnectionPoints)(IEnumConnectionPoints** ppEnum) { ... // Make the connection point enumerator. CComEnumConnectionPoints* pEnum = NULL; ATLTRY(pEnum = new CComObject<CComEnumConnectionPoints>) ... // Get the connection map. int nCPCount; const _ATL_CONNMAP_ENTRY* pEntry = T::GetConnMap(&nCPCount); // Allocate and initialize a vector of connection point object pointers IConnectionPoint** ppCP = (IConnectionPoint**)alloca(sizeof(IConnectionPoint*)*nCPCount); int i = 0; while (pEntry->dwOffset != (DWORD)-1) { ppCP[i++] = (IConnectionPoint*)((int)this+pEntry->dwOffset); pEntry++; } // Copy the pointers: they will AddRef this object HRESULT hRes = pEnum->Init((IConnectionPoint**)&ppCP[0], (IConnectionPoint**)&ppCP[nCPCount], reinterpret_cast<IConnectionPointContainer*>(this), AtlFlagCopy); ... // Return the enumerator. hRes = pEnum->QueryInterface(IID_IEnumConnectionPoints, (void**)ppEnum); if (FAILED(hRes)) delete pEnum; return hRes; }
ATL's IConnectionPointImpl<> Template
Finally, we have the IConnectionPointImpl<> template, which is a base class for each proxy class generated by the ATL Implement Connection Point Wizard. This template maintains a dynamic array of IUnknown pointers, which represent the client-supplied interfaces send in via IConnectionPoint::Advise() (the ATL utility class CComDynamic- UnkArray represents this array). Here are the relevant parts of IConnectionPointImpl<>:
// IConnectionPointImpl<> provides framework support for the all important // IConnectionPoint interface. template <class T, const IID* piid, class CDV = CComDynamicUnkArray > class ATL_NO_VTABLE IConnectionPointImpl : public _ICPLocator<piid> { // Here is the typedef for IEnumConnections typedef CComEnum<IEnumConnections, &IID_IEnumConnections, CONNECTDATA, _Copy<CONNECTDATA> > CComEnumConnections; typedef CDV _CDV; public: // Get the IID of this connection interface. STDMETHOD(GetConnectionInterface)(IID* piid2) { if (piid2 == NULL) return E_POINTER; *piid2 = *piid; return S_OK; } // Get the connection point container. STDMETHOD(GetConnectionPointContainer) (IConnectionPointContainer** ppCPC) { T* pT = static_cast<T*>(this); // No need to check ppCPC for NULL since QI will do that for us return pT->QueryInterface(IID_IConnectionPointContainer, (void**)ppCPC); } // Not defined inline... STDMETHOD(Advise)(IUnknown* pUnkSink, DWORD* pdwCookie); STDMETHOD(Unadvise)(DWORD dwCookie); STDMETHOD(EnumConnections)(IEnumConnections** ppEnum); CDV m_vec; };
Advise() will do as you expect. Increase the size of the dynamic array of IUnknown pointers and return a cookie to the connected client. To discover the IID for the outbound interface this source proxy is able to communicate with, a call is first made to GetConnectionInterface():
// Advise me. template <class T, const IID* piid, class CDV> STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(IUnknown* pUnkSink, DWORD* pdwCookie) { T* pT = static_cast<T*>(this); IUnknown* p; HRESULT hRes = S_OK; ... IID iid; GetConnectionInterface(&iid); // Get sink. hRes = pUnkSink->QueryInterface(iid, (void**)&p); if (SUCCEEDED(hRes)) { pT->Lock(); *pdwCookie = m_vec.Add(p); hRes = (*pdwCookie != NULL) ? S_OK:CONNECT_E_ADVISELIMIT; pT->Unlock(); if (hRes != S_OK) p->Release(); } else if (hRes == E_NOINTERFACE) hRes = CONNECT_E_CANNOTCONNECT; if (FAILED(hRes)) *pdwCookie = 0; return hRes; }
Unadvise() removes the client from the dynamic array based on the assigned cookie:
// Take me off the mailing list. template <class T, const IID* piid, class CDV> STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Unadvise(DWORD dwCookie) { T* pT = static_cast<T*>(this); pT->Lock(); // Get the correct unknown pointer from the CComDynamicUnkArray. IUnknown* p = _CDV::GetUnknown(dwCookie); // Remove it. HRESULT hRes = m_vec.Remove(dwCookie) ? S_OK : CONNECT_E_NOCONNECTION; pT->Unlock(); if (hRes == S_OK && p != NULL) p->Release(); return hRes; }
To wrap up, ATL's support for connection points, EnumConnections(), creates a COM enumerator full of all the CONNECTDATA structures, based on the CComEnumConnections typedef. Also, be aware that the destructor of IConnectionPointImpl<> releases all remaining IUnknown references.
| < Free Open Study > |
|