After "How do I make my server into an NT service?" one of the most frequently asked ATL questions for Charles is "How do I create and use Connection points in an MFC client and ATL COM-based server?" This article and sample code will show you how to set up successful callback mechanisms in your own servers.
My editor, Kate Gregory, is a kind woman, but with the busy schedules we staff writers keep, she's often forced to go around snapping a whip (not that we mind) asking for our work. Had our relationship been less human and more client/server-oriented, she would've used a technique to rapidly deploy the message "Give me your stuff . . . now!" to all of us from one entry point. This is where the subject of connection points starts.
Connection points are COM's technique to simulate "functioncallbacks." Imagine you have an ATL application acting as a server and a typical MFC application acting as a client of this server. Assume that theMFC client application wants to be informed of an event that the ATL server application generates.
You could write a function within the ATL server that accepts a pointer for an interface that the MFC client implements. Then, the server has a live interface pointer that was implemented, instantiated, and passed from the client, and the server could call any methods on this interface. Typical "callback" scenario, right? Well, unfortunately, as your faithful ATL angel, I must be the carrier of bad news. There are a few inherent problems. First, how is the ATL server supposed to know what methods the interface supports? For example, your MFC client application might implement an IAlarm interface, and my MFC client application might implement an IAlarm interface, yet we can have completely different methods under that IAlarm interface. So, in essence, the server has no way of guaranteeing that the IAlarm interface passed is the interface it expects. Littleproblem, eh? Second, what if my MFC application creates several objects that expose an IAlarm interface, and I want the ATL server to notify all of these "live" interfaces that some event has occurred? More work is now required by the ATL server to maintain the handling of additional passed instantiated interfaces. Slightly larger problem, huh? And just to rub salt in the wounds, what happens if the MFC client objects that have been exposing IAlarm are now being destroyed? Doesn't the ATL server have to remove any interface references to client-supported objects that no longer exist? Can I guess? You're beginning to see the need for a standard mechanism to handle callbacks in COM. You realize that Microsoft loves you too much to force you, the straining developer, to have to come up with a customized solution each time your application requires a callback mechanism.
Besides, I've got to write about something, so why not connection points? Oh, my-does this naming "hint" at the technique? Unfortunately, as with most current technology, the terms associated with this technique are a little confusing. Hopefully, this article will begin to clarify the concepts, terms, and implementation associated with connection points.
Our ATL server will be a "connectable object." A connectable object is one that supports "outgoing interfaces." An outgoing interface is an interface that's only described by the ATL server and is not implemented by the server. The actual implementation of the interface will be implemented by the clients who want to use the connection point capability of the server. Not only does an outgoing interface allow the server to communicate with a client, but it also resolves the same-named-interface-but-different-methods problem. This is because the ATL server uses its own IDL file to describe the interface and the expected layout (signature) of the interface methods. Eventually, this interface declaration gets compiled into the ATL server's type library, which is later extracted by the client application for understanding the interface and its method signatures, leaving the actual implementation code to the client.
Most significantly, the ATL server object will define the interface, but it won't implement the interface. This is much like having a C++ object's header file without the implementation files. Personally, I find the term "outgoing interface" confusing. In reality, it's the client that implements the interface and passes it to the server object-hence, isn't it really an "incoming" interface from the server's perspective? Clearly, nobody at Microsoft asked me what I thought, so "outgoing interface" it is! Even more peculiar, the actual name given for the client-side implementation of the outgoing interface is a "sink." Yeah, sure . . . I get it.
Next, the ATL server object (connectable object) has very special, standard interfaces used to designate what interfaces will be exposed as outgoing interfaces. Every outgoing interface is exposed via a "connection point" in the server, which is supported through the IConnectionPoint interface. The other connection point interface the server must implement is named IConnectionPointContainer. The server's IConnectionPointContainer interface is another interface with administrative-like qualities that the server exposes to clients.
Clients use the IConnectionPointContainer to enumerate what interfaces will be supported by the server-that is, what interfaces will have an IConnectionPoint interface to support them as connection points.
Let's take a quick look at how this works. Figure 1 shows the client and the server. The steps are as follows:
1.The client gets IConnectionPointContainer.
2.The client calls the method FindConnectionPoint for the desired sink interface IID, which returns IConnectionPoint.
3.The client uses the IConnectionPoint interface to pass the sink interface-that is, the actual interface implemented by the client.
4.The server calls methods on the sink.
The serverATL provides lots of support for connection points, leaving little raw code for you to implement. Here are the steps that are required:
1.Using AppWizard, create an ATL EXE project called "ATLConnectionPoint."
2.In the ClassView pane, do the following: Right-click on "ATLConnectionPoint classes" | New ATL Object... (ATL object Wizard Properties dialog box) | Simple Object | enter "AnyInterfaceForExample" in the short name edit box.
3.Important: Before hitting the OK button, select the "Attributes" tab on the ATL Object Wizard Properties dialog box. Then check the "Support Connection Points" check box (see Figure 2). You'll see that the expected IAnyInterfaceForExample interface has been created for you. In addition, another interface called "_IAnyInterfaceForExampleEvents" was automatically generated. This is your outgoing interface name. It's an IDispatch-derived interface and will be added to your coclass (the IDL description of the class that supports the interface) as well as the C++ class that supports the interface. Looking at the CAnyInterfaceForExample class that supports IAnyInterfaceForExample, you'll see that a new derivation has been added to the base list "public IConnectionPointContainerImpl<CAnyInterfaceForExample>". Also, the subsequent macro has been added to the class:
BEGIN_CONNECTION_POINT_MAP(CAnyInterfaceForExample)
END_CONNECTION_POINT_MAP()
This macro assists in ATL's embedded connection point/container interface support so that you needn't create that supporting code just as the BEGIN_COM_MAP assists in establishing ATL's built-in class factory support. At this point, you could change the name of this event interface in the IDL file, and the class references if you prefer a different name. We'll stick with the default, since I tend to agree with Microsoft's naming convention that when an interface represents events occurring within the interface it was named after, the _IAnyInterfaceForExampleEvents name starts with a hyphen, as is typical Microsoft notation for either internal interfaces or ones that aren't created via CreateInstance().
4.You can now add methods defining the outgoing interface. Any methods you add to this interface will be part of the description of the outgoing interface. Right-click on the "_IAnyInterfaceForExampleEvents" name in the ClassView pane and add a method called "SendMessage(BSTR* pbstrMessage)". Important: Had you added a method to a standard interface via the same method, you would've seen a stub function with minimal code added to the supporting C++ class. Because this is a connection point-an outgoing interface-the wizard intelligently ignores adding any stub C++ functions. Another reason I like the wizards!
5.We need to make a few changes to the default code generated for us. We want to derive the outgoing interface from IUnknown instead of IDispatch. One reason is that VB recognizes only one default IDispatch, and IAnyInterfaceForExample is our main dual interface. Another reason is that VC works with vtables, and to get VC to work with IDispatch event sinks takes much more extra work, which I won't go into detail about here. The other reason is that . . . er . . . it works. Make the following two changes in the IDL file: First, modify the interface to derive from IUnknown, as shown in Listing 1; second, modify the coclass statement to reflect the interface type, as shown in Listing 2 (see page 5).
Listing 1. The interface modified to derive from IUnknown.
[
uuid(312495C2-B244-11D2-A074-00105AA9F4D1),
helpstring("_IAnyInterfaceForExampleEvents Interface"),
// ADD THIS NEXT LINE
oleautomation
]
// CHANGE THIS CODE
//
//dispinterface_IAnyInterfaceForExampleEvents
//{
//properties:
//methods:
// TO THIS CODE
//
interface_IAnyInterfaceForExampleEvents : IUnknown
{
[id(1),
helpstring("method SendMessage")] HRESULT
SendMessage([in, out]BSTR*
pbstrMessage)
};
Listing 2. The coclass statement modified to reflect the interface type.
coclass AnyInterfaceForExample
{
[default] interface
IAnyInterfaceForExample;
;
// CHANGE THIS CODE
//
// [default, source]
dispinterface _IAnyInterfaceForExampleEvents;
//
;
// TO THIS CODE
//
[default, source] interface
_IAnyInterfaceForExampleEvents;
};
6.Now that we've defined the interface, compile the IDL file by right-clicking the *.idl file in the FileView pane and selecting Compile. This will produce the type library that will be used in the next step to create the code required to "fire"events.
7.We've now defined the outgoing interface, but we don't have the code that actually handles calling methods on a single outgoing interface. Also, we need a technique for calling the same outgoing interface method for any and all outgoing interfaces that have been sent to the server (that is, added to the server's list) for referencing. Remember that a client can create many instances of an outgoing interface and need to have each and every instance to be notified of an event from the same server.
Right-click on the class that supports the outgoing interface connection point in ClassView and select "Implement Connection Point...". A dialog box will display any outgoing connection points your application supports from the newly compiled type library. Choose the _IAnyInterfaceForExampleEvents interface, and press OK. This will generate a header file with a special class called a "proxy" class, cryptically named "CProxy_WhateverTheEventInterfaceNameIs." In our example, that would be "CProxy_IAnyInterfaceForExampleEvents." This event proxy class is used as a base derivation to the C++ class that implements the connection point, thereby allowing for method calls on the outgoing interface. Simply said, your original interface class can now say, "Hey client . . . Waaakkke up . . . I got somethin' to tell you!" This step also creates an entry in the connection point map macros found in the supporting C++ class (see Listing 3).
Listing 3. Connection Point Map.
BEGIN_CONNECTION_POINT_MAP(CAnyInterfaceForExample)
CONNECTION_POINT_ENTRY(IID__IAnyInterfaceForExampleEvents)
END_CONNECTION_POINT_MAP()
8.In the CAnyInterfaceForExample, a member function to call the events was added with the name "Fire_ WhateverTheEventInterfaceNameIs." This is the function that should be called by the server whenever it wishes to publish an event to all connected clients. In our example, we need to have some reason to fire this event. We'll add a method called DoSomething() to our IAnyInterfaceForExample interface, which, in turn, will fire the event. The event could be fired at any time in the server, but I chose to fire the event from this exposed method because . . . because . . . well, because it seemed logical. If not, please direct all negative comments to the award-winning ultimate waste-of-space http://www.house.gov/house/Starr.htm. We'll add a function called DoSomething() to IAnyInterfaceForExample, which will then fire the event. The code will look like Listing 4.
Listing 4. The DoSomething() function.
STDMETHODIMP CAnyInterfaceForExample::DoSomething()
{
BSTR bstr =
::SysAllocString(L"Just fired an event from the ATL COM
server");
Fire_SendMessage(&bstr);
return S_OK;
}
9.Don't forget to register the server after it's built by typing this at a command line:
atlconnectionpoint /regserver
That's it for the ATL COM server. Now, all that remains is to create a client that implements and instantiates the outgoing interface and then advises the server of that interface.
The clientWe'll create a client with a CFormView containing buttons (see Figure 3). "Start Server" and "Stop Server" will create and destroy an instance of the server. "Advise Server" and "Unadvise Server" will instantiate the outgoing interface and then pass it to the server or remove it from the server.
Finally, the button called "Call DoSomething()" has the client application call one of the server's functions. In turn, the server's DoSomething() function will fire an event calling a method on the outgoing interface that had been passed by the client during the advise stage. The server/client event calls would look like Figure 4.
Let's begin:
1.Create a standard MFC EXE application named "TestATLConnectionPoint," and include it as part of the ATLConnectionPoint workspace.
2.Edit the formview to include the following text buttons and command handlers: "Start Server," "Advise Server," "Call DoSomething()," "Unadvise Server," and "Stop Server."
3.Make sure your #import statement of the ATLConnectionPoint.exe type library specifies named_guids and raw_interfaces_only as part of the parameters. This will ensure the availability of macros and GUID variables that will be utilized later in the code.
4.In one *.h header file, manually add the class that will support the interface. This sounds worse than it actually is-read on . . .
We need to instantiate a COM object that supports the interface to be passed to the server. COM object! COM object?! How exactly do we do that in an MFC client? Now is the time to embrace the Visual Studio/C++ creators at Microsoft and accept the love and attention they've given our inner developer child. With VC++ 6.0, it's now possible to mix MFC and ATL together with a simple click of the wizard! That is, the wizard can add supporting ATL code (CExeModule, registration, and ATL initialization) as well as an object supporting an interface with the required IDL file. Microsoft has made it possible to have an MFC project with ATL code and vice versa.
We need to leverage C++ objects and macros defined in ATL headers and implementation files so that we can quickly create a class that supports the COM requirements. In the next step, we'll insert a simple ATL object, which will automatically add the required support code for ATL to our MFC project, then we'll turn right around and delete the object we just added as well as any references to it in the IDL file. We didn't need the object-it was just a cheap excuse to get the wizard to add the other ATL supporting code we needed. Finally, we'll manually add the class that will be the implementation of the outgoing interface.
5.In the ClassView pane, right-click on the MFC application name "CTestATLConnectionPoint." Select "New ATL Object." A dialog box will appear asking you if you want to "Add ATL support to your MFC project." Select "Yes." You'll probably see another dialog box that reports an "Error generating project." Dismiss this dialog box and ignore this error.
Important exception: I mentioned earlier that we would "insert a simple ATL object" and then "turn right around and delete the object." The previously mentioned error dialog box is, in fact, a bug in the wizard that accidentally saves us from the extra step of deleting the newly added object files and references. The error only occurs the very first time you add a simple ATL object to an MFC project. The wizard added all of the supporting ATL code we required but fails to actually add a new ATL interface object. Any subsequent attempts to add ATL objects will work-it's just this first attempt that fails. This is perfect for our needs because we have all of the ATL supporting code without having to remove the files and IDL references to an unnecessary object.
6.Go to the menu and select File | Files tab | C/C++ Header file | enter "Events.h" in the filename.
7.We need to manually create the ATL/C++ based class in the newly added blank header appearing in the Visual Studio editor. Place the lines shown in Listing 5, which define the class that implements the event (the outgoing interface according to the server) interface. The #import named_guids raw_interfaces_only is what allows us to use the IID, LIBID, and so forth. Save the file.
Listing 5. Implementing the event.
class CATLConnectionPointEvent :
public
IDispatchImpl<_IAnyInterfaceForExampleEvents,
&IID__IAnyInterfaceForExampleEvents, &LIBID_ATLCONNECTIONPOINTLib>,
public CComObjectRoot
{
public:
CATLConnectionPointEvent() {}
;
BEGIN_COM_MAP(CATLConnectionPointEvent)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(_IAnyInterfaceForExampleEvents)
END_COM_MAP()
;
;
public:
;
STDMETHODIMP CATLConnectionPointEvent::SendMessage(BSTR *
pbstrMessage)
{
CString
strTemp((BSTR)pbstrMessage);
AfxMessageBox(strTemp);
return S_OK;
;
};
;
DECLARE_NOT_AGGREGATABLE(CATLConnectionPointEvent)
// Remove the comment from the preceding line if you don't want
// your object to support aggregation.
;
public:
};
8.In the MFC view class where we're using the event functionality, place the following:
;
#include "events.h"
We're almost done. We only need to create an instance of this CATLConnectionPointEvent event object (which supports the _IAnyInterfaceForExampleEvents interface) and notify the activated server of this instantiated interface. Add the code shown in Listing 6 to OnAdviseServer().
Listing 6. Extra code for OnAdviseServer().
// Create interface and
extract IUnknown of instantiated COM object.
CComObject<CATLConnectionPointEvent>* pATLConnectionPointEvent =
new CComObject<CATLConnectionPointEvent>;
;
// Retrieve IUnknown of
newly created interface using ATL helper class.
CComPtr<IUnknown>
ATLConnectionPointEventUnknown =
pATLConnectionPointEvent;
;
// AtlAdvise
// Instead of
CreateInstance(), you can create an
// interface instance
via CComObject, an ATL helper class.
HRESULT hr =
AtlAdvise(
(*m_pIAnyInterfaceForExample),
ATLConnectionPointEventUnknown,
IID__IAnyInterfaceForExampleEvents,
&m_dwCookie);
This code requires a little explanation. Normally, a developer has no idea of the "C++ class" information that wraps and supports an interface, so interfaces are normally created and retrieved via the CoCreateInstance() or smart pointer's CreateInstance(). But remember that this class is an outgoing interface-it was the ATL server that handed the client the expected function signatures and nothing else. With an outgoing interface, the actual C++ class that supports the interface is coded from within our client MFC application, and we have the class details available. So use ATL's CComObject helper class to instantiate the C++ class, thus instantiating an object that supports the interface. The first line uses CComObject to create an "instance" of CATLConnectionPointEvent because this object houses the IAnyInterfaceForExampleEvents interface. In effect, using CComObject<CATLConnectionPointEvent> is the same as coding pIAnyInterfaceForExampleEventsPtr.CreateInstance(…).
The next line uses the ATL helper class CComPtr, which is a simple wrapper, to extract the IUnknown of the instantiated IAnyInterfaceForExampleEvents pointer.
Earlier in the sample application, we instantiated a pointer to a smart pointer member variable that wraps the interface "IAnyInterfaceForExample" named "m_pIAnyInterfaceForExample." We use this m_pIAnyInterfaceForExample smart pointer as part of the ATLAdvise call as well as later in the code when we want to call its DoSomething() method. ATLAdvise requires a pointer to the IUnknown of the object the MFC client wants to connect with (the instantiated server interface). Because m_pIAnyInterfaceForExample is a smart pointer, there's an "IUnknown*" extractor override in the smart pointer class, which saves me the work of creating CComObject/CComPtr's to retrieve the IUnknown of the server interface.
Finally, we call ATLAdvise to "notify" the server of our instantiated interface. This is the step that hides all of the work to get IConnectionPointContainer/IConnectionPoint interfaces from the server and make the appropriate method calls on that server. ATLAdvise is how COM creates a connection between the ATL server's connection point and our MFC client's sink. The member variable m_dwCookie retains a number that uniquely identifies the connection, which must be saved in order to remove the connection established through ATLAdvise. To accomplish this reference removal, one uses . . . here it comes . . . the ATLUnadvise() function.
The rest takes care of itself. The event handler class will receive notifications as the server "dishes" them out. Take a close look at the sample code called "ATLConnectionPoint," available in the Subscriber Downloads at www.pinpub.com/vcd. I guess until I define the outgoing interface and Kate implements it, I'll be able to continue buying a little more time between articles (darn those faulty ISP e-mail servers . . .).
Charles Steinhardt, an MFC/C++ consultant in New York City, provides advanced Windows system consulting, including troubleshooting, for local and international clients. CSteinhardt@omg-inc.com.