Developers Workshop to COM and ATL 3.0
| < Free Open Study > |
|
Easing a Client's Burden with IProvideClassInfo(2)
If you were keeping track of the interaction between a client, sink, and connection point, you should be a bit alarmed regarding the number of round trips to establish a connection. Think about this: The client first calls CoCreateInstance() to obtain IID_IConnection- PointContainer (#1). From this interface, the client obtains a connection point interface (#2). The client then calls Advise(), passing in the IUnknown of the sink implementing the outbound interface (#3). Then, the source object queries the client for the correct IID of the outbound interface (#4)! Four round trips later, we have established a two-way conversation. This is excessive, but keep in mind that COM's connection point architecture was deliberately designed to be extendable and as generic as possible. To help ease the client's burden, many connectable objects elect to support two additional standard interfaces: IProvideClassInfo and IProvideClassInfo2.
Using the methods of these interfaces, a client is able to receive a pointer to an ITypeInfo interface, and can discover the set of, and type of, interfaces the object supports. As well, a client is able to discover the IID [default, source] interface in one call. While some interactions will be unavoidable, IProvideClassInfo(2) can help some object consumers get the ball rolling with less fuss and bother. Each interface is defined in <ocidl.idl> as so:
// Allow a client to get the type information. [object, uuid(B196B283-BAB4-101A-B69C-00AA00341D07), pointer_default(unique)] interface IProvideClassInfo : IUnknown { HRESULT GetClassInfo([out] ITypeInfo ** ppTI); }; // Allow a client to get the IID of the [default, source] interface. [object, uuid(A6BC3AC0-DBAA-11CE-9DE3-00AA004BB851), pointer_default(unique)] interface IProvideClassInfo2 : IProvideClassInfo { typedef enum tagGUIDKIND { GUIDKIND_DEFAULT_SOURCE_DISP_IID = 1 } GUIDKIND; HRESULT GetGUID([in] DWORD dwGuidKind, [out] GUID * pGUID); };
GetGUID() is configured to be extendable in that the first parameter specifies the type of GUID the client wants to obtain. Currently, the GUIDKIND structure only knows how to return the default source dispinterface of the coclass. In other words, IProvideClassInfo2 can only help you out if your [default, source] has been set up as a dispinterface, which as you recall, ATL does automatically.
To support these two interfaces on your coclass, ATL provides the IProvideClass- InfoImpl<> and IProvideClassInfoImpl2<> templates. Each is defined in <atlcom.h> if you want to check it out yourself; just keep in mind that IProvideClassInfoImpl2<> requires the CLSID of your coclass and the IID of the default source dispinterface.
As one interface derives from another, you will add the most derived template to your coclass's inheritance chain and add the appropriate COM_MAP entries. Typically you will want to grab the functionality of both interfaces, thus you may configure your ATL coclass as such:
// IProvideClassInfo(2) helps various COM language mappings obtain relevant // information in a more timely fashion. class ATL_NO_VTABLE CCoSomeObject: public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoSomeObject, &CLSID_CoSomeObject>, public IConnectionPointContainerImpl<CCoSomeObject>, public IDispatchImpl<ISomeObject, &IID_ISomeObject, &LIBID_ATLCONNOBJSERVERLib>, public CProxy_ISomeObjectEvents<CCoSomeObject>, public CProxy_IAnotherOutbound<CCoSomeObject>, public IProvideClassInfo2Impl<&CLSID_CoSomeObject, &DIID__ISomeObjectEvents> { ... BEGIN_COM_MAP(CCoSomeObject) COM_INTERFACE_ENTRY(ISomeObject) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IConnectionPointContainer) COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer) COM_INTERFACE_ENTRY(IProvideClassInfo) COM_INTERFACE_ENTRY(IProvideClassInfo2) END_COM_MAP() ... };
Note | If your coclass needs to send events to a web-based client, it must support IProvideClassInfo2. |
Lab 12-2: Building a Connectable Object with ATL
This lab will give you the chance to build a connectable object using the ATL framework and the numerous CASE tools that are used in the process. For a change of pace, this lab will have nothing whatsoever to do with shapes or automobiles. As well, this lab will illustrate one possible technique for creating a C++ client sink using ATL.
On the CD The solution for this lab can be found on your CD-ROM under:Labs\Chapter 12\ATLEventsLabs\Chapter 12\ATLEvents\VB ClientLabs\Chapter 12\ATLEvents\CPPEventClient
Step One: Create the Initial Coclass
In this lab, you will be creating a small life simulation, modeling the growth of a person from childhood to old age. Begin by inserting a Simple Object into a new ATL EXE server named CoChild. Be sure you select Support Connection Points from the Attributes tab and a dual interface named IChild. In the default interface, add two properties and a single method, as so:
// AskChildQuestion() will be our cue to fire out a custom event. interface IChild : IDispatch { [propget, id(1)] HRESULT Name([out, retval] BSTR *pVal); [propput, id(1)] HRESULT Name([in] BSTR newVal); [propget, id(2)] HRESULT Age([out, retval] short *pVal); [propput, id(2)] HRESULT Age([in] short newVal); [id(3)] HRESULT AskChildQuestion(); };
By way of a friendly reminder, if you configure your [default] interface as a custom vTable interface, be sure to add the [oleautomation] attribute to the IDL description. If you do not, the IChild interface cannot be marshaled between process boundaries. Recall that the [oleautomation] attribute forces registration of your interfaces and sets the ProxyStub- Clsid32 subkey to point to the universal marshaler.
Next, implement the Age and Name property for your CoChild class (we will get to AskChildQuestion() in just a bit).
Step Two: Add a Custom Event
Because you have specified support for connection points, the ATL Object Wizard has included a [default, source] interface in your IDL file. We will be adding a single event named ChildSays(), which will fire out a single BSTR parameter:
// Note that we specify the parameter as [in]! dispinterface _IChildEvents { properties: methods: [id(1), helpstring("method ChildSays")] HRESULT ChildSays([in] BSTR msg); };
Before you run the Implement Connection Point Wizard, be sure you recompile your IDL file in order to refresh your type information. From the wizard, select your outbound interface. This tool will generate a connection point proxy class named CProxy_IChildEvents. If you examine this class, you will see Fire_ChildSays() will walk the list of currently connected clients, sending out the supplied BSTR to each.
If you examine your CoChild coclass, you will see your connection map has been updated (don't forget to check for the bug!), and the proxy class has been added to your inheritance chain. All we need to do now is fire our custom event.
Step Three: Fire the Custom Event
We will fire our custom event whenever a client calls the AskChildQuestion() method (as you may know, children always have something to say). Add a private CComBSTR to your coclass to represent the message and a helper function that will generate a random message based on the current age of the child. GetAMessage() will test the age member variable and send out a baby, toddler, teenager, or adult message:
// This helper function will grab a message based on the age of the child. void CCoChild::GetAMessage() { // Here are the possible messages. CComBSTR babyMsg[3] = {L"MommaDatta", L"Why sky is blue?", L"Poppa is yucks"}; CComBSTR youngMsg[3] = {L"I love my parents", L"Can I have some candy please?", L"Read me a bedtime story"}; CComBSTR teenMsg[3] = {L"You don't know what it is like to be ME!", L"I want an eyebrow ring", L"My world is falling APART"}; CComBSTR adultMsg[3] = {L"I love COM", L"I want more COM", L"Life is COM. I love VARIANTS."}; // Must include <time.h> for these methods. srand( (unsigned)clock() ); int item = rand() % 3; // How old is the child? if (m_Age >= 1 && m_Age <= 3) m_bstrMessage = babyMsg[item]; else if (m_Age >= 4 && m_Age <= 12) m_bstrMessage = youngMsg[item]; else if (m_Age >= 13 && m_Age <= 21) m_bstrMessage = teenMsg[item]; else if (m_Age >= 22) m_bstrMessage = adultMsg[item]; }
Now, we can fire our custom message to any interested client:
// When a client calls AskChildQuestion(), fill m_bstrMessage and fire our // custom event. STDMETHODIMP CCoChild::AskChildQuestion() { // Go get a new message. GetAMessage(); // Fire the message. Fire_ChildSays(m_bstrMessage); return S_OK; }
Compile your server, and move onto the VB test client.
Step Four: A VB Test Client
Construct a VB EXE client that will allow the user to create a CoChild object, and set the Name and Age of this kid. Recall that the age of this child will be used to determine which array of BSTRs (baby, toddler, teenager, or adult) will be used to obtain a random message. Declare the CoChild in your [General][Declarations] section using the WithEvents keyword.
' Recall we cannot use the New keyword at the same time we create an object ' WithEvents. ' Dim WithEvents kid As CoChild
Next, add a single VB Timer object to your form (note the stopwatch icon in Figure 12-10). Select this object, and using the VB Properties Window, configure your timer to have an interval of 500. Figure 12-10 shows the GUI under construction (assume the Make Kid button sets the CoChild to a new instance):
In the Timer event of your Timer object (which will now be called every ½ second), ask your child a question, and advance the Age property by 1, to ensure your child comes of age:
' The VB Timer object will call this function automatically. How often ' depends on the value of the Interval property. ' Private Sub Timer1_Timer() ' Ask kid a question. kid.AskChildQuestion ' Raise the kid's age. kid.Age = kid.Age + 1 txtAge = kid.Age End Sub
Finally, in your CoChild event hander, take the incoming message and place it into the ListBox object:
' When the timer object asks your child a question, it will fire off the answer ' to this event sink. ' Private Sub kid_ChildSays(ByVal msg As String) Dim s As String s = kid.Name & " is " & kid.Age & " and says: " & msg ' Add new string to top of list box. List1.AddItem s, 0 End Sub
Go ahead and run your project, and watch your child grow up:
Next up, let's see how to achieve the same effect using C++.
Step Five: A C++/ATL Client
Setting up a VB sink is very simple. If we were to create a connection to the CoChild's [default, source] interface in raw C++, we would have quite a task ahead of us. Thankfully, ATL does provide a number of helper items (templates, sink maps, and global functions) to get a C++ event client up and running. Begin by creating a brand new Win32 Console Application (a Simple Project) named CPPEventClient.
First of all, our C++ client will need to make use of some ATL-specific code, and we must explicitly include <atlbase.h> and <atlcom.h>. Furthermore, as <atlcom.h> is looking for an instance of CComModule named _Module, we need to create a dummy object. Edit your existing cppeventclient.cpp file as so:
// We need to "fool" ATL into thinking we are creating a COM server. // #include <iostream.h> // For cout. #include "atlevents.h" // MIDL-generated file. #include "atlevents_i.c" // MIDL-generated file. #include <windows.h> #include <atlbase.h> CComModule _Module; #include <atlcom.h> int main(int argc, char* argv[]) { CoInitialize(NULL); CoUninitialize(); return 0; }
This will configure your C++ client to make use of the ATL event helpers.
As you recall, a client that wishes to be informed of events sent from a coclass needs to create a "sink" object which implements the [source] interface in question. CoChild defined the following dispinterface:
// The [default, source] interface. dispinterface _IChildEvents { properties: methods: [id(1), helpstring("method ChildSays")] HRESULT ChildSays([in] BSTR msg); };
Again, if we were to implement this interface in straight C++, we would need to contend with the implementation of the four methods of IDispatch (which is no fun at all). ATL provides a handy template that is used to implement the IDispatch interface for event sinks, named IDispEventSimpleImpl<>. Define the following class right before your main() loop (everything is inlined, so we don't need a separate header and *.cpp file for this sink class):
// Info for the events handler. _ATL_FUNC_INFO OnLookAtKidInfo = {CC_STDCALL, VT_EMPTY, 1, {VT_BSTR}}; // The class implementing the sink. class CKidSink : public IDispEventSimpleImpl<1, CKidSink, &DIID__IChildEvents> { public: CKidSink(IChild* pKid) { m_pKid = pKid; m_pKid->AddRef(); // Attach to [default, source] DispEventAdvise((IUnknown*)m_pKid); } virtual ~CKidSink() { // Detach from source. m_pKid->Release(); DispEventUnadvise((IUnknown*)m_pKid); } // Call this method when the kid talks. void __stdcall OnLookAtKid(BSTR msg) { USES_CONVERSION; cout << "Kid says: " << W2A(msg) << endl; SysFreeString(msg); } // IDispEventSimpleImpl<> needs a sink map. BEGIN_SINK_MAP(CKidSink) SINK_ENTRY_INFO(1, DIID__IChildEvents, 1, OnLookAtKid, &OnLookAtKidInfo) END_SINK_MAP() private: IChild* m_pKid; };
First off, we have created a sink object (CKidSink) deriving from IDispEventSimpleImpl<>. The first parameter is a simple numeric ID for the source object, and parameter two is the name of this sink class. The final parameter is the IID of the dispinterface defining the events.
In order for the IDispEventSimpleImpl<> template to do its magic, we need to provide an ATL sink map (using the BEGIN_SINK_MAP and END_SINK_MAP macros). This sink map is populated with the SINK_ENTRY_INFO macro (among others), which takes the following arguments:
// Arguments for the sink map. BEGIN_SINK_MAP(CKidSink) SINK_ENTRY_INFO( 1, // ID of source. DIID__IChildEvents, // IID of dispinterface. 1, // DISPID in dispinterface. OnLookAtKid, // Method to call for this event. &OnLookAtKidInfo) // Info (parameters) from the event. END_SINK_MAP()
The final parameter passed into the SINK_ENTRY_INFO macro is an _ATL_FUNC_INFO structure, which specifies the parameters and return value for the given method of the [source] interface. As AskChildQuestion() will send us a BSTR, we must specify the _ATL_FUNC_INFO as so:
// Info for the events handler. _ATL_FUNC_INFO OnLookAtKidInfo = {CC_STDCALL, VT_EMPTY, 1, {VT_BSTR}};
Notice that the sink object maintains a private IChild* member variable. The main() loop will need to send us the IChild interface so we can call the ATL helper functions DispEventAdvise() and DispEventUnadvise(). We do so in the overloaded constructor and destructor of the sink:
// The sink object needs to Advise and Unadvise to the source: CKidSink(IChild* pKid) { m_pKid = pKid; m_pKid->AddRef(); // Attach to [default, source] DispEventAdvise((IUnknown*)m_pKid); } virtual ~CKidSink() { // Detach from source. m_pKid->Release(); DispEventUnadvise((IUnknown*)m_pKid); }
Finally, we need to implement the event handler itself. We will take the BSTR sent by CoChild and pump it to the IO stream:
// Call this method when the kid talks. void __stdcall OnLookAtKid(BSTR msg) { USES_CONVERSION; cout << "Kid says: " << W2A(msg) << endl; SysFreeString(msg); }
This completes the sink. The last step is to configure main to create the CoChild and set up the sink object:
// Now we can get down to biz. int main(int argc, char* argv[]) { CoInitialize(NULL); // Make the kid. IChild* pChild = NULL; CoCreateInstance(CLSID_CoChild, NULL, CLSCTX_SERVER, IID_IChild, (void**)&pChild); // Make the sink. CKidSink* kSink = new CKidSink(pChild); // Play a game of 25 questions. short age = 0; for(int i = 0; i < 25; i++) { pChild->get_Age(&age); cout << "Age is: " << age << " "; pChild->AskChildQuestion(); // Watch the child grow. pChild->put_Age(age + 1); // Wait just a bit to allow a new random message to be generated. Sleep(500); } // Clean up. delete kSink; if(pChild) pChild->Release(); CoUninitialize(); return 0; }
I would bet that this code is old hat by now. The only new logic to comment on is the creation of the sink object. When you run this new client you will see the following (possible) output:
That wraps up our examination of how to configure two objects to talk with each other. The callback mechanism does entail fewer overheads (and less complexity) than the standard COM connection point protocol; however, custom callback interfaces are unusable among late bound clients (such as Internet Explorer). While a good deal of work needs to be done when building a connectable object by hand (raw C++), ATL makes it as simple as accessing the correct CASE tool. As we have also seen, ATL does provide some help establishing the client-side sink.
| < Free Open Study > |
|