Second, add a call to RegisterShellFileTypes in CTIAApp::InitInstance so that the correct file extension gets associated with your app. RegisterShellFileTypes will only work correctly if you have string resource 129 set up with the correct file extension first.
2. Add a Simple Object Model
Now, lets talk about making this puppy invisible and slapping an Automation layer on top of it.
Please note, this paper is intended merely to present two techniques for implementing COM objects in your MFC app. It is not intended to direct you toward the correct object model for your application. Focusing on your customers needs will probably lead you in the right direction for deciding on what objects and relations should be in your model.
TIAs object model will have only three objects in it: Application, Document and Rectangle. Figure 1 depicts this in standard UML static structure notation.

Figure 1
The Document object will be an example of exposing COM interfaces directly from an implementation object. The Application and Rectangle objects will be examples of using thin layer COM objects which refer (through implied context or through a member variable) to an underlying implementation object.
Checking the full server check box during the running of app wizard outlined in Step 1 caused the wizard to generate a document subclass derived from COleServerDoc. Checking the OLE Automation check box caused the wizard to add a dispatch map to the document subclass and to generate a Type Library source file (*.odl or *.idl).
Use class wizard to set up the skeleton for TIAs initial Automation objects. Class wizard provides an easy to use dialog interface for adding properties and methods in all the necessary places in the source files. However, do not depend on class wizard to do the right thing for you all the timethat kind of dependence is a bad thing to foster. As a developer, you need to understand what the code thats being generated does. Otherwise, when theres a problem, it becomes very difficult to figure out what the cause of the problem is. Its much easier to solve such problems if you have taken the time to study and understand the code that class wizard spits out. Go look up the macro definitions (use that F12 key) for all the dispatch map macros and for the interface map macros. Understanding whats really going on in CCmdTarget to enable Automation capabilities is crucial for being able to debug your app.
When you are using class wizard to add properties or return values or parameters, the list of available variable types changes based on the current state of the wizard. So, for example, sometimes youll see BSTR in the drop down list of variable types, whereas other times youll see LPCTSTR or CString. They are essentially interchangeable for our purposes. Class wizard generates correct string code when one of these type is selected. The type thats passed via Automation to the outside world is almost always BSTR when you get right down to it. CString is generally pretty good at dealing with both BSTR and LPCTSTR when it needs to.
For the purposes of making it easy to transition to dual interfaces later on, I recommend using the Get/Set methods for Automation properties. Theres essentially a one-to-one mapping with dual interface methods and its a breeze to implement read-only properties with this mechanism. When you click the Get/Set radio button in the class wizard Add Property dialog, the string type that shows up in the drop down variable type list is BSTR. If another radio button is checked, though, it may be LPCTSTR or CString. So if BSTR isnt showing up in your drop down list, click on the Get/Set button first.
2.1 Before using class wizard, remove the *.odl (or *.idl) file from the project. I prefer to use a custom build step to build the Type Library and TLB header file and then copy those files into a source directory. There are two reasons for using a custom build step. The first reason is that the custom build step gets performed first before any other files get built. This is good for steps that generate source code so that other source files in the project can include the generated files. The second reason is to copy the TLB and TLB header files into a source directory so that you still have a copy of them around after you blow away the output directory.
2.2 Use class wizard to add some Automation properties and methods to the document object. Bring up the Class Wizard dialog and click on the Automation tab. Choose your document class from the drop down list of classes.
2.3 Add the following properties and methods to the document:
P/M
|
Name
|
Parameters
|
Return Type
|
Notes
|
P
|
Name
|
Get/Set
|
BSTR
|
Default, RO
|
P
|
Visible
|
Get/Set
|
long
|
-
|
P
|
Modified
|
Get/Set
|
long
|
-
|
P
|
FullPathName
|
Get/Set
|
BSTR
|
RO
|
M
|
Save
|
void
|
long
|
-
|
M
|
SaveAs
|
LPCTSTR szFullPathName
|
long
|
-
|
M
|
AddColoredRect
|
long nLeft, long nTop, long nWidth, long nHeight, long nRGB
|
LPDISPATCH
|
-
|
2.4 Then in the document dispatch map, change the property set methods for both Name and FullPathName to SetNotSupported; in the class definition and class implementation remove the SetName and SetFullPathName methods. Doing those two things make Name and FullPathName read-only properties.
Now, lets take a look at creating some thin wrapper COM objects for the (already implemented with C++ internally) MFC application CTIAApp and the TIA_ColoredRect item.
Again, use class wizard to get the jump start if you want, but understand the code that its writing for you.
2.5 Add an application class as follows:
2.5.1 Class CTIAAppAut derives from CCmdTarget
2.5.2 File names are TAppAut.*
2.5.3 Createable by Type ID: TIA.Application
2.5.4 Dont pollute your Component Gallery with thisits specific to this application
2.6 Add these properties and methods to app:
P/M
|
Name
|
Parameters
|
Return Type
|
Notes
|
P
|
Name
|
Get/Set
|
BSTR
|
Default, RO
|
P
|
FullPathName
|
Get/Set
|
BSTR
|
RO
|
P
|
Visible
|
Get/Set
|
long
|
-
|
P
|
Version
|
Get/Set
|
long
|
RO
|
P
|
CommandLine
|
Get/Set
|
BSTR
|
RO
|
M
|
AddDocument
|
BSTR szFullPathName
|
LPDISPATCH
|
"" for new blank document
|
M
|
GetDocumentCount
|
void
|
long
|
-
|
M
|
GetDocument
|
VARIANT vDocID
|
LPDISPATCH
|
-
|
M
|
RemoveDocument
|
VARIANT vDocID
|
long
|
-
|
2.7 Make Name, FullPathName, Version and CommandLine read-only properties by replacing the Set functions with SetNotSupported in the dispatch map. Remove declarations and implementations of the corresponding Set methods.
2.8 Add an item class as follows:
2.8.1 Class CTIARectAut derives from CCmdTarget
2.8.2 File names are TRectAut.*
2.8.3 Support Automation, but not createable (these will be retrieved through the document method AddColoredRect)
2.8.4 Dont pollute your Component Gallery with thisits specific to this application
2.9 Add these properties and methods to CTIARectAut:
P/M
|
Name
|
Parameters
|
Return Type
|
Notes
|
P
|
Name
|
Get/Set
|
BSTR
|
Default, RO
|
P
|
Color
|
Get/Set
|
long
|
-
|
M
|
SetRectangle
|
long nLeft, long nTop, long nWidth, long nHeight
|
void
|
-
|
M
|
GetRectangle
|
long *pnLeft, long *pnTop, long *pnWidth, long *pnHeight
|
void
|
-
|
2.10 Make Name a read-only property by replacing the Set function with SetNotSupported in the dispatch map. Remove the declaration and implementation of the corresponding Set method.
After adding these three Automation objects, with the properties and methods listed in the above tables, youve laid the groundwork for the simple object model previously illustrated in Figure 1.
OK, now youre ready to fill in the guts of all these Automation methods so that other developers can write VB/A code to drive this app. To that end, look to the source code files in TIAlook up the method that youre interested in to see how its implemented. The remainder of the article discusses some relevant, important issues if you need elaboration beyond the source code and its accompanying comments.
The file MFCUtils.cpp contains the implementation of some useful helper functions which will greatly simplify implementing many of these Automation methods. They show or hide the apps main window, find a CDocument based on name, index or interface pointer, and maintain a deferring updates flag. I believe they are all general to any MFC app. That is, you shouldnt have any problems taking this single source file and its corresponding header and compiling them into any MFC-based project. Of course, if you export the functions from a DLL, youll need to add the correct AFX_MANAGE_STATE stuff at the top of each function... Please let me know if you do run into problems using these functions in your app so that I can keep MFCUtils.cpp general and useful.
Now is a good point to pause in our tutorial again so that we can compile, link and run ... and automate! You should be able to run the Dim as Object sample script, TIATestObject against TIA at this point and have it create, open and save some documents, show and hide the app or individual documents, and create items within the documents.
Now that weve made it this far, lets take it one step further and add dual interfaces to each of the Automation objects.
3. Add Dual Interfaces
In the sample code for TIA, all the relevant places where modifications were made to add dual interface support are labeled with the comment // DUAL_INTERFACE_SUPPORT. Search on that to find the nooks and crannies of the code related to this section.
The files MFCDual.cpp and MFCDual.h are copied from the MFC sample ACDUAL. I dont remember making any modifications to the originals from that project, although I may have rearranged stdafx.h or initguid.h header files. MFCDual.h contains useful macros that enable one-line implementation of (a) delegating the IDispatch portion of the dual interface to CCmdTarget, (b) ISupportErrorInfo, and (c) declaring nested class implementations of both the dual interface and ISupportErrorInfo. You know, like all your other favorite MFC macros...
The first thing to do before adding dual interfaces to the objects in the ODL file is to make a bunch of GUIDs that are all near each other alphabetically in string form.
3.1 Run GuidGen.exe and copy new GUIDs repeatedly, pasting them into a text file. I made 32 for TIA; find the results of my GuidGen session in TIAGuids.cpp. TIA really only needs 10 for this version, but 32 gives room for expansion later.
Basically, the rules for writing dual interfaces are like this:
You have to write them by hand. MFCs class wizard up through DevStudio 5.0 does not generate dual interfaces for you. Or if it does, the feature is well hidden enough that I have never found it...
- Derive all dual interfaces from IDispatch; if it doesnt derive from IDispatch then its not dual, its custom. A dual interface derives from IDispatch and has methods beyond those defined in IDispatch.
- Make sure the dual attribute is set in the odl/idl file on the interface.
- For each property, youll need one or two methods depending on whether the property is read-only, write-only or read/write. Strange as it may seem, there may be some write-only properties that actually make sense.
With dual interfaces, all properties are actually defined as one or two methods, all methods are still methods, and all methods return an HRESULT. Specify actual return values as [out, retval] parameters. This allows these method calls to be remotable (marshallable) giving the intermediate transport layer (RPC) the ability to return error codes on any method calls that have to go through proxies and stubs, whether cross-thread, cross-process or cross-machine. You just return HRESULTs, dont worry about all that proxy/stub stuffCOM will take care of you. Read Don Box to understand more about marshaling in depth.
Hint: Dont declare two [out] parameters on a propget and make one of them the retvalMIDL doesnt like this and it doesnt give you a very handy error message either.
Now the generated GUIDs come in handy.
3.2 Add three dual interfaces to TIA.odl by hand.
3.3 Change the GUIDs for everything else too, so that all the GUIDs start with the same 6 digits. That will help us find them in the registry if we need to because theyll all be clustered together, alphabetically. When we change the GUIDs in the odl file, we also have to change the in-code GUIDs that class wizard generated for us. See the sample code commented // CHANGED_GUID for these cases.
3.4 Also, while editing the odl file, rename the coclasses and dispinterfaces to be consistent: Application, Document and Rectangle for the coclasses and ITIAApplicationDisp, ITIADocumentDisp and ITIARectangleDisp for the dispinterfaces.
3.5 Add methods and properties to the new dual interfaces. Theres a mental mapping that you can make from dispinterface to dual interface. Study the definitions of the three dual interfaces, ITIAApplication, ITIADocument and ITIARectangle in TIA.odl and compare them to the corresponding dispinterface declarations. Upon inspection, its fairly straightforward to translate from the dispinterface definitions that class wizard has generated to the dual interface definitions that you have to write by hand. Getting in there and doing a few yourself is the best way to get comfortable with writing odl source. The key points in this translation are that all methods return HRESULTs, all properties become one or two methods with [propput] or [propget] attributes and all return values from the dispinterface turn into [out, retval] parameters.
3.6 Once the odl source is complete, run just the custom build step to generate a copy of the header file TiaTLB.h. Now for each dual interface, following in the footsteps of the MFC sample app ACDUAL, there is a list of steps to follow to add that interface to the appropriate C++ class.
3.6.1 Add a nested class declaration for supporting the dual interface inside your class header. Use the BEGIN/END_DUAL_INTERFACE_PART and DECLARE_DUAL_ERRORINFO macros from MFCDual.h. Then copy and paste the method signatures from the interface definition in TiaTLB.h and remove the PURE or =0 designation from each.
For example, from the class declaration for CTIADoc:
// *****************************************************************************
// DUAL_INTERFACE_SUPPORT
BEGIN_DUAL_INTERFACE_PART(Dual_TIA_Doc, ITIADocument)
// Copy and paste ITIADocument methods here from TiaTLB.h
// after it has been generated for the first time...
// Don't forget to remove the "PURE" or "=0" designation.
// We do need to make this a concrete class...
STDMETHOD(get_Name)(THIS_ BSTR FAR* pbstrName);
STDMETHOD(get_FullPathName)(THIS_ BSTR FAR* pbstrFullPathName);
STDMETHOD(get_Modified)(THIS_ long FAR* pnModified);
STDMETHOD(put_Modified)(THIS_ long nModified);
STDMETHOD(get_Visible)(THIS_ long FAR* pnVisible);
STDMETHOD(put_Visible)(THIS_ long nVisible);
STDMETHOD(Save)(THIS_ long FAR* pnResult);
STDMETHOD(SaveAs)(THIS_ BSTR bstrFullPathName, long FAR* pnResult);
STDMETHOD(AddColoredRect)(THIS_ long nLeft, long nTop, long nWidth, long nHeight, long nRGB, ITIARectangle FAR* FAR* ppRect);
END_DUAL_INTERFACE_PART(Dual_TIA_Doc)
DECLARE_DUAL_ERRORINFO()
3.6.2 Add a part entry for the dual interface (and one for the ISupportErrorInfo interface) to the interface map for each Automation object.
For example, add this to the map for CTIADoc:
INTERFACE_PART(CTIADoc, IID_ITIADocument, Dual_TIA_Doc) // DUAL_INTERFACE_SUPPORT
DUAL_ERRORINFO_PART(CTIADoc) // DUAL_INTERFACE_SUPPORT
3.6.3 Then add implementation macros for delegating the IDispatch portion of the dual interface to the preexisting CCmdTarget implementation.
Again, using CTIADoc as the example:
// Delegate standard IDispatch methods to MFC IDispatch implementation:
DELEGATE_DUAL_INTERFACE(CTIADoc, Dual_TIA_Doc)
3.6.4 Then add the implementation macro for supporting ISupportErrorInfo.
Example:
// Implement ISupportErrorInfo for this dual interface:
IMPLEMENT_DUAL_ERRORINFO(CTIADoc, IID_ITIADocument)
3.6.5 Then add an implementation of each method declared in the dual interface.
Example:
STDMETHODIMP CTIADoc::XDual_TIA_Doc::get_Name(BSTR FAR* pbstrName)
{
METHOD_PROLOGUE(CTIADoc, Dual_TIA_Doc)
TRY_DUAL(IID_ITIADocument)
{
(*pbstrName)= pThis->GetName();
return NOERROR;
}
CATCH_ALL_DUAL
}
3.7 Build it and run. Use the sample VB code to test the new TIA Automation layer.
After implementing all the methods on all the dual interfaces, youre ready to see what kind of speed difference it makes to use the dual interfaces. A simple test in VB/A should prove to you that with CCmdTargets implementation of IDispatch, you are likely to see a real performance difference between using dual interfaces and not using them. One exception is when the operation performed inside the guts of the method is slow anyway.
Congratulations! Youve made it through the tutorial section! The rest of the article contains a bit more reasoning and a few more tips and hints. So stay tuned...
Related Topics
The Thin COM Wrapper
Wrapping an already-implemented C++ object with a COM layer is not hard. There are two basic approaches to the problem: expose an interface directly from the existing C++ object, or implement an entirely new object making the two objects aware of each other. This article demonstrates both approaches. The latter approach lends itself well to enabling separate lifetimes for each object and to enabling multiple COM objects representing the same underlying real object. The useful life of the COM object is the overlap of the two objects lifetimes. The two objects need a bidirectional communication mechanism to keep synchronized with each other unless theres another overriding rule governing their lifetimes.
For example, Figure 2 is an illustration in the form of a sequence diagram that shows what happens when an Automation client obtains a Document object, calls the AddColoredRect method to obtain a Rectangle object and then closes and releases the document before releasing the Rectangle object.

