Developers Workshop to COM and ATL 3.0

 < Free Open Study > 


A standard coclass is composed of some number of interfaces that the client may acquire via calls to QueryInterface(). The set of interfaces implemented by a coclass and obtained by a client are termed inbound interfaces, in that the flow of communication comes from the client into the object. As you know, we may graphically represent each inbound interface with a distinct lollipop. In IDL, we mark the set of inbound interfaces for a coclass using the interface keyword. Thus, the following IDL definition may be represented by Figure 12-1:

Figure 12-1: A coclass with four inbound interfaces.

// Some inbound interfaces on some coclass. [uuid(9AB4976E-67E6-11D3-B929-0020781238D4)] coclass CoObject { [default] interface IOne;      interface ITwo;      interface IThree; };

While this is all well and good, we do not yet have a way for a coclass to communicate with the client. While we do have COM exception handling that allows the object to inform the client something has gone wrong, we have no way for our coclasses to report more general and object-specific information back to the client.

For example, what if we have a coclass wanting to inform the client that a worker thread has completed some background task? If we have built a GUI-based COM object, such as an ActiveX control, we might wish to tell the client when the coclass was clicked by the mouse. A financial package may have a COM object that needs to inform the client when a stock quote has been updated. How can we provide a way for a COM object to report information back to an interested client?

In reality, there are many ways to configure a two-way conversation. As we will see throughout this chapter, COM does define a standard protocol (connectable objects); however, a developer does have other options.

Polling an Object

The first (and worst) way a client can discover if something of interest has happened to a coclass is by polling the object. Here, a client obtains some inbound interface via QueryInterface() and continuously calls a method on that interface to see if some condition has been met. As an example, if an inbound interface named IMouseAction defines a method named WereYouClicked(), the client might call WereYouClicked() over and over again whenever it wishes to see if the coclass has indeed been clicked by the mouse.

The problem with this approach is that the client never really knows exactly when the coclass has been clicked, but rather asks the same question again and again, which can be a waste of round trips. Furthermore, the coclass may have been clicked some time ago (or at least since the last time it was asked), but must wait to inform anyone of the new state of affairs until WereYouClicked() is again called by the client. It is generally agreed upon that object polling is slow, inefficient, and not a great design pattern to lean on too heavily. The polling pattern is illustrated in Figure 12-2.

Figure 12-2: Polling an object is a waste of time and energy.

The Callback Mechanism

A better approach is to have the client provide a way for the object to inform it when the click event has occurred. Therefore, rather than asking the object the same question during idle clock cycles, the client simply tells the coclass "If this happens to you, tell me about it." In this case, the client implements an interface defined by the server in what is traditionally called a sink object. The client then passes a pointer to the sink interface into the coclass. The coclass holds onto this interface pointer, and calls methods on the sink when something interesting happens (e.g., "Hey! They clicked me!"). This is often called firing or raising an event.

Assume we have an outbound interface named IMouseEvents, which has a single method named IveBeenClicked(). The client will implement this interface in a sink and pass reference to the coclass. To allow the client to pass in the sink reference, the coclass might define an additional inbound interface (which we may call IIn) defining two methods named Advise() and Unadvise(). A client could call these methods when it wishes to be informed of events, and when it is no longer interested. This approach is typically called a callback scenario, as illustrated in Figure 12-3. Interfaces defined by the server and implemented by a client sink are termed outbound interfaces (sometimes known as callback interfaces).

Figure 12-3: Bidirectional communication using a callback interface.

As you can see, we have flipped the roles here. The coclass (in some respects) is acting as a client, while the client's sink is acting (in some respects) as the server. So, before we begin to examine the ins and outs of COM's connectable object standard, we will first take a look at how to get two objects communicating using a callback interface.

Developing a Callback Mechanism in C++

To illustrate the callback technique, we will yet again extend the functionality of CoHexagon. Let's provide a way for CoHexagon to inform the client when it has completed its rendering task. To begin this coding example, we first need to define the callback interface. This interface will be implemented by the client and sent to the coclass. When something happens (e.g., we have finished drawing), the coclass will call methods back on the client sink using this interface pointer. To keep things simple, our callback interface (IShapeEvents) defines a single method with no parameters. Assume that we have added this interface definition to the CoHexagon's *.idl file:

