How Connections Work

[Previous] [Next]

Bidirectional communication between two pieces of software is a common requirement. Given two independent software components, it's often useful to have an object notify its client or clients of various goings-on. The classic example is ActiveX controls, in which 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 of the callback interface. These interfaces defined by the object and implemented by the client are called outgoing, or outbound, interfaces.

Incoming vs. Outgoing Interfaces

Most of the interfaces we've been working with throughout the book have been incoming interfaces—that is, interfaces implemented by a COM object. Incoming interfaces are 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 shown here:

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

Incoming interfaces are the norm for COM objects, providing a way for clients to call in to COM objects. Figure 12-1 illustrates a COM object with incoming interfaces.

click to view at full size.

Figure 12-1. A COM object with incoming interfaces.

The other kind of interface, an outgoing interface, is implemented by the client so that the COM object can call the client. When developing bidirectional communication between two objects, the client must implement an interface that's defined by the object. The trick to getting bidirectional communication set up is to get the lollipop (the interface implemented by the client) over to the object. Figure 12-2 illustrates an outgoing COM interface.

click to view at full size.

Figure 12-2. An outgoing COM interface.

COM already specifies several standard incoming/outgoing interface pairs that define a connection mechanism. One of the best examples is an interface named IAdviseSink. IAdviseSink is used in conjunction with the IDataObject interface. The IAdviseSink and IDataObject pair of interfaces is useful for transferring presentations (and other things) between OLE Document objects and OLE Document clients. Here's how the two interfaces work together. Objects implement IDataObject. Once clients connect to the object and call QueryInterface for IDataObject, the client can plug its implementation of IAdviseSink in to the client by using IDataObject::Advise and begin receiving notifications whenever the data inside IDataObject changes. This happens whenever the object calls back to the client's implementations of IAdviseSink::OnViewChange or IAdviseSink::OnDataChange. The complementary IAdviseSink and IDataObject interfaces are shown here:

 interface IAdviseSink : IUnknown {     HRESULT OnDataChange(FORMATETC *pFormatetc, STGMEDIUM *pStgmed);     HRESULT OnViewChange(DWORD dwAspect, LONG lindex);     HRESULT OnRename(IMoniker *pmk);     HRESULT OnSave();     HRESULT OnClose(); }; interface IDataObject : IUnknown {     HRESULT GetData(FORMATETC *pformatetcIn,          STGMEDIUM *pmedium);     HRESULT GetDataHere(FORMATETC *pformatetc,          STGMEDIUM *pmedium);     HRESULT QueryGetData(FORMATETC *pformatetc);     HRESULT GetCanonicalFormatEtc(FORMATETC *pformatectIn,          FORMATETC *pformatetcOut);     HRESULT SetData(FORMATETC *pformatetc,          STGMEDIUM *pmedium, BOOL fRelease);     HRESULT EnumFormatEtc(DWORD dwDirection,          IEnumFORMATETC** ppenumFormatEtc);     HRESULT DAdvise(FORMATETC *pformatetc, DWORD advf,          IAdviseSink *pAdvSink,         DWORD *pdwConnection);     HRESULT DUnadvise(DWORD dwConnection);     HRESULT EnumDAdvise(IEnumSTATDATA **ppenumAdvise); }; 

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

Microsoft has taken a shot at solving this problem by defining connectable objects. Microsoft invented connectable objects to connect an ActiveX control to its client so that 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.

Connection Point Interfaces

Let's start by examining the COM interfaces involved in connections—IConnectionPoint and IConnectionPointContainer. The object (rather than the client) implements both these interfaces. These interfaces exist for the sole purpose of connecting an object to its client. Once the connection is made, they drop out of the picture.

Let's look at IConnectionPoint first. The following code shows the interface in the raw:

 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; }; 

You can probably guess the nature of this interface from the function names. Objects can implement this interface so that clients have a way to subscribe to events. Once a client acquires this interface, the client can ask to subscribe to data change notifications via the Advise function. Notice that the Advise function takes an IUnknown pointer, so the callback interface can be any COM interface at all. IConnectionPoint also contains the complementary Unadvise function. Clients use this function to terminate the connection between the client and the object. We'll see how the other functions are useful a little later in the chapter.

