Steve Robinson and Alex Krasilshchikov
Panther Software
The first DCOM client application we built was ThePusher. In that example you learned how easy it is to connect to an out-of-process server and push data into the out-of-process application. The next step is getting data out of the server and into a different client application. In the Windows development world, if you had multiple views and a single document, one view might push data into the document and send a message to the other views telling them to get new data out of the document. Or, even better, the document could be real smart and automatically send the data to the other views whenever they needed updating. This is exactly what we are going to do with our DCOM server. Whenever TheServer gets new data, we are going to automatically send the information downstream to connected clients. However, Windows messages do not work across COM. You can't just call the trusty API function ::SendMessage. There is, however, a fantastic solution with COM and DCOM called connection points.
Connection points are one of the most powerful capabilities of COM and DCOM. In fact, they are at the root of the most popular of all COM objects, ActiveX controls. In COM terminology, connection points are a mechanism through which advisory connections are established between a sink object and a source object. In everyday programming terms, that means that a source object has the ability to hold some outside object.
What we are going to do is take a pointer to an interface in our client and pass it to a server. The interface in our client will have methods. Therefore, when we pass the pointer to an interface in our client to the server, the server will be able to call methods in the client. In COM terminology, we are going to create an IConnectionPoint and place it into an IConnectionPointContainer. This is very similar to callback functions in C. Each IConnectionPoint interface inside the server wraps around a proprietary client interface. This proprietary interface is called a sink, and it has to be implemented by each client that wants the server to push data to this interface. Let's take a look at IConnectionPoint and IConnectionPointContainer.
The IConnectionPointContainer interface is implemented in a server and indicates the existence of outgoing interfaces. It also provides access to these holders of outgoing interfaces, called IConnectionPoints. A proprietary sink interface, IConnectionPoint, is declared in a server but is actually implemented in a client. A client application does a QueryInterface to get an interface to the server. Once the client has an interface to the server, it queries the server for a connection point container, and finds a connection point container interface within the server. Next, the client finds the connection point interface that it desires. Once the pointer to the appropriate connection point interface is found, the client uses this pointer to call a method belonging to IConnectionPoint called Advise. Advise establishes a connection between a connection point in the server and the client's sink interface. The client's sink implements the outgoing interface supported by this connection point that resides in the server. The secret, as noted earlier, is that the server actually declares the client's sink, which gives it knowledge of the client's sink COM object. Implementing connection points without ATL is actually fairly straightforward, and looks very similar to the following code:
IUnknown* pUnknown != NULL; ISomeObject* pISomeObject; SCODE sc = pUnknown->QueryInterface(IID_ISomeObject, (void**)&pISomeObject); if (SUCCEEDED(sc) && pISomeObject != NULL) { IConnectionPointContainer* pIConnectionPointContainer = NULL; sc = pISomeObject->QueryInterface( IID_IConnectionPointContainer, (void**)&pIConnectionPointContainer); if (SUCCEEDED(sc) && pIConnectionPointContainer != NULL) { sc = pIConnectionPointContainer->FindConnectionPoint( IID_ISomeConnectionSink, &m_pIConnectionPoint); pIConnectionPointContainer->Release(); if (SUCCEEDED(sc) && pIConnectionPoint != NULL); { IUnknown* pSinkAsUnknown = (IUnknown*)* m_pIOurSink; sc = m_pIConnectionPoint->Advise(pSinkAsUnknown, &m_dwConnectionCookie); if (SUCCEEDED(sc)) return TRUE; } } } return FALSE;
When the client needs to clean up, it calls the opposite of Advise, which is UnAdvise. UnAdvise terminates the connection. If you were to write an Unadvise function without ATL, it would look something like:
m_pIConnectionPoint->Unadvise(m_dwConnectionCookie); m_pIConnectionPoint->Release();
ATL implements connection points in an identical manner. However, your buddies at Microsoft put everything into a nice wrapper, and since ATL COM objects are generated from an IDL file, marshalling is handled for you. ATL's Advise (which is cleverly called AtlAdvise) is implemented as follows:
HRESULT hRes = AtlAdvise(pISomeObjectWithConnectionPointContainer, pUnknownOfOurSinkObject, //gets the unknown of the sink IID_IOfSink, //id of the sink &dwConnectionCookie); //array of DWORD place holders
AtlUnadvise, which is similar to IConnectionPoint::Unadvise, works as follows:
HRESULT hRes = AtlUnadvise(pISomeObjectWithConnectionPointContainer, IID_IOfSink, &dwConnectionCookie);
Now we are ready to implement an IConnectionPointContainer in TheServer's COM object and declare an IConnectionPoint Interface in TheServer's IDL file. Open TheServer project and open the file TheServerComObject.h. As part your base class, add the following line:
public IConnectionPointImpl<CTheServerComObject, &IID_IMySinkID>,
so that your class declaration appears as follows:
class /* ATL_NO_VTABLE */ CTheServerComObject : public CComObjectRootEx<CComObjectThreadModel>, public CComCoClass<CTheServerComObject,&CLSID_TheServerComObject>, public ISupportErrorInfo, public IConnectionPointContainerImpl<CTheServerComObject>, public IConnectionPointImpl<CTheServerComObject,&IID_IMySinkID>, public IDispatchImpl<ITheServerComObject, &IID_ITheServerComObject, &LIBID_THESERVERLib> {
IConnectionPointContainerImpl implements a connection point container and manages a list of IConnectionPointImpl outgoing interfaces. Notice that we have implied that the Interface ID (IID) of the outgoing interface will be IID_IMySinkID, which is now passed to the template class IConnectionPointImpl.
The next item on our agenda is to enter a connection point for the specified interface into the connection point map, so that the connection point can be accessed. This can be accomplished by adding one line of code to the connection point macro.
BEGIN_CONNECTION_POINT_MAP(CTheServerComObject) CONNECTION_POINT_ENTRY(IID_IMySinkID) END_CONNECTION_POINT_MAP()
Connection point entries in the map are used by IConnectionPointContainerImpl. The class containing the connection point map must inherit from IConnectionPointContainerImpl
Now we are ready to declare the sink interface in our IDL file. Immediately after the declaration of ITheServerComObject, add the following code:
//sink interface [ object, //use guidgen to generate a unique id uuid(869703A1-9824-11d0-A4F8-0000B4533EC9), dual, helpstring("MySink Interface"), pointer_default(unique) ] interface IMySinkID : IDispatch { };
As noted in the comment, we used guidgen.exe, which comes with Visual C++, to generate a new ID. Guidgen.exe is very easy to use (two mouse clicks). It is located in \msdev\bin. You also need to add information to CTheServerComObject's coclass declaration at the bottom of the library definition, as follows:
[default, source] interface IMySinkID;
Take a look at the IDL shipped with the sample to see exactly where it should be placed, if necessary. Once this is done, select Rebuild | All, and the project should compile and create links, as well as perform the custom build step of registering your object and its interfaces.
Now let's add a method in our COM object class to push data into the sinks maintained in the connection point container. The method that needs to be declared in our class declaration file and implemented in our TheServerComObject.cpp follows. The comments explain exactly what is occurring when this method is called.
void CTheServerComObject::PushDataIntoSink(long lNewValue) { HRESULT hr = S_OK; Lock(); // lock ownership of a critical section // outgoing sinks we will find IMySinkID* pSink = NULL; // m_vec is of type CComDynamicUnkArray which holds // a dynamically maintained array of connection points // as IUnknowns IUnknown** pp = m_vec.begin(); while(pp < m_vec.end() && hr == S_OK) { pSink = (IMySinkID*) *pp; hr = S_FALSE; if(pSink != NULL) { // call sink function hr = pSink->SinkReceiveData(lNewValue); _ASSERTE(SUCCEEDED(hr)); pp++; } } Unlock(); // release critical section }
You should notice that a sink method called SinkReceiveData is utilized. SinkReceiveData is called from within CTheServerComObject by the sink object that is actually created in the client. Because CTheServerComObject needs to have knowledge of this function in order to compile, we need to declare it in the sink interface in the IDL file. Once it has been declared in the sink interface in the IDL file, the MIDL compiler is able to generate it in TheServer.h, which is included at the top of TheServerComObject.cpp. Therefore, CTheServerComObject's implementation will have knowledge of the SinkReceiveData method and can compile cleanly. The sink interface should now appear as:
//sink interface [ object, //use guidgen to generate a unique id uuid(869703A1-9824-11d0-A4F8-0000B4533EC9), dual, helpstring("MySink Interface"), pointer_default(unique) ] interface IMySinkID : IDispatch { HRESULT SinkReceiveData([in] long lValue); };
Finally, we need to move data from our inbound function to our outbound function. We can do this very easily by modifying the AcceptNewValue function.
Before we modify AcceptNewValue, we will add some additional logic to TheServer. We need to add an array to CComModule so that we can iterate through all our sinks when we want to push data to connected outgoing interfaces (like a document iterating through its views).
Therefore, open stdafx.h and add the following lines to the top of the file. Remember, now that you are including an MFC file you need to change the Build Settings to link to MFC. Their addition will result in more MFC support (these lines were copied directly from a standard AppWizard generated stdafx.h file).
//now add the MFC standard includes #include <afxwin.h> // MFC core and standard components #include <afxext.h> // MFC extensions #include <afxdisp.h> // MFC OLE automation classes #ifndef _AFX_NO_AFXCMN_SUPPORT #include <afxcmn.h> // MFC support for Windows Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT
In the CExeModule class declared in this file, add the following line that adds the member variable m_PtrArray:
CPtrArray m_PtrArray;
Add the line _Module.m_PtrArray.Add(this); to the Constructor so that new client objects are added to the array. (Note that in real-world situations, you would most likely want the client implementation itself to be responsible for adding and removing itself from the server's array.)
CTheServerComObject::CTheServerComObject() { m_pUnkMarshaler = NULL; m_lCurrentValue = 0; _Module.m_PtrArray.Add(this); }
After adding the method for maintaining the array, you may have guessed the requirements for the AcceptNewValue function:
The AcceptNewValue function is below:
STDMETHODIMP CTheServerComObject::AcceptNewValue ( long lNewValue, //in long FAR* lpFormerValue //out ) { if(lNewValue >= 0 && lNewValue <= 2) { CTheServerComObject* pObj = NULL; int iSize = _Module.m_PtrArray.Add(this); for(int iIndex = 0; iIndex < iSize; iIndex++) { pObj = (CTheServerComObject*)_Module.m_PtrArray.GetAt(iIndex); if(pObj != NULL) { pObj->PushDataIntoSink(lNewValue); } } //now update our members *lpFormerValue = m_lCurrentValue; m_lCurrentValue = lNewValue; return S_OK; } //return S_FALSE for value not accepted! return S_FALSE; }
Rebuild everything. Now we are ready to implement the sink in an ActiveX control client.