Figure 2
The VB client code for this sequence diagram would look like this:
Dim oApp as TIA.Application
Dim oDoc as TIA.Document
Dim oRect as TIA.Rectangle
Set oApp = CreateObject(TIA.Application)
Set oDoc = oApp.GetDocument(1)
Set oRect = oDoc.AddColoredRect(10, 10, 20, 20, 0)
oApp.RemoveDocument oDoc
Set oDoc = Nothing
After this point oRect will not be able to communicate
with its underlying C++ implementation object...
The underlying object gets destroyed when the document
gets destroyed in the Set oDoc = Nothing call.
Attempts at calling these methods will result in an error.
Set oRect = Nothing
The Document object is an example of exposing the interface directly from the C++ implementation object. I illustrate this technique with the document object because thats the way class wizard happens to make it. Although the interfaces are implemented directly on the object, the Document is still a challenge from an Automation perspective because there is a method on the Application object to close a document. If the Document gets closed while some client still has an interface to it, then we have to make sure that all methods on that interface can be called safely even after a close has occurred.
The thin COM wrapper object for the TIA_ColoredRectangle class is CTIARectAut. Figure 2 shows an instance of each of these typesoRectInternal is a TIA_ColoredRectangle and oRect is a CTIARectAut. The keys to making the thin COM wrapper work are ownership and lifetime. Some internal C++ object is already implemented. Each instance of this object is owned by some keeper object. In our case, the keeper of TIA_ColoredRectangles is the document. The only way to create one is to call the documents AddTIARect method. The only way to destroy one is to destroy the document. The rectangles go away when the documents DeleteContents method is called. If we wanted to destroy them individually before document destroy time, we would have to implement a DestroyTIARect method on the document, too.
Given this information, we can now construct a thin COM wrapper object for a TIA_ColoredRectangle. The way to get at the thin wrapper is to call a method on the wrapped object itself. The wrapped object will create the thin wrapper and remember a reference to it for its own lifetime. The wrapper object will be given a pointer to the wrapped object and remember it for its lifetime. Heres the important point: each wrapper and wrappee must notify the other when one is being destroyed. For our internal C++ object, that would be in the destructor. For our thin COM wrapper object, that would be in the OnFinalRelease code. That way the thin COM wrapper knows that it cant communicate with the real implementation beyond the implementation objects destructor. And the implementation object knows not to hand out any more references to its stashed interface pointer if the thin COM wrapper goes away. Rather it would have to create a new one if a client was requesting one. Actually, if the internal C++ object holds a reference on the thin COM wrapper until the C++ objects destructor time, then the thin COM wrapper, once it is brought into existence, will by definition always be around at least as long as, if not longer than the C++ object.
Adding Dual Interfaces
Once you build an object model for your app, making its interfaces dual can improve its performance. A dual interface is an IDispatch-derived interface which has an Automation compatible signature. That just means it can be marshaled across thread boundaries if necessary. Since it inherits from IDispatch, clients can treat it as either the custom interface or IDispatch and it will work. Since it is a custom pre-defined interface, clients can bind to it early at compile time and avoid the overhead of calling GetIDsOfNames and packaging up VARIANT arguments for Invoke. They can instead call it directly with raw arguments. Of course, the degree of performance improvement you get depends entirely on the guts of your methods. If youre doing something slow inside the method itself, adding dual interfaces is not going to buy you much in the way of performance improvement. However, its still nice for clients to be able to write to a compile-time bound interface, even if there is no performance benefit for your application.
Try TIA with both syntaxes in VB/A. Dim as Object and Dim as TIA.Application (or Document or Rectangle). Which one is faster on your machine and by how much? The testing I did on TIA indicates a 30-35% time savings for the dual interfaces. The actual savings youll see will vary depending on what the methods actually do. If theyre slow on the inside, expect a much smaller performance improvement from adding dual interfaces. If, however, things are blazingly fast and IDispatch is the source of your speed woes, then adding dual interfaces can help significantly.
Deferring Updates
Implementing a deferred updating technique can help to avoid redraw during Automation induced document changes. If an Automation controller changes the state of your document, its nice not to have to redraw immediatelyif youre getting one call through Automation, youre likely to get many calls. When a programmer writes a script against your object model, hes going to be calling more than one method to do whatever his or her program does.
Usually, in response to a document change, you call SetModifiedFlag(TRUE) and then UpdateAllViews and/or UpdateAllItems. In CTIADoc, this behavior is encapsulated in a Changed method which we call whenever the document has changed in a significant way. (In TIA, the only Changed call comes from adding a rectangle to the document.)
If we change the document via Automation, we first call MU_SetDeferringUpdates(TRUE) from the method that changes the state of the document, then make our changes and call Changed. Notice inside the implementation of Changed, we only call UpdateAll* if MU_GetDeferringUpdates returns FALSE. If instead it returns TRUE, we set a flag to let us know if we were changed while deferring updates was on. Then later, at idle time, we check that flag and if its true and were not deferring anymore, we resend ourselves the Changed message, which will then trigger a view update.
This technique will not turn off redraw entirely for your views. If a windows paint message comes through (because the user resized the TIA document window, for example) then your views draw method will get called. It merely defers posting updates to the documents views when Automation methods are the cause of the document change.
Theres one more piece of the puzzle that makes this work reliably. In the app, during idle time, we call MU_StopDeferringUpdatesAfter(nMilliseconds). This will turn off the deferring updates flag if, and only if, the app is idle and nMilliseconds have passed since the last call to MU_SetDeferringUpdates(TRUE). Idling continues until this much time has passed or a Windows message comes into the apps message queue. After turning off deferrals it will kick the app with one more idle message which will get broadcast off to the documents and allow them to update their views if they were changed during the deferral period.
TIA.Application.Visible and TIA.Document.Visible
The MFC specific advice regarding how to make the app invisible and how to close documents via Automation is no special adviceyou could derive it yourself by analyzing the MFC source code. However, it is my hope that you may want to implement these features with your MFC app and that this article will provide you with the right pointers to save you the time and energy that Ive already spent.
Making an MFC application invisible when driving its documents through Automation is more difficult than it would appear at first glance. Some hidden window gotchas will surely bite you unless youve previously worked through this problem. This article and the sample code attempt to protect you from the pain.
Theres a little more to do than calling ShowWindowsee CTIADoc::OnIdle to understand why... OnShowViews should only be called when the actual visible state of the document changes, not merely when the documents frame windows visible bit changes. Because we are allowing the app to be visible or invisible also, we need to take its visibility into account too in OnIdle. If the app is invisible, then all of its documents are also invisible. If the app is visible, then the document checks its frame windows visibility and reports it as its own.
TIA.Application.RemoveDocument
Closing an MFC document involves taking into account the environment, whether the document is open stand-alone, open as an embed in another container document, or open in-place.
Because CTIADoc is derived from COleServerDoc, it is not easy to close it through Automation simply by firing an OnCloseDocument or OnFileClose message off to it. The document in question could be open as a result of an open embed or link or an in-place editing session in some container app. It may have been opened by the end user and then acquired through the Application.GetDocument method. If a document was opened by an end user, should an Automation program be allowed to close it? It may have been added through the Application.AddDocument method. All that having been said...I believe its true that the ForceClose method I have developed for CTIADoc accounts for all those different scenarios.
The RemoveDocument method is a little rude to the end user, though. If you call Application.RemoveDocument on a modified document, the implementation ignores the modified flag and forces the document closed anyway. The Document.Modified property is exposed for this reason. If you want to drive TIA through Automation, the capability exists for your program which drives TIA to check whether or not its going to disturb the user to close a document out from under him or her.
General Class Wizard Advice
There are some things you just learn to do after working with a given development environment for a while. For example, I avoid specifying OLE_COLOR as a parameter type using Class Wizard. MkTypLib doesnt like it. Just use a long and put the knowledge of it being an OLE_COLOR internal to your method. Although it would be nice to get VBA to display the little color popup for a color property, wouldnt it? Im not sure if they recognize the OLE_COLOR type or if they recognize only certain pre-defined DISPIDs as colors... If anybody out there knows, drop me an email with the info.
Stick with BSTR, long and double as your basic parameter types. Marshallable interfaces are OK, too, like IUnknown, IDispatch, or custom or dual interfaces from your own type library. Use VARIANT for parameters where many different types make sense or for parameters that have default values. If the VARIANT comes in to your method empty then assume the default value. SAFEARRAYs are also useful for passing large amounts of data across a single method call.
There is one thing to watch out for if you add properties and methods using class wizard more than once: ordering. Class wizard numbers the properties first with DISPIDs starting at 0 for the default, then 1, 2, and so on. Then class wizard numbers the methods starting at the next number after the last property. Which DISPID goes with which property or method is, for the most part, irrelevant to you. However, by default, the dispatch map starts at 1 and goes up from there with a special entry at the end for the default property. The default property always has explicit DISPID 0. Sometimes, after some combinations of adding and deleting properties and methods in class wizard, it will leave you with duplicate DISPID entries in the odl file. Just look at the dispatch map, start counting at 1 and make the odl file reflect the reality of the dispatch map. When CCmdTarget is looking up a method or property based on its DISPID, the dispatch map is how it knows which method to call with which arguments. The dispatch map is the code CCmdTarget actually usesmake your Type Library match it by making sure the odl file matches it.
Type Library
Embedding a Type Library into an EXE or DLL as a resource is merely a convenient packaging mechanism. There may be a valid reason why youd like to ship a separate file for your Type Library, such as versioning or customer demand. Perhaps your latest TLB is built in as a resource, but you ship your applications older version TLB files as stand alone files.
See the line of code in TIA.rc2 which includes the TypeLib as a resource in TIA.exe. Also, theres a line of code in InitInstance which takes care of registering the TypeLib. You could modify that InitInstance code to register a standalone TypeLib file if you wish.
One thing I have found helpful for building correctly implemented software is to remove the ODL or IDL file from the DevStudio project and replace it with a custom build step that performs the equivalent action. I do this because I want the ODL file to generate the header file for my interfaces for me rather than writing my interface in ODL (or letting class wizard write it, as the case may be) and then writing it again in a C++ header file. True, you can modify the ODL build step to generate the header file, but if you actually include the generated header file in the same project (a desirable action for a number of reasons) DevStudio doesnt seem to handle the dependencies properly. Im not sure I understand why because its certainly not a complex dependency. Nmake can handle it just fine with the correct make file. The ODL file simply must be processed first and then the C++ file dependencies need to be analyzed taking into account mktyplib or MIDLs output. If anyone can explain a way to get DevStudio to work correctly in this respect, I would certainly like to hear about it.
A custom build step will force the ODL file to build first and you are more likely to get a consistent build out of DevStudio than with an ODL/IDL file added to the project. A habit that Ive gotten myself into which I highly recommend is blowing away the whole output directory and doing a Rebuild All whenever you make a change to the ODL/IDL file. Otherwise, you may spend a large chunk of time pulling your hair out over a simple out of date OBJ file. Stepping through a function, stepping into a method on a valid looking object and BOOM! Your stack pointers *%!$^@ !* 0x00000000you know its time for a Rebuild All...
Use GuidGen.exe to Obtain a Range of GUIDs
Its helpful to be able to identify quickly any GUIDs of yours that are associated with your classes and interfaces. Class wizard generates these for you using the same functionality that GuidGen uses. However, your type library and document GUIDs are generated when the app is first created at app wizard time; GUIDs for the other Automation classes and interfaces are generated when you add them with class wizard. Because they are added at two different times, you end up with numbers that may not even be in the same ballpark as one another. To make them more recognizable, I recommend using GuidGen to generate however many GUIDs you need (plus a few extras) all at one time and use those. I made 32 GUIDs for TIA altogether; I actually used 10one for the type library and three each for the application, document and rectangle objects (coclass, dispinterface and dual interface for each object). Now while they are all still unique, they all at least start with the same 6 digits and over time, I will learn to recognize those 6 digits when I am trying to figure out what the heck is going on in my computers registry...
Adding an Interface to an MFC COM Object
MFC uses nested classes to implement COM interfaces on CCmdTarget derived classes. This is a standard technique for exposing multiple interfaces from a single object without using multiple inheritance. ATL, on the other hand, uses multiple inheritance quite liberally to achieve the same goal. More details on both techniques of implementing multiple interfaces on an object can be found in Brockschmidt (near the end of Chapter 2) and other standard reference texts on COM and OLE.
MFC associates an interface id (IID) with a member variable and uses the offset of that member variable to compute the correct interface pointer when that IID is queried for. This is what the INTERFACE_PART macros do. They declare QueryInterface, AddRef and Release for your nested class, and then you declare the custom methods for your interface in between the BEGIN and END macros. The convention is to name the nested interface class the same as the interface only without the leading I. For example, in the macro DECLARE_DUAL_ERROR_INFO, the name SupportErrorInfo is used as the argument to BEGIN and END_INTERFACE_PART. This results in a nested class by the name of XSupportErrorInfo and a single instance of that nested class by the name of m_xSupportErrorInfo.
The whole concept of nested classes was quite foreign the first time I encountered it. Then one day, I suddenly understood that its just like any other class, but it happens to be declared inside a containing class. (Aha! he said...) Because of this, you see stuff like COleControl::XOleControl::GetControlInfo all over the place in the MFC source. Its much easier to deal with after that fog has lifted... The nested class technique is used extensively throughout MFC for interface support. The more you get comfortable with it, the better off youll be debugging MFC COM objects.
So get to work! What are you sitting there for? Youve got some interfaces to implement...
Ideas for Future Tutorial Sessions
The areas where TIA lacks, which I think would make good next steps for further tutorials are:
Step 4. Support IEnumVARIANT for the Applications documents and for the Documents rectangles. (Allows For/Each syntax usage in VB/A...)
Step 5. Add delete of rectanglesas it stands, once a rectangle is in there, its in there for good
Step 6. Add undo support
Step 7. Make embedding support work correctlyneeds a real rectangle for items in the document
Step 8. Add DocObject support, too
Let me know if youd like to see another step in this tutorial. Comments and feedback always welcome.
About the Author
David Cole is a software developer obsessed with understanding every possible way to implement COM objects. (OK, maybe not obsessed, but certainly at times preoccupied...) When hes not working on his day job at Visio Corporation, writing articles for CodeGuru on his bus ride home or doing yet another programming project on the side, he enjoys the important things in life: hanging out with his wife and kids, being generally musical and eating all kinds of chocolate. He can be reached by email at DLRdave@aol.com.
References
- ACDUAL, the MFC sample app demonstrating how to add dual interfaces to existing MFC classessearch for it on your VC CD or on the web at http://www.microsoft.com
. Also see MFC Technical Note 65.