Introduction
This article is intended to explain the concept behind connection points with
a clear practical example, which will demonstrate an in-process COM server and an
MFC client that uses the server.
What Exactly Is It?
It is a method used by a COM object to call back to the client. In other
words, clients get call back notification from the COM object.
Perhaps you have heard about callback functions. Well, it goes like this.
Suppose you have a COM object that exposes an interface IArithematic
, and has a
resource intensive method, say Add(int a,int b)
- ("Anything for
simplicity," as the hermit said when he took up residence naked in an
Himalayan cave. Sam Weller�Pickwick Papers). Imagine that this method is going
to take a lot of time and you do not want to wait until that task is finished.
You can use that time for something. So here is where a connection point comes
in. You assign a function ExecutionOver(int Result)
in your client code, which
the COM object can call after it has finished executing the Add method.
So, when the COM object is finished with the task, it calls the client
function ExecutionOver
(passing the result of addition). The client happily pops
up the result in a message box. That's the whole gist. We shall go into the
details now.
How Does the COM Object Know How to Call ExecutionOver??
Imagine that the client exposes an interface ISink
, which has a method
ExecutionOver(int result)
. Now, if the client can pass this interface to the COM
object, the COM object can happily call ExecutionOver
. For example, in the COM
the code fragment may look like this:
ISink *pClientSink;
HRESULT Add(int a , int b)
{
pClientSink->ExecutionOver(a+b);
}
This is what really happens. The rest is for making this whole thing generic
enough. Microsoft has implemented this by defining connectable objects. Let's
start by examining the COM interfaces involved in connections - IConnectionPoint
and IConnectionPointContainer
. The object (rather than the client) implements
both these interfaces.
Both the interfaces are shown below.
interface IConnectionPointContainer : IUnknown {
HRESULT EnumConnectionPoints(
IEnumConnectionPoints **ppEnum) = 0;
HRESULT FindConnectionPoint(REFIID riid,
IConnectionPoint **ppCP) = 0;
};
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;
};
Now, let's go one step at a time and see how the whole thing works.
A COM client calls CoCreateInstance
to create the COM object. Once the client
has an initial interface, the client can ask the object whether 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 that IConnectionPoint
interface pointer and calls IConnectionPoint::Advise(
[in] IUnknown *pUnk, [out] DWORD *pdwCookie)
to hand over its implementation of
the callback interface so that the object can call back to the client. To make
it clear once more, the pointer to IUnknown
passed to the advise method is the
pointer of an interface that's defined and implemented in the client EXE.
Okay, now let's illustrate the whole thing by a practical example.
- Create a new ATL-COM AppWizard Project and name it ConnectionCOM.
- Right-click on the class view and create a new ATL object.
Name it Add (interface IAdd).
Before you click the OK button, be sure to check the Support Connection
point checkbox.
Click OK.
Note the classes generated in the class view. You will find one IAdd
and
_IAddEvents
.
The latter is just a proxy class; it is to be implemented on the client side. It
came because we ticked the Connection_Points
check box.
Add a method 'Add(int a,int b)
' to IAdd
interface and a method
'ExecutionOver(int Result)
' to the _IAddEventsInterface
. The class view will be as shown below.
But because we have selected a dual interface and we do not need all that
hassle, let's take out the support for IDispatch
by editing the IDL file. Below
is the original file.
:
:
library CONNECTIONCOMLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(AFE854B0-246F-4B66-B26F-A1060225C71C),
helpstring("_IAddEvents Interface")
]
interface _IAddEvents : IUnknown
{
[id(1), helpstring("method ExecutionOver")] HRESULT
ExecutionOver(intResult);
};
[
uuid(630B3CD3-DDB1-43CE-AD2F-4F57DC54D5D0),
helpstring("Add Class")
]
coclass Add
{
[default] interface IAdd;
[default, source] interface _IAddEvents ;
};
};
Whew! The client side is almost finished now. Now, do a build because we need
the type library to do a neat thing with ATL. Now, right-click on the CoClass
and click Implement Connection Point.
Check _IAddEvents in the ensuing dialog box.
A CProxy_IAddEvets
class is generated with the Fire_ExecutionOver(int result)
method. This will take care of how the COM object will call the client interface
(and takes care of multiple clients calling the same COM DLL and other such
issues). Now, let's implement our old IAdd
Interface Add method.
STDMETHODIMP CAdd::Add(int a, int b)
{
Sleep(2000);
Fire_ExecutionOver(a+b);
return S_OK;
}
Do a build and the COM is ready. Make sure that the COM is registered.
Now the Client Side
Create a new MFC AppWIzard(exe) Dialog based project - ConnectionClient
. It
looks like this:
Now comes the main part.
We create a CSink
class that derives form _IAddEvents
. You can use the class
wizard for it. You have to supply the header where the _IAddEvents
interface is
defined. For that, copy the ConnectionCOM.h and ConnectionCOM.tlb files to your
client EXE's project folder and add these lines to Sink.h file.
#include "ConnectionCOM.h"
#import "ConnectionCOM.tlb" named_guids raw_interfaces_only
Now we have the additional task of implementing each method defined in the _IAddEvents
interface. (Never forget that a COM interface is just a pure abstract base class
and that the derived class has to implement all of its methods.)
So, let's implement the first ExecutionOver
:
STDMETHODIMP ExecutionOver(int Result)
{
CString strTemp;
strTemp.Format("The result is %d", Result);
AfxMessageBox(strTemp);
return S_OK;;
};
Now comes QueryInterface, AddRef, and Release.
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void
**ppvObject)
{
if (iid == IID__IAddEvents)
{
m_dwRefCount++;
*ppvObject = (void *)this;
return S_OK;
}
if (iid == IID_IUnknown)
{
m_dwRefCount++;
*ppvObject = (void *)this;
return S_OK;
}
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef()
{
m_dwRefCount++;
return m_dwRefCount;
}
ULONG STDMETHODCALLTYPE Release()
{
ULONG l;
l = m_dwRefCount--;
if ( 0 == m_dwRefCount)
delete this;
return l;
}
We are now almost there.
Now, on the dialog class on the SendToServer
Button Click event, we shall do
the last bit of coding.
#include "Sink.h" // for our CSink class
#include <atlbase.h> // for ATL smart pointers
void CConnectionClientDlg::OnSendToServer()
{
UpdateData(1);
HRESULT hr;
hr =CoInitialize(NULL);
if(hr != S_OK)
return -1;
CComPtr<IAdd> pAdd;
hr =pAdd.CoCreateInstance(CLSID_Add);
if(hr != S_OK)
return -1;
IConnectionPointContainer * pCPC;
hr = pAdd->QueryInterface(IID_IConnectionPointContainer,
(void **)&pCPC);
if ( !SUCCEEDED(hr) )
{
return hr;
}
hr = pCPC->FindConnectionPoint(IID__IAddEvents,&pCP);
if ( !SUCCEEDED(hr) )
{
return hr;
}
pCPC->Release();
IUnknown *pSinkUnk;
CSink *pSink;
pSink = new CSink;
if ( NULL == pSink )
{
return E_FAIL;
}
hr = pSink->QueryInterface (IID_IUnknown,(void **)&pSinkUnk);
hr = pCP->Advise(pSinkUnk,&dwAdvise);
pAdd->Add(m_number1 ,m_number2);
pCP->Release();
return hr;
}
Now, do a build of the dialog EXE. Now fire away. That's it for connection
points.