Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Design

Undocumented Corner


ATL and Connection Points

Scot is a cofounder of Stingray Software. He can be contacted at [email protected]. George is a senior computer scientist with DevelopMentor and can be contacted at [email protected]. They are the coauthors of MFC Internals (Addison-Wesley, 1996).


Calling into COM objects using an incoming interface is much like calling methods on a C++ class. However, there are times when you want to have a COM object call back out to the client. Once you grasp the basic principle behind COM (that interfaces and implementations should be treated separately) and understand how to call methods through a COM interface, the next question you ask is how to set up COM objects and clients so that the clients can get callback notifications. COM's Connection technology addresses this question -- particularly for Visual Basic and VBScript programmers. This month, we'll take a look at how two COM objects can set up this communication scheme, whereby the object calls back to the client when the object is written using the Active Template Library (ATL). We'll first examine how connections work and then we'll examine how ATL implements them.

Bidirectional Communication

Bidirectional communications between two pieces of software is a common requirement. Given two independent software components, there are many situations where it's useful to have an object notify its client(s) of various goings on. The classic example of this is ActiveX Controls, where the controls notify their clients of special events. Once an object and a client agree on how the object should call back to the client, the client needs a way of connecting its implementation(s) of the callback interface(s). These interfaces defined by the object and implemented by the client are called outbound or outgoing interfaces.

COM defines several standard incoming/outgoing interface pairs that define a connection mechanism. One of the best examples is the IAdviseSink interface, used in conjunction with the IDataObject interface. This pair of interfaces is useful for transferring presentations (among other things) between OLE Document objects and OLE Document clients. Objects implement IDataObject. Once clients connect to the object and QueryInterface for IDataObject, the client can plug its implementation of IAdviseSink into the client using IDataObject::DAdvise and begin receiving notifications whenever the data inside IDataObject changes. This happens whenever the object calls back to the client's implementations of IAdviseSink:: OnViewChanged or IAdviseSink::OnDataChanged.

This is a specific case of outbound interfaces. The interfaces and the connection process are well understood by both the client and the object. But now imagine you're a software designer and you want to create a very generalized case of this connection strategy. Perhaps you're inventing a new kind of COM object and you'd like the COM object to be able to call back to its client, but you also want to generalize the connection mechanism so it's not specific to the interfaces involved. For example, imagine you wanted to establish a connection to an object in a generalized way -- in much the same way QueryInterface lets clients ask for an object's outgoing interface. How do you do it?

Microsoft has taken a shot at it and defined connectable objects. Connectable objects were invented to connect an ActiveX Control to its client so the control can report events back to its client. After all, ActiveX Control events are simply a way for a Control to call back to the client. Let's take a look at how connectable objects are used for establishing a connection between two COM objects.

Incoming versus Outgoing Interfaces

The ability for a client to call into a COM object has been available ever since COM was invented. Any interface implemented by a COM object is an incoming interface, so named because the interface handles incoming method calls. For clients, acquiring a COM object's incoming interface is a matter of creating the COM object in the usual way (using a function like CoCreateInstance) and calling methods on the interface, as in Listing One Incoming interfaces are the norm for COM objects, providing a way for clients to call into COM objects. Figure 1 illustrates a COM object with incoming interfaces.

However, there are also outgoing interfaces that are implemented by the client so the COM object can call the client. Figure 2 illustrates an outgoing COM interface.

The Interfaces

We'll start by examining the COM interfaces involved in connections: IConnectionPoint and IConnectionPointContainer. Both of these interfaces are implemented by the object (as opposed to the client). These interfaces exist for the sole purpose of connecting an object to its client. Once the connection is made, these interfaces drop out of the picture. For instance, Listing Two shows the IConnectionPoint interface in the raw.

You can probably guess the nature of this interface from the function names. Objects implement this interface so clients have a way to subscribe to events. Once a client acquires this interface, the client may ask to subscribe to data change notifications via the Advise function. The Advise function takes an IUnknown pointer, so the callback interface can be any COM interface at all. This interface also contains the complementary Unadvise function. We'll examine how the other functions are useful in a minute.