// IShapeEvents is implemented by the client but // defined in the server's type library. [uuid(893D8210-688A-11d3-B929-0020781238D4), object] interface IShapeEvents : IUnknown { HRESULT FinishedDrawing(); };

Notice how IShapeEvents has not been set up as a raw dispinterface or even a dual interface. The reason is to reduce the coding of the client's sink object. If we were to define IShapeEvents as IDispatch savvy, the sink would need to contend with fleshing out the details of GetIDsOfNames(), Invoke(), GetTypeInfo(), and GetTypeInfoCount() as well as the methods of IUnknown, which for this example would be a bit of overkill.

To ensure that type library dependent languages can implement this interface, we must include it within the scope of CoHexagon's library statement. Take note, however, that we are not specifying IShapeEvents as an interface implemented by CoHexagon! This interface is only listed in the type information to allow clients to implement it in a sink object if they so choose:

// Clients need to reference this interface to implement it, so be sure it is visible. [uuid(7005A5E0-688C-11d3-B929-0020781238D4), version(1.0)] library HexCallBackServer { importlib("stdole32.tlb"); // Need to forward declare to get into type library. interface IShapeEvents; [uuid(881552B0-688C-11d3-B929-0020781238D4)] coclass CoHexagon { [default] interface IShapeEdit; interface IDraw; }; };

With the definition of callback interface out of the way, we now must provide some way to allow the client to send the coclass a pointer to the sink implementing the IShapeEvents interface. The coclass will hold onto this interface and use it to inform the client when it has indeed finished drawing. We will follow tradition and configure the coclass to support the Advise() and Unadvise() methods, which are defined within a custom interface named IEstablishCommunications. Keep in mind that the name of this interface and its methods are completely arbitrary. They could be called anything at all.

// IEstablishCommunications is implemented by the coclass to allow // the client to send in the IShapeEvents* parameter. [uuid(CB26A7F0-688B-11d3-B929-0020781238D4), object] interface IEstablishCommunications : IUnknown { // I want to hear when you are finished drawing. HRESULT Advise([in] IShapeEvents *pCallMe); // I am tired of listening to you. HRESULT Unadvise(); };

If we were to add the IEstablishCommunications interface to CoHexagon, we would of course need to derive from this interface, add a QueryInterface() provision, and list this new interface in our coclass type information. Notice that the Advise() method takes a single [in] parameter of type IShapeEvents*. The client will send in an IShapeEvents pointer that we hold onto during our lifetime, and thus we need a place to store it. Here are the relevant adjustments to the CoHexagon coclass:

// Provide a client a way to send in an IShapeEvents interface pointer. class CoHexagon : public IDraw, public IShapeEdit, public IEstablishCommunications { public: ... // IEstablishCommunications STDMETHODIMP Advise(IShapeEvents *pCallMe); STDMETHODIMP Unadvise(); // Used to talk back to client. IShapeEvents * m_ClientsImpl; };

Note 

In this implementation of our callback, we are making the assumption that only a single client will be "listening" to our events. We will see how to configure a coclass to support multiple connections to outbound interfaces a bit later in this chapter.

Implementing Advise() is simple enough. Store the incoming interface pointer in our private IShapeEvents* member variable for later use. Unadvise() is just as easy. Set the private member variable to NULL:

// Client has sent me an interface to callback on. STDMETHODIMP CoHexagon::Advise(IShapeEvents *pCallMe) { m_ClientsImpl = pCallMe; m_ClientsImpl->AddRef(); // Add a reference to the client side sink. return S_OK; } // My cue to stop sending events to the client. STDMETHODIMP CoHexagon::Unadvise() { m_ClientsImpl->Release(); // Release hold on the client side sink. m_ClientsImpl = NULL; return S_OK; }

The final task for the coclass is to call the FinishedDrawing() method where appropriate.

// As a coclass, we decide under what conditions to call the client-side interface. STDMETHODIMP CoHexagonCallBack::Draw() { // Perform any drawing code first and call client sink when finished. ... m_ClientsImpl->FinishedDrawing(); return S_OK; }

Using a Callback from Visual Basic

