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

.NET

How MFC Does ActiveX Connections


Dr. Dobb's Journal April 1997: Undocumented Corner

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


When they fully grasp the gestalt of the Component Object Model (COM) for the first time, many people ask this question: "I certainly understand being able to call into a COM object using an interface. But what if I want to have my COM object call back out to the client?" OLE/ActiveX Connections were invented to address this question. This month we'll take a look at how two COM objects can set up a communication scheme whereby the object calls back to the client. We'll first examine how Connections work, then look at how MFC implements them.

As it turns out, having a two-way communication channel 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. ActiveX Control Events are an example of the utility of ActiveX Connections.

Incoming versus Outgoing Interfaces

The ability for a client to call into a COM object has been available 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; see 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 is another kind of interface called an "outgoing interface." Outgoing interfaces (see Figure 2) are those interfaces implemented by the client so that the COM object can call the client.

COM already defines several standard outgoing interfaces. One of the best examples is an interface named IAdviseSink, which is used in conjunction with another interface named IDataObject. These are useful interfaces for transferring presentations (among other things) between OLE Document objects and OLE Document clients. Objects implement IDataObject. Once connected 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 very specific case. The interfaces and the connection process are well-understood by both the client and the object. But suppose 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. How would you do it?

Microsoft has taken a shot at it and defined connectable objects. Microsoft invented connectable objects 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.

The Interfaces

We'll start by examining the COM interfaces involved in connections -- IConnectionPoint and IConnectionPointContainer. Both 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 (much like monikers). Let's look at IConnectionPoint first; see Listing Two.

You can probably guess the nature of this interface from the function names. Objects implement this interface so that 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. Notice the Advise function takes an IUnknown, so the callback interface can be any COM interface at all. Of course this interface also contains the complementary Unadvise function.

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 (also known as the client). However, this interface is implemented by the object. A more descriptive name for this interface may have been IConnectionPointHolder because it holds connection points.

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, it 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. MFC remains the most convenient way to write ActiveX Controls, so you might expect MFC to implement connections. MFC's support for connections consists of three classes and a set of macros.

How MFC Does It

Most of MFC's support for connections lies within the CCmdTarget class. A quick gander at CCmdTarget reveals the following connection-related parts of CCmdTarget. Listing Four shows these parts.

The minute you see the DECLARE_CONNECTION_MAP macro, you should immediately recognize it as an MFC-style lookup mechanism (think of message maps). Hmm. It appears as though MFC's going to use a similar device for connections.

Digging through the source files a bit more reveals some new macros: BEGIN_CONNECTION_PART, END_CONNECTION_PART, CONNECTION_IID, BEGIN_ CONNECTION_MAP, END_CONNECTION_ MAP, and CONNECTION_PART. If you're at all familiar with MFC's mechanism for implementing COM, this may look familiar. MFC implements COM using macros that expand into nested classes, with each nested class implementing an interface. MFC's COM macros provide a lookup mechanism for QueryInterface that maps interface IDs to the vtbls implemented by the nested classes. Again, it appears as though something similar is going to happen with MFC's implementation of connections. Let's take a closer look at the macros, starting with BEGIN_CONNECTION_PART, END_CONNECTION_PART, and CONNECTION_IID. MFC's support for connections is implemented using nested classes. For every connection point implemented by a CCmdTarget-derived object, that object has a nested class to handle the connection point. Remember again what the connection point has to do -- it has to implement IConnectionPoint so clients can subscribe and unsubscribe to notifications. BEGIN_CONNECTION_PART adds a nested class derived from CConnectionPoint, which is documented -- it simply implements the IConnectionPoint interface. CConnectionPoint is derived from CCmdTarget and has the MFC COM macros for implementing nested classes and interface map.

The best way to examine these macros is to find a situation where they're used. MFC-based ActiveX controls, for instance, have two connection points by default: one for notifying clients about events and another for notifying clients about property changes. In fact, if you look in AFXCTL.H at the class COleControl, you'll see a connection part for events and a connection part for properties.

The BEGIN_CONNECTION_PART macro adds a nested class derived from CConnectionPoint to COleControl for handling the connections. CConnectionPoint implements IConnectionPoint and includes a few handy overridable functions. The CONNECTION_ IID macro overrides the function CConnectionPoint::GetIID necessary to implement IConnectionPoint::GetInterface. Finally, END_CONNECTION_PART ends the nested class declaration and adds that class as a data member to the CCmdTarget-derived class. Using these macros effectively implements IConnectionPoint for each connection supported by the COM object.