Clients may implement any callback interface and use IConnectionPoint to hand the interface over to the object so the object may call back to the client. Once the object has the callback interface (passed via Advise's first parameter), the object can easily call back to the client. This begs the next question -- how can the client acquire a connection point in the first place? The answer is through the IConnectionPointContainer interface (see Listing Three).

IConnectionPointContainer is an unfortunate name for this interface, especially given the history of ActiveX Controls. The name IConnectionPointContainer may lead you to conclude that it's implemented by the control container (aka, the client). However, this interface is implemented by the object. A more descriptive name for this interface may have been IConnectionPointHolder or IConnectionPointCollection because it holds connection points. At any rate, this is the name we have to live with.

As you can tell from the second function, this is the interface a COM client uses to acquire an IConnectionPoint interface (which the client can then use to establish a connection).

A COM client calls CoCreateInstance to create a COM object. Once the client has an initial interface, the client can ask the object if it supports any outgoing interfaces by calling QueryInterface for IConnectionPointContainer. If the object answers "yes" by handing back a valid pointer, the client knows it can attempt to establish a connection.

Once the client knows the object supports outgoing interfaces (that is, is capable of calling back to the client), the client can ask for a specific outgoing interface by calling IConnectionPointContainer::FindConnectionPoint using the GUID representing the desired interface. If the object implements that outgoing interface, the object hands back a pointer to that connection point. At that point, the client uses IConnectionPoint::Advise to plug in its implementation of the callback interface so the object can call back to the client.

To support this connection functionality, the object needs to implement these two interfaces. The most common occurrence of this functionality is within ActiveX Controls, whose clients like to listen to the controls for events. ATL's support for connections consists of some template classes and a set of macros.

Setting up Outgoing Interfaces in ATL

Setting up the outgoing interfaces in an ATL-based object involves two steps:

  • Describing the callback interface the client needs to implement.
  • Adding the connection support to the object.

As with all other interfaces in COM, the callback interfaces for an ATL-based object are described in IDL. For example, imagine you're implementing an object named CConnectableObj that has an outgoing interface. Normally, you'd add the object to your server by selecting New ATL Object from the Insert menu. The Wizard dialog box pops up asking you to name the object and to specify other aspects of the ATL object such as the threading model. One of the options you may select includes whether the object implements connections. Listing Four shows the C++ class created by the Wizard (with the "Support Connection Points" option checked).

Callback interfaces (outgoing interfaces) are those interfaces described by the object and implemented by the client. When using ATL, the starting place for defining the outgoing interface is within the IDL. Listing Five is an outgoing interface defined in the IDL.

When the project is compiled, the IDL is compiled into a type library. Clients of this object will use this information to know how to implement the callback interface. Notice in this example that the outgoing interface is a dispatch interface (an instance of IDispatch). The outgoing interface doesn't have to be a dispatch interface. However, if it is a dispatch interface, your object can call back to a wider variety of clients. The next step when developing the object is to come up with some way to call through the outgoing interface.

Visual C++ gives you an easy way to call back to an object through an outgoing interface -- the ATL proxy generator. You can get to it by selecting Developer Studio Components from the Components and Controls option from the Add to Project menu and then choosing the ATL Proxy Generator. The ATL Proxy Generator reads type libraries and generates easy-to-call functions for calling back to the client. In this case, the class generated by the ATL proxy generator wraps functions for calling back to the client through IDispatch::Invoke. Listing Six shows the class generated by the proxy generator. Whenever the object wants to make calls back to the client, the object just needs to call Fire_Event1, Fire_Event2, and Fire_Event3. By the way, notice that CProxy_DSomeEvents is derived from IconnectionPointImpl.

While using these tools from within Visual C++ is fairly straightforward and you can use them without understanding the underpinnings, you're always better off understanding how ATL implements connection points so you can make important design decisions and have an easier time debugging the code.

ATL and IConnectionPointContainer

Remember the basic premise behind COM is separating interfaces from their implementations. As long as the client gets back the interface (function table) it requested through QueryInterface, the client is happy. That interface may point to some C++-based code, some VB-based code, some Delphi-based code, or whatever. The client doesn't care what happens behind the interface (as long as it works, of course). When some client code uses IConnectionPointContainer and IConnectionPoint pointers connected to a COM object implemented using ATL, the client is talking to some C++-based source code written using templates. ATL implements IConnectionPointContainer through a template named IConnectionPointContainerImpl.

IConnectionPointContainerImpl is parameterized with one parameter -- the class implementing IConnectionPointContainer (that would be the ATL-based class you're in the middle of implementing). Remember the purpose of IConnectionPointContainer is to provide a way for clients to ask about whether an object supports current outgoing interfaces (each represented by a separate IConnectionPoint interface).

IConnectionPointContainerImpl maintains a collection of IConnectionPoint interfaces using the ATL helper class CComEnum. Before diving into IConnectionPointContainerImpl, we need to examine how ATL maintains a collection of connection points -- through a mechanism called connection maps. ATL's collection points maps are implemented through a set of macros including BEGIN_ CONNECTION_POINT_MAP, CONNECTION_POINT_ENTRY, and END_CONNECTION_POINT_MAP. For example, if you want to set up a list of connection points in the CConnectionObj class, you'd sandwich the CONNECTION_ POINT_ENTRY between the BEGIN_ CONNECTION_POINT_MAP and the END_CONNECTION_POINT_MAP like Listing Seven. This defines the set of connection points that the client can use to connect to the object. The BEGIN_CONNECTION_POINT_MAP defines a function named GetConnMap, which returns a pointer to an array of _ATL_CONNMAP_ENTRY structures. The _ATL_CONNMAP_ENTRY is simply an address that points to an IConnectionPoint interface. The CONNECTION_ POINT_ENTRY macro calculates the address of the pointer on the fly using a helper class named _ICPLocator, which performs a QueryInterface-style operation to find the pointer based on the GUID.

IConnectionPointContainerImpl implements EnumConnectionPoints by simply filling the connection point collection and passing back the IEnumConnectionPoint interface. IConnectionPointContainerImpl uses the connection map's GetConnPoint to retrieve the list of connection points and fill the collection of connection points.

IConnectionPointContainerImpl implements FindConnectionPoint by retrieving the list of connection points using the connection map's GetConnPoint. FindConnectionPoint just rips through the list of connection points finding the requested connection point. When FindConnectionPoint locates the connection, the function passes back the connection point interface after calling AddRef through it. IConnectionPointContainer is fairly straightforward. The other half of the picture is how ATL implements IConnectionPoint.

ATL and IConnectionPoint

ATL implements IConnectionPoint through a templatized class named IConnectionPointImpl. Take a look back at the ATL proxy in Listing Six (notice it derives from IConnectionPointImpl) IConnectionPointImpl's template parameters include the class implementing IConnectionPoint (that would be the proxy class), the GUID of the connection point, and a class that manages the connections.

IConnectionPointImpl implements the individual connection points of an ATL-based COM class. IConnectionPointImpl doesn't have much state -- it maintains the GUID identifying the connection point and a collection of IUnknown interfaces that the object uses to call back to the client. That's really all the state required for implementing a connection point. The rest of IConnecitonPointImpl is implemented as a set of function templates. The two most important functions of IConnectionPointImpl are Advise and Unadvise. When a client wants to subscribe to callbacks, the client calls IConnectionPoint::Advise passing in an unknown pointer. IConnectionPointImpl implements Advise by inserting the unknown pointer into the collection of callback interfaces and returning the vector position in the pdwCookie parameter.

Clients use IConnectionPoint::Unadvise to stop receiving callbacks. IConnectionPointImpl implements Unadvise by looking up the unknown pointer using the dwCookie parameter, which happens to be the index into the collection of unknown pointers. If Unadvise finds the unknown pointer in the vector, Unadvise removes the pointer from the advise list and then releases the pointer.

The rest of the IConnectionPoint functions (GetConnectionInterface, GetConnectionPointContainer, and EnumConnections) aren't used as often. However, IConnectionPoint implements them just to make sure the interface implementation contract is complete. IConnectionPointImpl implements GetConnectionInterface by simply returning the GUID representing the connection point. IConnectionPointImpl maintains a pointer to the connection point container class (it was passed in as a template parameter). IConnectionPointImpl implements GetConnectionPointContainer by casting that pointer as IConnectionPointContainerImpl to return the IConnectionPointContainer vtable (this, of course, assumes the IConnectionPointContainer class is derived from IConnectionPointContainerImpl). Finally, IConnectionPointImpl implements EnumConnections by filling a CComEnum-based class with the unknown pointers known by the object and passing back the IEnumConnections interface implemented by the CComEnum-based class.

Conclusion

While developers have been able to create ActiveX Controls using MFC for some time now, using MFC imposes certain design decisions and your control has to link to the MFC DLL. ATL is a lightweight framework for implementing COM classes. In addition to providing all the machinery for writing basic COM classes, ATL includes the interfaces necessary to implement ActiveX Controls. Part of that machinery includes describing interfaces that the client is willing to implement so the ActiveX Control can call back to the client. ATL fully supports this connection mechanism through its IConnectionPointContainerImpl and IConnectionPointImpl classes.

DDJ

Listing One

ISomeInterface* pSomeInterface = NULL;HRESULT hr;
hr = CoCreateInstance(CLSID_SomeObject, NULL, CLSCTX_ALL, 
                                   IID_ISomeInterface, *pSomeInterface);
if(SUCCEEDED(hr)) {
   pSomeInterface->Function1();
   pSomeInterface->Function2();
   pSomeInterface->Release();
}

Back to Article

Listing Two

interface IConnectionPoint : IUnknown {  HRESULT GetConnectionInterface(IID *pIID) = 0;
  HRESULT GetConnectionPointContainer(IConnectionPointContainer **ppCPC) = 0;
  HRESULT Advise(IUnknown *pUnk, DWORD *pdwCookie) = 0;
  HRESULT Unadvise(DWORD dwCookie) = 0;
  HRESULT EnumConnections(IEnumConnections **ppEnum) = 0;
};

Back to Article

Listing Three

interface IConnectionPointContainer : IUnknown {  HRESULT EnumConnectionPoints(IEnumConnectionPoints **ppEnum) = 0;
  HRESULT FindConnectionPoint(REFIID riid, IConnectionPoint **ppCP) = 0;
};

Back to Article

Listing Four

class ATL_NO_VTABLE CConnectableObj :    public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CConnectableObj, &CLSID_ConnectableObj>,
   public IConnectionPointContainerImpl<CConnectableObj>,
   public IDispatchImpl<IConnectableObj, &IID_IConnectableObj, 
                        &LIBID_CONNECTABLEOBJSVRLib>
{
 ...
};

Back to Article

Listing Five

[   uuid(5BE91C40-BA0D-11D1-8CAA-D099043C7E50),
   version(1.0),
   helpstring("connectableobjsvr 1.0 Type Library")
]
library CONNECTABLEOBJSVRLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");
   [
      uuid(21C85C43-0BFF-11d1-8CAA-FD10872CC837),
      helpstring("Events created from the control")
   ]
   dispinterface _DSomeEvents {
      properties:
      methods:
        [id(1)] void Event1([in]short x, [in] short y);
        [id(2)] void Event2([in] float x);
        [id(3)] void Event3();
   }
   [
      uuid(5BE91C4E-BA0D-11D1-8CAA-D099043C7E50),
      helpstring("ConnectableObj Class")
   ]
   coclass ConnectableObj
   {
      [default] interface IConnectableObj;
      [default, source] dispinterface _DSomeEvents;
   };
}

Back to Article

Listing Six

template <class T>class CProxy_DSomeEvents : 
   public IConnectionPointImpl<T, &DIID__DSomeEvents, CComDynamicUnkArray>
{
public:
//methods:
//_DSomeEvents : IDispatch
public:
   void Fire_Event1(short x, short y)
   {   
      // gnarly code for calling 
      //   IDispatch::Invoke()
   }
   void Fire_Event2(float x) 
   {
      // gnarly code for calling 
      //   IDispatch::Invoke()
   }
   void Fire_Event3()
   {
      // gnarly code for calling 
      //   IDispatch::Invoke()
   }
};

Back to Article

Listing Seven

BEGIN_CONNECTION_POINT_MAP(CConnectableObj)   CONNECTION_POINT_ENTRY(DIID__DSomeEvents)
END_CONNECTION_POINT_MAP()

Back to Article


Copyright © 1998, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.