Clients can implement any callback interface and use IConnectionPoint to hand the interface over to the object. Once the object has the callback interface (passed via Advise's first parameter), the object can easily call back to the client. This action begs the question of how the client can acquire a connection point in the first place. The answer is through the IConnectionPointContainer interface, shown here:

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

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

As you can tell from the second function, FindConnectionPoint, IConnectionPointContainer is the interface a COM client uses to acquire a pointer to an IConnectionPoint interface (which the client can then use to establish a connection). Let's take a look at the whole process.

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 (in other words, is capable of calling back to the client), the client can ask for a specific outgoing interface by calling IConnectionPointContainer::FindConnectionPoint using the GUID that represents 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 that the object can call back to the client.

Connection Points and IDL

IDL specifically supports defining interfaces as outgoing interfaces. Listing 12-1 shows some basic IDL containing an incoming and an outgoing interface.

Listing 12-1. IDL code describing an incoming and an outgoing interface

 import "oaidl.idl"; import "ocidl.idl";     [         object,         uuid(04D6A3EE-A7E6-11D2-8039-84DD6A000000),         helpstring("IIncoming Interface"),     ]     interface IIncoming : IUnknown     {         HRESULT Method1();         HRESULT Method2();     }; [     uuid(04D6A3E2-A7E6-11D2-8039-84DD6A000000),     version(1.0),     helpstring("SomeServer 1.0 Type Library") ] library SomeServerLib {     importlib("stdole32.tlb");     importlib("stdole2.tlb");     [         uuid(04D6A3F0-A7E6-11D2-8039-84DD6A000000),         helpstring("IEvents Interface")     ]     interface IEvents     {         HRESULT Event1([in]short x);         HRESULT Event2([in]BSTR bstr);     };     [         uuid(04D6A3EF-A7E6-11D2-8039-84DD6A000000),         helpstring("SomeObj Class")     ]     coclass SomeObj     {         [default] interface IIncoming;         [default, source] dispinterface IEvents;     }; }; 

When examining this code, you'll notice that the SomeObj class involves two interfaces: IIncoming and IEvents. IIncoming has the [default] interface attribute applied to it. This means that when a scripting language uses this coclass, the scripting language understands IIncoming as the object's default interface.

Notice also that the IEvents interface has the [source] attribute applied to it. The source keyword indicates that the IEvents interface is an outgoing interface (an interface implemented by the client and called by the object).

Listing 12-2 illustrates the code required to set up and tear down a connection using the interface described in the preceding IDL.

Listing 12-2. Using connection points.

 #include "SomeSvr_i.c" // Produced by MIDL #include "SomeSvr.h" // Produced by MIDL HRESULT ConnectToObject(IUnknown *pObject,      IEvents *pEventSink,      DWORD *pdwCookie) {     IConnectionPointContainer *pcpc = 0;     HRESULT hr;     hr = pObject->QueryInterface(IID_IConnectionPointContainer,         (void**)&pcpc);     if(SUCCEEDED(hr)) {         IConnectionPoint *pcp = 0;         hr = pcpc->FindConnectionPoint(IID_IEvents,              &pcp);         if(SUCCEEDED(hr)) {             hr = pcp->Advise(pEventSink,                  pdwCookie);             pcp->Release( );         }         pcpc->Release( );     }     return hr; } HRESULT DisconnectFromObject(IUnknown *pObject,      DWORD dwCookie) {     IConnectionPointContainer *pcpc = 0;     HRESULT hr;     hr = pObject->QueryInterface(IID_IConnectionPointContainer,         (void**)&pcpc);     if(SUCCEEDED(hr)) {         IConnectionPoint *pcp = 0;         hr = pcpc->FindConnectionPoint(IID_IEvents,              &pcp);         if(SUCCEEDED(hr)) {             hr = pcp->Unadvise(pdwCookie);             pcp->Release( );         }         pcpc->Release( );     }     return hr; } class CEventSink : public IEvents { public:     STDMETHODIMP QueryInterface(REFIID riid,         void** ppv)     {         if(riid == IID_IUnknown ||             riid == IID_IEvents)         {             *ppv = static_cast<IEvents*>(this);             ((IUnknown*)(*ppv))->AddRef();             return S_OK;         } else         {             return E_NOINTERFACE;         }     }     STDMETHODIMP_(ULONG) AddRef()     {         return 2; // This is a global object.     }     STDMETHODIMP_(ULONG) Release()     {         return 1; // This is a global object.     }     STDMETHODIMP Event1(short x)     {         // Handle event 1.         return S_OK;      }     STDMETHODIMP Event2(BSTR bstr)     {         // Handle event 2.         return S_OK;     } }; main()  {     IUnknown* pUnk;     HRESULT hr;     hr = CoCreateInstance(CLSID_SomeObject, NULL,          CLSCTX_ALL, IID_IUnknown,         (void**)pUnk);     if(SUCCEEDED(hr))     {         CEvents events;         IEvents* pEvents;         DWORD dwCookie;         events.QueryInterface(IID_IEvents, (void**)&pEvents);          ConnectToObject(pUnk, pEvents, &dwCookie);         // Do some stuff here that might generate events.         DisconnectFromObject(pUnk, dwCookie);     } } 

In addition to the normal connection and activation mechanism defined by the ActiveX protocol, COM defines another way to activate controls: through the IQuickActivate interface.

IQuickActivate

ActiveX controls originally started life as OLE controls—COM objects that followed the full OLE Embedding protocol. In addition to the numerous round-trips required by the OLE Embedding protocol to activate the control, OLE controls also required round-trips to hook up the event sink. The result was an extraordinary number of unnecessary round-trips—especially if the control somehow ended up in a different apartment than the client inhabited. To handle this issue of requiring many round-trips to activate a control, the ActiveX specification detailed a new interface named IQuickActivate, which bundles all the parameters and handshaking that must go on between the client and the object to perform activation. Instead of taking a ton of round-trips to activate a control, a client activates a control by acquiring the IQuickActivate interface from the control, packaging all the parameters in QACONTAINER and QACONTROL structures, and calling IQuickActivate::QuickActivate. The following code shows the IQuickActivate interface and associated structures:

 typedef struct  tagQACONTAINER {     ULONG cbSize;     IOleClientSite *pClientSite;     IAdviseSinkEx *pAdviseSink;     IPropertyNotifySink *pPropertyNotifySink;     IUnknown *pUnkEventSink;     DWORD dwAmbientFlags;     OLE_COLOR colorFore;     OLE_COLOR colorBack;     IFont *pFont;     IOleUndoManager *pUndoMgr;     DWORD dwAppearance;     LONG lcid;     HPALETTE hpal;     IBindHost *pBindHost;     IOleControlSite *pOleControlSite;     IServiceProvider *pServiceProvider; } QACONTAINER; typedef struct  tagQACONTROL {     ULONG cbSize;     DWORD dwMiscStatus;     DWORD dwViewStatus;     DWORD dwEventCookie;     DWORD dwPropNotifyCookie;     DWORD dwPointerActivationPolicy; } QACONTROL; interface IQuickActivate : public IUnknown {     HRESULT QuickActivate([in] QACONTAINER *pQaContainer,         [out, in] QACONTROL *pQaControl);     HRESULT SetContentExtent([in] LPSIZEL Sizel);     HRESULT GetContentExtent([out] LPSIZEL pSizel; }; 

Using this protocol, clients can quickly activate the objects and more efficiently set up an event sink and a property notify sink. Objects still need to implement connection points, even when clients activate them using QuickActivate.



Inside Atl
Inside ATL (Programming Languages/C)
ISBN: 1572318589
EAN: 2147483647
Year: 1998
Pages: 127

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net