Now that we've explored the basic architecture of connection points, it's time to examine a complete implementation of a connectable object, replete with enumerators and full implementations of the IConnectionPointContainer and the IConnectionPoint interfaces. Let's begin with the main interface exposed by an object that expresses the existence of outgoing interfaces: IConnectionPointContainer. A client that wants to hook up with a connectable object initially queries for the IConnectionPointContainer interface. From there, the client can locate individual connection points by calling IConnectionPointContainer:: FindConnectionPoint or IConnectionPointContainer::EnumConnectionPoints. FindConnectionPoint takes the IID of the desired source interface and, if successful, returns a pointer to the relevant IConnectionPoint interface. EnumConnectionPoints is more complicated because it returns a pointer to an object that implements the IEnumConnectionPoints interface. (Enumerators are discussed next in this chapter.)
An implementation of IConnectionPointContainer::EnumConnectionPoints might look like this:
HRESULT CInsideCOM::EnumConnectionPoints( IEnumConnectionPoints** ppEnum) { CEnumConnectionPoints* pEnum = new CEnumConnectionPoints( reinterpret_cast<IUnknown*>(this), (void**)m_rgpConnPt); return pEnum->QueryInterface(IID_IEnumConnectionPoints, (void**)ppEnum); } |
This code instantiates the CEnumConnectionPoints class using the new operator and then returns a pointer to the IEnumConnectionPoints interface exposed by the object. To the constructor of the CEnumConnectionPoints class, we pass a pointer to the current object (this) for reference counting and an array of connection points that can be enumerated (m_rgpConnPt).
The array of connection points is set up during the construction phase of the InsideCOM object, as shown here:
const NUM_CONNECTION_POINTS = 1; CInsideCOM::InsideCOM() : m_cRef(0) { g_cComponents++; m_cRef = 0; // Initialize all the connection points to NULL. for(int count = 0; count < NUM_CONNECTION_POINTS; count++) m_rgpConnPt[count] = NULL; // Create our connection point. m_rgpConnPt[0] = new CConnectionPoint(this, IID_IOutGoing); m_rgpConnPt[0]->AddRef(); // Additional connection points can be instantiated here. } |
In this example, NUM_CONNECTION_POINTS equals 1, indicating that this connectable object supports only one source interface. The code, however, is defined in such a way that you can easily extend it to support connectable objects with multiple source interfaces. You simply set the NUM_CONNECTION_POINTS constant to the number of source interfaces supported by an object and instantiate the individual connection point objects with the IID of each supported source interface. More sophisticated implementations of the IConnectionPointContainer::EnumConnectionPoints method might use a linked list instead of an array to more flexibly support an arbitrary number of connection points.
An enumerator object iterates through a sequence of items. You can use an enumerator object in COM+ when a set of items lends itself to enumeration. Each enumerator object implements a different interface depending on the type of object it enumerates. A number of enumerator interfaces are defined, including IEnumUnknown, IEnumString, and IEnumMoniker, each of which is implemented by an enumerator object that enumerates objects of the specified type. However, all of the enumerator interfaces have the same four methods: Next, Skip, Reset, and Clone. This is not to say that all enumerators are alike; the methods of each enumerator interface generally take parameters of different types since each enumerator enumerates different kinds of things. Behind the facade of the four methods, you can implement the enumerator object in whatever way you see fit (using a linked list, an array, and so on).
The concept of an enumerator interface can perhaps best be expressed using C++ templates, as shown here:
template <class T> class IEnum : public IUnknown { public: virtual HRESULT Next(ULONG cElement, T* pElement, ULONG* pcFetched)=0; virtual HRESULT Skip(ULONG cElement)=0; virtual HRESULT Reset(void)=0; virtual HRESULT Clone(IEnum<T>** ppEnum)=0; }; |
If you ever need to design a set of custom interfaces in COM+, you might need to enumerate objects of a certain type that you have defined. In this case, it is a good idea to design a custom enumerator interface using the same four methods and following the pattern set by standard enumerator interfaces.
Here is the IDL definition of the enumerator interface IEnumConnectionPoints, which enumerates objects that implement the IConnectionPoint interface:
interface IEnumConnectionPoints : IUnknown { // Get me the next connection point(s). HRESULT Next( [in] ULONG cConnections, [out, size_is(cConnections), length_is(*pcFetched)] IConnectionPoint* ppCP, [out] ULONG* pcFetched); // Skip the next connection point(s). HRESULT Skip([in] ULONG cConnections); // Start at the beginning. HRESULT Reset(void); // Give me a new enumerator object. HRESULT Clone([out] IEnumConnectionPoints** ppEnum); } |
The CEnumConnectionPoints class we created for the full connection points sample (which is on the companion CD in the Samples\Connection Points\Full folder) is publicly derived from IEnumConnectionPoints and thus implements the three methods of IUnknown plus the four methods of IEnumConnectionPoints. The Next method, shown in the following code, is the heart of every enumerator. You call it to retrieve one or more consecutive items from the enumeration. The first parameter of the Next method is always the number of items to be retrieved. The number of items actually retrieved from the enumerator is returned in the third parameter, a pointer to a long value allocated by the caller. This pointer must point to a valid address unless the client is retrieving only one item, in which case the third parameter can be NULL. The second parameter of the enumerator retrieves the desired items from enumeration. In the case of the IEnumConnectionPoints interface, the Next method retrieves pointers to connection point objects:
HRESULT CEnumConnectionPoints::Next(ULONG cConnections, IConnectionPoint** rgpcn, ULONG* pcFetched) { if(rgpcn == NULL) return E_POINTER; if(pcFetched == NULL && cConnections != 1) return E_INVALIDARG; if(pcFetched != NULL) *pcFetched = 0; while(m_iCur < NUM_CONNECTION_POINTS && cConnections > 0) { *rgpcn = m_rgpCP[m_iCur++]; if(*rgpcn != NULL) (*rgpcn)->AddRef(); if(pcFetched != NULL) (*pcFetched)++; cConnections--; rgpcn++; } return S_OK; } |
Recall from Chapter 2 that you must call IUnknown::AddRef when a method returns an interface pointer. Since the IEnumConnectionPoints::Next method hands out pointers to the IConnectionPoint interface, it must first call AddRef. The caller is responsible for calling Release through each pointer enumerated by this method.
The IEnumXXXX::Skip method instructs the enumerator to skip a specified number of elements; if successful, subsequent calls to IEnumXXXX::Next return elements after those that were skipped. In the following implementation of IEnumConnectionPoints::Skip, the m_iCur private member variable is incremented so that it correctly identifies the element immediately following those that were skipped:
HRESULT CEnumConnectionPoints::Skip(ULONG cConnections) { if(m_iCur + cConnections >= NUM_CONNECTION_POINTS) return S_FALSE; m_iCur += cConnections; return S_OK; } |
The IEnumXXXX::Reset method orders the enumerator to position itself at the first element in the enumeration. Note that enumerators are not required to return the same set of elements on each pass through the list, even if Reset is not called. For example, an enumerator for a list of files in a directory might continually change in response to changes in the underlying file system. In this sample, the implementation of IEnumConnectionPoints::Reset simply sets m_iCur to 0, causing it to refer to the first element in the array:
HRESULT CEnumConnectionPoints::Reset() { m_iCur = 0; return S_OK; } |
The IEnumXXXX::Clone method creates and returns a pointer to an exact copy of the enumerator object, making it possible to record a point in an enumeration sequence, create a clone and work with that enumerator, and later return to the previous element in the first enumerator. In the implementation of the IEnumConnectionPoints::Clone method shown in the following code, the new operator creates a new instance of the CEnumConnectionPoints class. In the CEnumConnectionPoints constructor, a copy of the array of connection points referenced by m_rgpCP is created and AddRef is called on each. The m_iCur member variable that references the current element in the enumerator is also copied, ensuring that the new enumerator has the same state as the current enumerator.
HRESULT CEnumConnectionPoints::Clone(IEnumConnectionPoints** ppEnum) { if(ppEnum == NULL) return E_POINTER; *ppEnum = NULL; // Create the clone. CEnumConnectionPoints* pNew = new CEnumConnectionPoints( m_pUnkRef, (void**)m_rgpCP); if(pNew == NULL) return E_OUTOFMEMORY; pNew->AddRef(); pNew->m_iCur = m_iCur; *ppEnum = pNew; return S_OK; } |
Earlier in this chapter, we coded a hobbled version of the IConnectionPoint interface that implemented only the Advise and Unadvise methods. The time has come to implement the remaining three IConnectionPoint methods: GetConnectionInterface, GetConnectionPointContainer, and EnumConnections. GetConnectionInterface returns the IID of the source interface managed by the connection point. A client might call this method to learn the IID of a connection point retrieved from the IEnumConnectionPoints enumerator. A standard implementation of GetConnectionInterface is shown here:
HRESULT CConnectionPoint::GetConnectionInterface(IID *pIID) { if(pIID == NULL) return E_POINTER; *pIID = m_iid; return S_OK; } |
The GetConnectionPointContainer method is even simpler. It returns a pointer to the IConnectionPointContainer interface associated with the connection point. It exists to enable a client that happens to have a pointer to a connection point to work backward to the connection point's container. The following implementation of GetConnectionPointContainer simply calls QueryInterface to obtain the pointer. Conveniently, QueryInterface automatically calls AddRef.
HRESULT CConnectionPoint::GetConnectionPointContainer( IConnectionPointContainer** ppCPC) { return m_pObj->QueryInterface(IID_IConnectionPointContainer, (void**)ppCPC); } |
The EnumConnections method returns a pointer to the IEnumConnections interface, enabling a client to enumerate all the connections that exist on a connection point. The following code implements the EnumConnections method by instantiating the CEnumConnections class that implements the IEnumConnections enumeration interface:
HRESULT CConnectionPoint::EnumConnections( IEnumConnections** ppEnum) { *ppEnum = NULL; CONNECTDATA* pCD = new CONNECTDATA[m_cConn]; for(int count1 = 0, count2 = 0; count1 < CCONNMAX; count1++) if(m_rgpUnknown[count1] != NULL) { pCD[count2].pUnk = (IUnknown*)m_rgpUnknown[count1]; pCD[count2].dwCookie = m_rgnCookies[count1]; count2++; } CEnumConnections* pEnum = new CEnumConnections(this, m_cConn, pCD); delete [] pCD; return pEnum->QueryInterface(IID_IEnumConnections, (void**)ppEnum); } |
The IEnumConnections interface enables a client to enumerate the known connections of a connection point. This information about each connection is encapsulated in the CONNECTDATA structure defined in the ocidl.idl system IDL file and is shown here:
typedef struct tagCONNECTDATA { IUnknown * pUnk; DWORD dwCookie; } CONNECTDATA; |
The CONNECTDATA structure contains the IUnknown pointer to the connected sink as well as the cookie value that identifies the connection as returned by the IConnectionPoint::Advise method. The IEnumConnections interface defines the four methods common to every COM+ enumerator: Next, Skip, Reset, and Clone. To avoid dragging you through another enumerator, we'll omit the code for IEnumConnections here; you can find it on the companion CD in the Samples\Connection Points\Full\component.cpp source file.
Connection points offer a flexible and extensible way to enable bidirectional communication between an object and its clients. The fundamental architecture was derived from that developed for use by Visual Basic controls. As we've seen, Visual Basic has to deal with numerous requirements and issues when it works with events fired by objects. The connection points architecture makes this feasible for Visual Basic. Most applications do not have such stringent requirements, however. Not only is the connection points model complex, it can also be inefficient when it is used in a distributed environment by components spread across a network. Setting up a single connection point requires a minimum of four round-trips, as shown in the following list.5
Now the connectable object can call the methods of the IOutGoing interface implemented by the client's sink.
The four calls needed for a client to establish a relationship with a connectable object are not a significant performance problem when you use in-process components. The connection points architecture was designed primarily for working with in-process components. ActiveX controls that work with Visual Basic are always implemented as in-process components. When local or remote calls are involved, however, this overhead makes connection points less efficient. Of course, these four calls are required only once to establish the connection prior to the first outgoing method call. Subsequent outgoing calls require no special setup.
Thus, in the process of developing a component, you must consider whether connection points are the best means of enabling bidirectional communication with clients. If the object needs to work with Visual Basic clients, whether as an ActiveX control or simply as a nonvisual COM+ object using Visual Basic's WithEvents keyword, connection points are required. If the object needs to work with Java clients via the ConnectionPointCookie class, connection points are also required. If support for scripting languages such as VBScript and JScript is desirable, connection points are again the only way to go.
However, if Visual Basic, Java, and scripting language compatibility is not a concern, it might be more efficient to develop a simple custom interface through which the client passes an interface pointer implemented by its sink to the component. Such a protocol might require only one round-trip to set up the connection. Simply passing an interface pointer to a custom interface implemented by a sink object is sufficient. As a model for such a design, you can examine the architecture of the standard IDataObject and IAdviseSink interfaces. Although the IDataObject and IAdviseSink interfaces were designed before the generic connection points architecture became available, they are a good model on which to base a simpler, custom callback mechanism.