Developers Workshop to COM and ATL 3.0
| < Free Open Study > |
|
Now that you have discovered the dispinterface, you may wonder what the correct choice is for building a coclass. If you create a 100% automation object, you are ensured all COM language—including scripting clients—can make use of your object's functionality. As you can see, keeping a dispinterface up to date can be a bit of a bother. Writing C++ late binding code can also be a bit of a (huge) bother. On the other hand, you know and love vTable custom interfaces, and enjoy building coclasses that express themselves using QueryInterface(). However, doing so will keep scripting clients from using your COM objects. So what is the COM developer to do? Create objects that support dual interfaces.
Objects that support dual interfaces provide an IDispatch implementation as well as the set of custom vTable interfaces supported by the object. In this way, smart clients can simply access your custom interfaces (IDraw, IShapeEdit, ICreateCar, and so forth), while the dumb clients are still able to make use of your coclass using IDispatch. In this way, everyone is happy.
Building a dual interface object becomes even more easy to do if you make use of the COM library. For the most part, you write the IDL to describe the dual interface and use the associated COM library calls to fill in the details of GetTypeInfoCount(), GetTypeInfo(), GetIDsOfNames(), and Invoke(). This leaves writing your vTable methods as usual as your only task.
However, dual interfaces do have one very important restriction. If you build a dual interface object, every single parameter of every single method must be variant compliant. This includes the method parameters of your custom vTable interfaces as well. You are not allowed to "get sneaky" and attempt to write variant compliant dispinterface methods and non-compliant custom interface methods. Everyone must play by the same rules.
So then, in the world of COM today, we have three possible configurations between a coclass and the interfaces it supports, as shown in Figure 10-5. CoSquiggle is a pure automation object, which by definition supplies all functionality using IDispatch. CoFunnel supports a pure vTable interface, as it does not support IDispatch (and therefore is unusable from a scripting client). Finally, CoOval is a dual interface object, providing both early and late binding using IDispatch as well a custom interface.
Developing Dual Interfaces in IDL and Raw C++
As most things in COM, writing a dual interface begins with the IDL code. Defining a dual interface in IDL looks like the following:
// A dual interface [uuid(BDFD41A0-580A-11d3-B926-0020781238D4), object, dual] interface ISquiggle : IDispatch { [id(1)] HRESULT DrawASquiggle(); [id(2)] HRESULT FlipASquiggle(); [id(3)] HRESULT EraseASquiggle(); };
Like vTable interfaces, dual interfaces are themselves marked with the [uuid] and [object] attributes. Unlike a vTable interface, you must specify the [dual] attribute and derive your custom interface (in this case, ISquiggle) directly from IDispatch. Furthermore, each member in the dispinterface must be marked with a unique DISPID using the [id] attribute. This ensures that a coclass implementing this particular interface must contend with ten methods: three from IUnknown, four from IDispatch, and three methods of the custom ISquiggle.
Adding this interface to the library statement is exactly the same as working with a vTable object:
// A library statement for a server containing an object // supporting a dual interface. [uuid(BDFD41A1-580A-11d3-B926-0020781238D4), version(1.0)] library DualObject { importlib("stdole32.tlb"); [uuid(BDFD41A2-580A-11d3-B926-0020781238D4)] coclass CoDualSquiggle { [default] interface ISquiggle; }; };
When you send this file into the MIDL compiler, you will receive the generated files you already know and love. Therefore, early bound clients can still include the *_i.c and *.h generated files (C and C++ clients) as well as your server's type library information (VB, Java, and C++ clients). However, as your object is now supporting the methods of IDispatch, you are also able to provide your functionality to the dumb (scripting) clients in the world.
Building a Dual Interface Coclass by Hand
To support a dual interface on a coclass, you need to ensure that external COM clients are able to query for the set of custom interfaces (ISquiggle) as well as the IUnknown and IDispatch interfaces. Furthermore, rather than deriving directly from IDispatch, the coclass will derive from the dual interface. If CoSquiggle were to be retrofitted to implement the dual ISquiggle interface, we would begin by modifying the class definition as so:
// An object supporting a dual interface inherits both IDispatch // and the custom vTable interface. class CoDualSquiggle : public ISquiggle { public: CoDualSquiggle(); virtual ~CoDualSquiggle(); // IUnknown prototypes... // IDispatch prototypes... // These must now be public (and use the correct COM macros) STDMETHODIMP DrawASquiggle(); STDMETHODIMP FlipASquiggle(); STDMETHODIMP EraseASquiggle(); private: ULONG m_ref; };
Notice how the methods that were previously private helper functions in the raw dispinterface CoSquiggle (DrawASquiggle() and friends) have now been moved to the public sector. Think this one through. If a client queries for IID_ISquiggle, the client has access to each of the custom methods provided by ISquiggle, and the vTable of the coclass will direct the flow of logic to the correct function. On the other hand, if a client queries for IID_IDispatch, the coclass will assign a DISPID within GetIDsOfNames() and simply call these same functions from within the Invoke() implementation!
In effect, these same three methods will be called by both early and late bound clients—using two different mechanisms. We are still not finished retrofitting CoSquiggle to support this new dual interface. We now must make sure that clients can obtain a pointer to ISquiggle, and thus must update QueryInterface() as so:
// Be sure your QI returns vPtrs to both IDispatch and ISquiggle! STDMETHODIMP CoDualSquiggle::QueryInterface(REFIID riid, void** ppv) { // We do not support any custom interfaces here. // All object functionality is given via IDispatch if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_IDispatch) // <— This is for scripting clients! *ppv = (IDispatch*)this; else if(riid == IID_ISquiggle) // <— For early clients! *ppv = (ISquiggle*)this; else { *ppv = NULL; return E_NOINTERFACE; } ((IUnknown*)(*ppv))->AddRef(); return S_OK; }
The implementation of DrawASquiggle(), FlipASquiggle(), and EraseASquiggle() will also need to be updated just a bit to use the correct COM macros. For example, here is DrawASquiggle(); the other methods would be updated in the same manner:
// Need to add the correct COM macros. STDMETHODIMP CoDualSquiggle::DrawASquiggle() { // Drawing a squiggle... MessageBox(NULL, "Drawing a squiggle", "CoDualSquiggle", MB_OK | MB_SETFOREGROUND); return S_OK; }
The previous implementation of IDispatch will not need to change one bit. GetIDsOf- Names() will still assign the correct DISPID. Invoke() will still call the correct function based on the DISPID. That's it! Now that you have seen how to build a dual interface object, the next lab will allow you to try it out for yourself and retrofit your previous CoSquiggle with the necessary changes.
As an added bonus, this lab will illustrate how to use MFC's COleDispatchDriver class to build an MFC application using a late bound connection. After that, we will look at using the type information from within the coclass, to provide a streamlined implementation of IDispatch.
Lab 10-2: Supporting Dual Interfaces
A dual interface is a hybrid of the dispinterface and a custom vTable interface. If you worked through the previous lab, you have seen that using a dispinterface as a C++ client can be a pain. However, if your object does not support IDispatch, you can't be web enabled. In this lab, you will extend your previous lab to support a single dual interface, thus allowing smart clients to acquire custom vTable interfaces, while still allowing dumb clients to use your object via IDispatch.
On the CD The solution for this lab can be found on your CD-ROM under:Labs\Chapter 10\DualObjectLabs\Chapter 10\DualObject\Dual VB ClientLabs\Chapter 10\DualObject\MFC Client
Step One: Add Type Information
Note | Although I am assuming that you will be making modifications to Lab 10-1, the CD solution works with an entirely new in-proc server (so you can have separate 100% dispinterface and dual solution sets). |
Currently, CoSquiggle does not support any type information, and therefore your RawDisp project does not contain an IDL file. Open the RawDisp workspace from Lab 10-1 and insert a new IDL file named dualobject.idl. In this file, define a new dual interface named ISquiggle. Recall that the IDL definition of a dual requires the [uuid], [dual], and [object] attributes. Also recall that a dual interface derives from IDispatch, and therefore a coclass implementing a dual must support the four methods of IDispatch, the three methods of IUnknown, and any custom methods.
Add three methods to this dual interface (with a unique DISPID) named DrawA- Squiggle(), FlipASquiggle(), and EraseASquiggle(). Now, add a library statement which defines the CoSquiggle coclass supporting ISquiggle. Here is the complete IDL file (be sure to insert it into your project and turn off the MkTypLib compatible option):
// The IDL file for your dual CoSquiggle. import "oaidl.idl"; // A dual interface. [uuid(BDFD41A0-580A-11d3-B926-0020781238D4), object, dual] interface ISquiggle : IDispatch { [id(1)] HRESULT DrawASquiggle(); [id(2)] HRESULT FlipASquiggle(); [id(3)] HRESULT EraseASquiggle(); }; [uuid(BDFD41A1-580A-11d3-B926-0020781238D4), version(1.0)] library DualObject { importlib("stdole32.tlb"); [uuid(BDFD41A2-580A-11d3-B926-0020781238D4)] coclass CoDualSquiggle { [default] interface ISquiggle; }; };
Compile your project, and ensure that your IDL code comes through clean. The MIDL compiler will still generate the same set of files you have grown accustomed to, so feel free to take a look.
Step Two: Update CoSquiggle
An object that supports a dual interface derives from that dual interface rather than directly from IDispatch. Modify CoSquiggle to derive from ISquiggle:
// This coclass supports IDispatch and the custom ISquiggle. #include " dualobject.h" // MIDL file defining ISquiggle. class CoDualSquiggle : public ISquiggle { public: ... };
In the previous lab, we defined three helper functions which were called from the Invoke() logic based on the incoming DISPID:
// Your previous lab had a number of private helper functions... switch(dispIdMember) { case DISPID_DRAWSQUIG: DrawASquiggle(); // Private helper function. return S_OK; case DISPID_FLIPSQUIG: FlipASquiggle(); // Private helper function. return S_OK; case DISPID_ERASESQUIG: EraseASquiggle(); // Private helper function. return S_OK; default: return DISP_E_UNKNOWNINTERFACE; }
Late bound clients will still operate in exactly the same way. However, now that we also support a single custom vTable interface, early bound clients will call these same three helper functions directly.
// This coclass supports IDispatch and the custom ISquiggle. #include " dualobject.h" class CoDualSquiggle : public ISquiggle { public: ... // ISquiggle methods AND helper functions for Invoke()! STDMETHODIMP DrawASquiggle(); STDMETHODIMP FlipASquiggle(); STDMETHODIMP EraseASquiggle(); ... };
As you have used COM macros in your class definition, don't forget to update your implementation code to do the same. Finally, update CoSquiggle::QueryInterface() to return an ISquiggle vPtr. You may wish to add a diagnostic MessageBox() call to verify which interface was just handed out to the client, as shown below:
// CoSquiggle now offers services using ISquiggle and IDispatch. #include " dualobject_i.c" // MIDL-generated file for IID_ISquiggle. STDMETHODIMP CoDualSquiggle::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_IDispatch) // <— This is for scripting clients! { *ppv = (IDispatch*)this; MessageBox(NULL, "Using IDispatch", "Late binding", MB_OK | MB_SETFOREGROUND); } else if(riid == IID_ISquiggle) // <— For early clients! { *ppv = (ISquiggle*)this; MessageBox(NULL, "Using ISquiggle", "Early binding", MB_OK | MB_SETFOREGROUND); } else { *ppv = NULL; return E_NOINTERFACE; } ((IUnknown*)(*ppv))->AddRef(); return S_OK; }
All other methods of CoSquiggle will remain as is. Go ahead and recompile your server, because at this point you are finished! In the next step of this lab we will test each binding option using a Visual Basic client.
Step Three: A VB Client
If you would rather create a C++ client to test your new dual interface object, by all means, please do! Just query for ISquiggle or IDispatch and have your fun. Here we will take the quick and dirty option and create a VB client. This client allows the end user to specify the binding option before calling the methods of CoSquiggle. First off, set a reference to the CoSquiggle type information (use the Browse button to navigate to the debug folder), as we need the information within for early binding. Next, add two command buttons to your form, one for late binding and one for early binding as shown in Figure 10-6:
Behind door number one, add some VB code to create a late bound connection using IDispatch, as you have seen before:
' Going late bound without any type information. ' Private Sub btnLate_Click() Dim objLate As Object Set objLate = CreateObject("dualobject.codualsquiggle") objLate.DrawASquiggle objLate.EraseASquiggle objLate.FlipASquiggle Set objLate = Nothing End Sub
Behind door number two, make an early connection, making use of the type information provided by the CoSquiggle *.tlb file:
' Going early bound with type information. ' Private Sub btnEarly_Click() Dim objEarly As DualObject.CoDualSquiggle Set objEarly = New CoDualSquiggle objEarly.DrawASquiggle objEarly.EraseASquiggle objEarly.FlipASquiggle Set objEarly = Nothing End Sub
So then, as you can see, each type of binding will always ultimately call the same three public methods of CoSquiggle; the only difference is how the client chooses to get there. If the client has a pointer to IDispatch (i.e., the Object data type and CreateObject()) the method to call is resolved using a DISPID. Early bound clients call directly into the correct slot of the vTable, bypassing the dynamic lookup.
Step Four: An MFC Client
If you use MFC to build front ends for your COM servers, you will be happy to know that you do have some framework support to help with IDispatch programming. To give this a whirl, create a brand new EXE application using the MFC AppWizard (File | New). When you are configuring your new project, be sure to add support for Automation in step 3 (Figure 10-7). This option adds a call to AfxOleInit() in your InitInstance() method, which initializes the COM libraries (this also sets up your application to support a dispinterface of its own, which we do not care about for this lab).
Once you have a new EXE, open up the MFC AppWizard (Ctrl + W) and insert a new class From a Type Library using the Add Class button. From the resulting dialog box, open your server's *.tlb file.
What you will see next is a dialog listing each IDispatch-based interface found in your server's IDL file. As you can see, MFC is about to generate a new COleDispatchDriver derived class, which is used to wrap up calls to Invoke() on your behalf:
If you examine the header file of this new class, you will see that each of the methods you defined in your IDL have come through as public member functions:
// ISquiggle wrapper class class ISquiggle : public COleDispatchDriver { public: ISquiggle() {} // Calls COleDispatchDriver default constructor ISquiggle(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {} ISquiggle(const ISquiggle& dispatchSrc) : COleDispatchDriver(dispatchSrc) {} // Attributes public: // Operations public: void DrawASquiggle(); void FlipASquiggle(); void EraseASquiggle(); };
If you check out the implementation of each method, you will see a call to COleDispatch- Driver::InvokeHelper(). This handy method packs up your arguments into VARIANTS, places them into a DISPPARAMS structure, and calls Invoke(). For example, here is the code behind DrawASquiggle():
// Call Invoke(...[id(1)] ...) void ISquiggle::DrawASquiggle() { InvokeHelper(0x1, DISPATCH_METHOD, VT_EMPTY, NULL, NULL); }
So then, what MFC has done is created an OO wrapper around the ISquiggle interface. To use this wrapper class, first create some GUI mechanism to trigger the calls to CoSquiggle (your lab solution uses a custom Toolbar button). In your squiggling activation code, use your new ISquiggle shim class by creating an instance, calling CreateDispatch(), and invoking each method of ISquiggle:
// CreateDispatch() does the work to grab your object's IDispatch interface. void CMFCClientView::OnSquig() { ISquiggle s; s.CreateDispatch("DualObject.CoDualSquiggle"); s.DrawASquiggle(); s.FlipASquiggle(); s.EraseASquiggle(); }
That's it! MFC takes care of the grunt work.
To wrap up the story so far, when you support dual interfaces, you make your object as flexible as possible to the clients it serves. Now, before seeing how ATL can help, we will take a quick examination of using type information in the server code to make implementing the methods of IDispatch far less painful.
| < Free Open Study > |
|