Any client that wishes to hear about CoHexagon's events is responsible for implementing the correct callback interface (IShapeEvents) in a sink object. If we were to build a Visual Basic client making use of our callback, the first step is to set a reference to the server's type information. Because we made sure to include the IShapeEvents interface within the scope of our library definition, this will be seen from the VB Object Browser.

Now that the VB workspace can reference IShapeEvents, we need a class to implement it. The Visual Basic language provides the Implements keyword for classes wishing to support COM interfaces. To keep things simple, we will let the form object implement IShapeEvents, although you most certainly could create a new Visual Basic class module to do the same.

When you wish to implement a sink object in Visual Basic, you simply list all interfaces within the [General][Declarations] scope using the Implements keyword. Once this has been done, VB allows you to fill in the details of each method from the code window. Simply select the interface from the right-hand drop-down list box, and each method from the left drop-down list box. Once you select each method, VB automatically generates stub code. Figure 12-4 illustrates these points:

Figure 12-4: Implementing an interface in Visual Basic.

When the coclass detects it has completed its drawing cycle, it will call the client-side implementation of IShapeEvents::FinishedDrawing(). The client may then respond as it sees fit. Beyond implementing the outbound interface, all that is left to do is to inform the object we wish to be advised of the outgoing events via the IEstablishCommunications interface. Here is the complete code listing for the VB callback client:

' [General][Declarations] ' The client implements the callback interface. ' Implements IShapeEvents Private i As IEstablishCommunications Private o As New CoHexagon ' This will eventually trigger the event. ' Private Sub btnDraw_Click() o.Draw End Sub ' The client creates the object and accesses IEstablishCommunication to ' call Advise(). The VB "Me" keyword is much like the "this" pointer. ' In this context, "Me" is the Form object implementing IShapeEvents. ' Private Sub Form_Load() Set i = o i.Advise Me     ' The form implements IShapeEvents. End Sub ' The coclass will call this method using the callback. ' Private Sub IShapeEvents_FinishedDrawing() MsgBox "Drawing is complete!", , "Event from CoHexagon" ' If you still are interested in listening, you may disconnect in Form_Unload() i.Unadvise End Sub

Using a Callback from C++

Of course we can use our callback from a C++ client as well, using a bit more elbow grease. First, we also need to define a sink to implement the IShapeEvents interface. Assume we have such a class named CoSink, which will be available throughout the application as a variable in the global namespace. As CoSink is a global object, we can return fixed values from the implementation of AddRef() and Release(), as this object's lifetime is not based on external connections but is used throughout the application. The implementation of QueryInterface() will return a valid pointer to both IUnknown and IShapeEvents. The details of the sink's implementation of FinishedDrawing() are as you would expect by now:

// This class implements the outbound interface the coclass will be calling. class CCoSink : public IShapeEvents { public: ... // The coclass will call this method when it is finished drawing. STDMETHODIMP FinishedDrawing() { MessageBox(NULL, "Drawing is complete!", "Event from CoHexagon", MB_OK | MB_SETFOREGROUND); return S_OK; } };

With a sink in place, we may create the CoHexagon, and set an advisory relationship using IEstablishCommunications:

// A C++ client establishing communications with a coclass. CCoSink g_sink; // Will receive the events from the coclass. int main(int argc, char* argv[]) { ... // Fetch IEstablishCommunications. IEstablishCommunications* pEC; CoCreateInstance(CLSID_CoHexagon, NULL, CLSCTX_SERVER, IID_IEstablishCommunications, (void**)&pEC); // Set up the advise. pEC->Advise((IShapeEvents*)&g_sink); // Trigger the event. IDraw* pDraw; pEC->QueryInterface(IID_IDraw, (void**)&pDraw); pDraw->Draw(); // Disconnect from the event. pEC->Unadvise(); ... }

Lab 12-1: Building a Callback

In this lab you will extend the functionality of your raw C++ CoCar coclass to support the ability to send out events through a callback interface. As well, this lab will allow you to build a VB and C++ sink object, which will allow your clients to respond to what the CoCar has to say.

 On the CD   The solution for this lab can be found on your CD-ROM under:Labs\Chapter 12\CarCallbackLabs\Chapter 12\CarCallback\CPP ClientLabs\Chapter 12\CarCallback\VB Client

Step One: Generate the IDL