Once the connections are defined, CCmdTarget needs a way to look up the connections. Remember, once the client acquires the IConnectionPointContainer interface, the client will ask for a specific connection (sounds like the perfect job for a lookup table).

If you're familiar with how MFC does lookups, connection maps will seem old hat. Just like MFC uses lookup tables for message handling (message maps), COM support (interface maps), and Automation (dispatch maps), MFC uses a lookup table to support IConnectionPointContainer using connection maps.

Connection maps follow the same form as other MFC lookup tables -- there's a BEGIN_CONNECTION_MAP macro, END_CONNECTION_MAP macro, and CONNECTION_PART macro to fill in the table.

Adding the BEGIN_CONNECTION_MAP macro to a CCmdTarget-derived class implements the member function GetConnectionMap (which was added using the DECLARE_CONNECTION_MAP macro), and a pointer to an array of "connection entries" to that class. The CONNECTION_ PART macro pairs an interface ID to a CConnectionPoint class that implements IConnectionPoint. The END_CONNECTION_MAP macro terminates the array of IID/nested class pairs.

At this point, the class implements a set of connection points and a lookup mechanism for the connection points. For example, Figure 3 illustrates an ActiveX Control CSomeControl derived from COleControl.

Using the connection macros builds a structure similar to the one illustrated, where each connection point is represented by a nested class, and the mechanism for the connection lookup is chained together in the hierarchy (in exactly the same way as the rest of MFC's lookup devices, like message maps). The only things left are an implementation of IConnectionPointContainer and the rest of the IConnectionPoint functions (like Advise and Unadvise).

MFC's IConnectionPointContainer Implementation

Notice the XConnPtContainer structure declaration in the middle of the CCmdTarget declaration (Listing Four). CCmdTarget holds a pointer to MFC's implementation of IConnectionPointContainer in m_xConnPtContainer.m_vtbl. As you can probably guess from the name, CCmdTarget::EnableConnections fills this variable. Listing Five shows how the folks in Redmond fill the CCmdTarget class's connection point container vtable (this function can be found in OLECONN.CPP).

If you've ever looked into MFC's Automation support, you may notice how similar EnableConnections is to EnableAutomation. At any rate, calling EnableConnections is a requirement to getting the connection machinery up and running.

Notice the use of a class called COleConnPtContainer. This is an undocumented class that MFC uses to implement IConnectionPointContainer. If fact, if you look in the MFC source file OLECONN.CPP, you'll see the definition for COleConnPtContainer; see Listing Six. There isn't a whole lot to COleConnPtContainer -- it simply implements IConnectionPointContainer. The interesting functions are EnumConnectionPoints and FindConnectionPoint.

Clients call EnumConnectionPoints to obtain an enumerator to find out all the available connection points to which it can connect. MFC's implementation creates an instance of the CEnumConnPoints class, another undocumented MFC class. CEnumConnPoints is based on CEnumArray, MFC's generic void pointer COM-style enumerator. CEnumConnPoints holds a list of IConnectionPoint pointers. In addition to the regular enumeration functions, CEnumConnPoints has a function named AddConnectionPoint, which adds a pointer to an IConnectionPoint interface to the enumeration.

Whenever clients call IConnectionPointContainer::EnumConnectionPoints, the object builds a CEnumConnPoints on the fly and starts adding connection points to the enumerator. MFC's implementation first calls an overridable function in COleConnPtContainer named GetExtraConnectionPoints. This is overridable, so you can bypass the entire macros listed earlier if you want to add connection points. COleConnPtContainer takes all the connection points from GetExtraConnectionPoints and adds them to the enumerator. Then COleConnPtContainer walks the connection maps, adding connections to the enumerator. At the end, the client gets back an enumerator.

The other interesting function is FindConnectionPoint. Clients call FindConnectionPoint to figure out if a certain connection is available. FindConnectionPoint behaves similarly to EnumConnectionPoints, except FindConnectionPoint searches for a single connection point (named by GUID). MFC's implementation first calls the virtual function COleConnPtContainer::GetConnectionHook, which also lets you override the macros. If the connection point is available through GetConnectionHook, the object returns that connection point. Otherwise, the object walks the connection point map looking for the connection.

Back to IConnectionPoint: Making the Connection

Actual connections are made in IConnectionPoint::Advise, which takes two parameters: a pointer to IUnknown and a pointer to a DWORD. The IUnknown pointer is the interface to use to call back to the client. The DWORD represents a token the client can later use to turn off the connection.

When connecting, MFC's implementation checks to make sure that a connection is available. MFC lets you control the number of clients that can connect by overriding CConnectionPoint::GetMaxConnections. Then MFC calls QueryInterface through the IUnknown pointer to make sure the correct callback interface is available. If everything is still "go" at this point, CConnectionPoint::Advise makes the connection. If this is the first connection to this connection point, MFC caches the callback interface in CConnectionPoint::m_pUnkFirstConnection. Otherwise, Advise adds the interface pointer to the pot. CConnectionPoint maintains an array of callback interfaces (that is, interface pointer to clients that are interested in being called back by the object) in a CPtrArray named m_pConnections. CConnectionPoint caches the first callback interface as an optimization. Most of the time there will be only one client subscribing to a particular connection point. By caching the first callback pointer, MFC avoids the overhead of maintaining an array if it's unnecessary. CConnectionPoint::Advise returns the address of the callback interface as the token used by the client to call Unadvise.

Clients use CConnectionPoint::Unadvise to disconnect from the object (that is, to unsubscribe from any callback notifications). Clients call Unadvise using the token received back from the initial call to Advise. If the connection being terminated is the first one (that is, the token passed in is equal to CConnectionPoint::m_pUnkFirstConnection), CConnectionPoint::Unadvise releases the callback pointer (remember, Advise implicitly performed an AddRef by calling QueryInterface and sets the callback pointer to NULL). If the interface isn't the first connection, Unadvise goes through the array of connections looking for the one matching the token. When Unadvise finds the callback interface, Unadvise releases it and removes it from the array.

Once the connection is made, the object has a way to call back to the client. Of course, a classic application of this technology is ActiveX Controls. ActiveX Controls call back to their clients whenever they want to report an event. The typical callback mechanism is IDispatch -- the Automation interface. However, objects are not restricted to calling back to clients through IDispatch. The connection mechanism is flexible to accommodate any interface. However, if you'd like to use something other than IDispatch for your MFC-based ActiveX Controls, you're on your own. You'll probably want to use MFC's connection facilities.

Conclusion

Connections address the need for clients and objects to maintain a two-way communication channel. Callback interfaces have been available since COM first came into being several years ago. However, IConnectionPoint and IConnectionPointContainer define a protocol by which any general connection may be made. Connections were first added for ActiveX Controls (then called OLE Controls) to connect the main event set and the property notify interface to the client, but you can use the connection mechanism to set up a two-way communication channel between multiple objects whenever necessary.

DDJ

Listing One

ISomeInterface* pSomeInterface = NULL;HRESULT hr;


</p>
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 CCmdTarget : public CObject { ...
   EnableConnections();
   DECLARE_CONNECTION_MAP()
   struct XConnPtContainer {
      DWORD m_vtbl; // place-holder for IConnectionPointContainer vtable
   } m_xConnPtContainer;
   virtual BOOL GetExtraConnectionPoints(CPtrArray* pConnPoints);
   virtual LPCONNECTIONPOINT GetConnectionHook(const IID& iid);


</p>
   friend class COleConnPtContainer;
 ...
};

Back to Article

Listing Five

void CCmdTarget::EnableConnections() {   // construct an COleConnPtContainer instance just to get to the vtable
   COleConnPtContainer cpc;
   // copy the vtable (and other data) to make sure it is initialized
   m_xConnPtContainer.m_vtbl = *(DWORD*)&cpc;
}

Back to Article

Listing Six

class COleConnPtContainer : public IConnectionPointContainer{
public:
   STDMETHOD_(ULONG, AddRef)();
   STDMETHOD_(ULONG, Release)();
   STDMETHOD(QueryInterface)(REFIID, LPVOID*);
   STDMETHOD(EnumConnectionPoints)(LPENUMCONNECTIONPOINTS* ppEnum);
   STDMETHOD(FindConnectionPoint)(REFIID iid, LPCONNECTIONPOINT* ppCP);
};


Back to Article


Copyright © 1997, 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.