To keep focused on the callback logic, this lab will extend the CoCar project you first created in Chapter 4, given that this lab already had you create the component housing, class factory, and initial IDL code. If you did not create that project, load up the CD solution and begin there.

Open your existing project (or make a copy) and define a callback interface named IEngineEvents, which will allow a client to hear when the automobile is about to explode (e.g., the current speed is 10 miles below the max) or has exploded (e.g., the current speed is at the max). Add the following interface to your existing IDL file:

// IEngineEvents is implemented by the client // but defined by the server. [uuid(893D8210-688A-11d3-B929-0020781238D4), object] interface IEngineEvents : IUnknown { HRESULT AboutToExplode(); HRESULT Exploded(); };

Again, for ease of use, IEngineEvents is a custom IUnknown-derived interface rather than a dual. Next, we need to create an additional inbound interface, which the client may use to establish and terminate a connection to the engine events. Define a custom interface named IEstablishCommunications, which defines the Advise() and Unadvise() methods:

// IEstablishCommunications // Used to allow a client to send us a valid sink. [uuid(CB26A7F0-688B-11d3-B929-0020781238D4), object] interface IEstablishCommunications : IUnknown { HRESULT Advise([in]IEngineEvents *pCallMe); HRESULT Unadvise(); };

To finish up our IDL adjustments, we need to specify IEstablishCommunications as an additional inbound interface supported by CoCar, as well as forwardly declare IEngineEvents in our library statement (remember that a VB client has to see this interface in the type library to build the sink):

// Wrapping up the IDL. [uuid(7005A5E0-688C-11d3-B929-0020781238D4), version(1.0), helpstring("CarCallBack server")] library CarCallBackServer { importlib("stdole32.tlb"); // Need to forward declare to get into typelib. // Not implemented by coclass!!!! interface IEngineEvents; [uuid(881552B0-688C-11d3-B929-0020781238D4)] coclass CoCarCallBack { [default] interface ICreateCar; interface IEstablishCommunications; interface IStats; interface IEngine; }; };

Step Two: Configure the CoCar

Now we need to update our CoCar to send out the events under the correct circumstances. First, add IEstablishCommunications to your class's inheritance chain and update your implementation of QueryInterface() to return an IEstablishCommunications pointer.

Now that we have support for this new inbound interface we need to code the Advise() and Unadvise() methods. To begin, add a private member variable of type IEngineEvents* (named m_ClientsImpl) to your private sector, and set it to NULL in the constructor. Your Advise() method will assign this member to the client-supplied sink object. Unadvise() will disconnect the client from the events services:

// IEstablishCommunications STDMETHODIMP CoCarCallBack::Advise(IEngineEvents *pCallMe) { // Store the client sink for future use. m_ClientsImpl = pCallMe; m_ClientsImpl->AddRef(); return S_OK; } STDMETHODIMP CoCarCallBack::Unadvise() { // Release reference and set to null. m_ClientsImpl->Release(); m_ClientsImpl = NULL; return S_OK; }

The final adjustment to make to your CoCar logic is to fire out each event when you see fit. For this lab, let's say that we fire the AboutToExplode() event when our car's current speed is 10 miles below the maximum speed. We will fire Exploded() when the car's current speed equals (or surpasses) the maximum speed. With this in mind, update your current implementation of SpeedUp(). Notice that we are only sending this event if we have a non-NULL IEngineEvents pointer:

// Only send events using a non-null pointer and if // speed logic is just so. STDMETHODIMP CoCarCallBack::SpeedUp() { m_currSpeed += 10; if((m_maxSpeed - m_currSpeed) == 10 && (m_ClientsImpl != NULL)) { // Fire the about to explode event! m_ClientsImpl->AboutToExplode(); } if((m_currSpeed >= m_maxSpeed) && (m_ClientsImpl != NULL)) { // Fire the exploded event! m_ClientsImpl->Exploded(); } return S_OK; }

With this, we have now equipped CoCar to send out two custom events using an outbound interface. The problem is, nobody is currently listening! To remedy this situation, the final steps in this lab will build up some clients and their respective sink objects.

Step Three: A Visual Basic Client

Building a sink in VB is extremely simple. Begin by creating a new Standard EXE workspace and set a reference to your updated type information. Open the VB Object Browser and locate the IEngineEvents interface. This is the interface you must now implement in a Visual Basic class module. Also note that you can see the additional inbound interface IEstablishCommunications:

Figure 12-5: The sink is responsible for implementing outbound interfaces.

Build a simple GUI, which allows the end user to speed up the automobile. We will hardcode a pet name and maximum speed in the form's Load event, as well as set up the advisory connection. Now, using the Implements keyword, build a VB sink that responds to each event sent by the coclass. In your Form object's Unload event, terminate the connection. Here is the complete VB client code:

' [General][Declarations] ' Option Explicit ' This client implements the callback interface Implements IEngineEvents Private i As IEstablishCommunications Private o As New CoCarCallBack ' Increase the speed. ' This will eventually cause each event to be sent back to your sink. Private Sub btnBurnRubber_Click() Dim e As IEngine Set e = o e.SpeedUp Label1.Caption = "Curr Speed: " & e.GetCurSpeed End Sub ' The VB Sink! Private Sub IEngineEvents_AboutToExplode() MsgBox "Prepare to meet thy doom..." End Sub Private Sub IEngineEvents_Exploded() MsgBox "Your car has exploded!" End Sub ' Establish communications with the event set. Private Sub Form_Load() o.SetMaxSpeed 80 o.SetPetName "Brenner" Set i = o i.Advise Me End Sub Private Sub Form_Unload(Cancel As Integer) i.Unadvise End Sub

If you now run your application, you will see each event captured in your client-side implementation.

In this step of the lab, we implemented IEngineEvents directly into the Form object. However, if you would rather have an independent VB class module responsible for implementing to the outbound interface, you can insert a new VB class module (Project | Add Class Module) and use the Implements keyword as before (use the VB Properties Window to set the name of this class).

Using this new VB class (which is included on your CD-ROM and is named CVBSink.cls) you may set up your advisory connection and allow the Form object to be a bit more independent. If you are up to the task, try to retrofit your current VB client to make use of CVBSink, rather than implementing IEngineEvents directly in the Form object. Or, forget all about it and start on the C++ client!

Step Four: A C++ Client

Create a new Win32 Console Application, and copy the necessary MIDL-generated files from your CoCar server (*_i.c and *.h). Insert a new C++ class to build your sink, and derive from IEngineEvents. Recall that this interface derives from IUnknown, which means your sink must implement a total of five methods. AddRef() and Release() can simply return some hard-coded number, as our sink is a global object whose lifetime is not dependent on outstanding references. QueryInterface() will return pointers to IUnknown and IEngineEvents. Beyond this COM goo, all we need to do is implement each method of the IEngineEvents interface:

// Our C++ sink object. class CCoSink : public IEngineEvents { public: CCoSink(); virtual ~CCoSink(); // IUnknown as always... // IEngineEvents methods. STDMETHODIMP AboutToExplode() { MessageBox(NULL, "Prepare to meet thy doom...", "Event from coclass...", MB_OK | MB_SETFOREGROUND); return S_OK; } STDMETHODIMP Exploded() { MessageBox(NULL, "Your car is dead...", "Event from coclass...", MB_OK | MB_SETFOREGROUND); return S_OK; } };

Your main() function will need to create a CoCar and establish communications using the global sink object:

// Create a global sink and go to town. CCoSink g_sink; void main(int argc, char* argv[]) { CoInitialize(NULL); HRESULT hr; IEstablishCommunications* pEC; hr = CoCreateInstance(CLSID_CoCarCallBack, NULL, CLSCTX_SERVER, IID_IEstablishCommunications, (void**)&pEC); // Set up the advise... pEC->Advise((IEngineEvents*)&g_sink); // Create the car. ICreateCar* pCC; pEC->QueryInterface(IID_ICreateCar, (void**)&pCC); pCC->SetMaxSpeed(50); // Grab the engine. IEngine* pE; pEC->QueryInterface(IID_IEngine, (void**)&pE); // Speed things up to get each event. for(int i = 0; i < 5; i++) pE->SpeedUp(); pEC->Unadvise(); pEC->Release(); pE->Release(); pCC->Release(); CoUninitialize(); }

There you have it! Two clients making use of our car's callback functionality. What could be better? Next up, we will investigate COM's connectable object architecture.


 < Free Open Study > 

Категории