The purpose of this article is to teach how to:
- Make an MFC app, its documents and its other objects scriptable by adding a COM Automation object model. Highlights of the sample object model in TIA are a deferred updating technique, Visible properties and a RemoveDocument method.
- Wrap an internal C++ object, the “real” data, with a COM object interface to the data.
- Add dual interfaces to MFC COM objects.
So you want to add an object model to your MFC app. Perhaps you want to make your app or its documents invisible. Maybe you just want to be able to close documents by Automation. Well, you’ve come to the right place.
Adding a COM object model to your MFC app allows driving it from VB, VBA, C++, Java or even Delphi. With the proliferation of VB/A knowledge that’s out there, having an object model built into an application is rapidly becoming a necessity in today’s software market. If not a necessity, then at least a valuable commodity. Which tool is more likely to be recommended by a standards committee, the customizable, extendible one with the object model, or the one with exactly the same (or even slightly better!) feature set, but no Automation capabilities?
The various topics covered by this article are independently applicable. You may decide to use some, but not others. The reader should already have a basic understanding of how a non-Automation-enabled MFC app/doc/view application works.
First things first—build and run the sample app and its driving script before getting bogged down in all this reading!
- Open the sample DevStudio project TIA.mdp in DevStudio 4.0 or higher.
- Build the Win32 Debug target. Heck, build the Release target if you trust me…
- Run TIA.exe standalone first to get everything registered properly.
- Open up TiaAut.vsd in Visio 5.0 or higher and run the VBA macro TIARunTest or TIAFormInterface. If you don’t have Visio, import the VBA module TIATest.bas into any VBA host or VB and run the macro TIATest from there.
Of the code in TiaAut.vsd, TIATest is a script that runs through all the methods and properties of the object model in a reasonable fashion. TIAFormInterface brings up an interactive form, allowing the user to call methods and properties by point-and-click, in the order and with the arguments that he or she chooses.
Let’s move on to the “how to’s” of the tutorial before all you impatient developer types lose interest. The “Related Topics” section contains material for those who want to dig deeper to understand the “whys” a little better. The sample code, of course, is the ultimate reference. Just go study it and run it through the debugger. Then, come back to this paper later—maybe it will help you understand something better.
This section describes the exact steps used to create the accompanying sample app, TIA, The Invisible Application. For discussion of the reasoning behind some of the steps, refer to the remainder of the article.
I used MS DevStudio, Standard Edition (gasp), version 4.0 (GASP!) to produce the sample code that accompanies this article. I have verified that it converts, builds and runs under DevStudio 5.0 just fine. I have not verified this for DevStudio 6.0 as of this writing. My reasoning behind developing on this “older” platform is to reach as many developers out there as possible.
—Step up on soap box—
It is my belief that software developers should take a step back in time a little more often than they do and perhaps gain a bit of perspective on what it is we’re trying to accomplish with these infernal machines. While it’s great to get the latest and greatest hardware, OS, development environment and productivity software, I’m not convinced that our customers are always best served by the “give the software developers the hottest machines on the market” mentality/phenomenon. In fact, for people who bought machines much more than a year ago, the software being developed today is almost certainly guaranteed to be intolerably slow for them—the software developers just do not have the perspective that these customers do. New software runs more slowly than it should on older machines. What seems acceptable to developers on their development machines is completely frustrating to the end users stuck with yesterday’s hardware.
—Step down from soap box—
1.1 Run the MFC Exe App Wizard. (Remember, DevStudio 4.0 Standard Edition—adjust accordingly for any differences in newer wizards.) Name the new application TIA.
1.1.1 Wizard Step 1 – accept defaults
1.1.2 Wizard Step 2 – accept defaults
1.1.3 Wizard Step 3 – full server, OLE Automation
1.1.4 Wizard Step 4 – accept defaults
1.1.5 Wizard Step 5 – accept defaults
1.1.6 Wizard Step 6 – add TIA to class names without it (to make everything start with CTIA)
1.1.7 Click Finish
1.2 Immediately change your project warning level to 4 (most severe level) and set the option to treat all warnings as errors. This really will pay off in the long run—you should be ashamed of shipping code to your customers that the compiler doesn’t even like…
1.3 Select the Debug target and turn on browse info. Browse info is, like, the best invention in the whole world for learning other people’s code quickly. To turn on browse info, bring up the project settings dialog via the “Project>Settings” menu or the “Build>Settings” menu. In the project settings dialog, go to the “General” category of the “C/C++” tab and check “Generate Browse info.” Then go to the “Browse Info” tab and check “Build browse info file.” After building, place your cursor over a class name in any text file with your project open and hit the F12 key. Use F12 religiously to learn what structs and classes contain and what they inherit from.
1.4 In TIA.cpp, rename CAboutDlg to CTIAAboutDlg. Boy, talk about picky…
1.5 Add specific copyright info to the about dialog resource. While editing the resources, make some decent (or at least non-MFC-default) icons to give the app that “distinguished, been around the block” look.
1.6 In CTIAMainFrame::OnCreate, add code to call m_wndToolBar.SetWindowText so that when it floats it has a caption in the title bar. (Hey MFC App Wizard writers: do this for us!)
In order to prove that this app works, I added some ultra-simple code to keep track of a list of items for each document. The views onto a document simply iterate this list in order to draw the contents of the document. To make things a little more interesting than b-flat (or plain vanilla), the items are rectangles with specific coordinates and color. So… enter TIAItems.cpp and .h. There is a base class from which all document items inherit and a single useful subclass which are the actual items we’ll use.
The vision for TIA is as follows:
- each document owns a list of items
- each document is responsible for creating and destroying its items
- each view is able to access the list of items from its associated document for drawing them
- documents and items are accessible through Automation
1.7 To support that vision, add the following code:
1.7.1 Add a CObList member variable to CTIADoc.
1.7.2 Add the file TIAItems.cpp to the project. (I wrote this one by hand—no wizardry here…)
1.7.3 Add methods to CTIADoc: AddTIARect, DeleteTIAItems, GetTIAItemList, Changed and ForceClose (also all written by hand…)
1.7.4 Call DeleteTIAItems from the document’s DeleteContents method.
1.7.5 Add code to CTIADoc::Serialize to save and load the list of items.
1.8 Now, in order to create some of these items through the UI and verify that save and reopen documents works, add some code to the view class. In order to spice things up even more, add three member variables in the view that control the color and the size of the rectangles to create. The user controls the location of the rectangles by where he or she clicks. Add m_rgb, m_nWidth and m_nHeight to the view along with a simple mechanism to produce a little variety. The simple variety enhancer included cycles through 4 (count ‘em, 4) colors and chooses one each time a new view is created. Call CTIADoc::AddTIARect from the LBUTTONUP handler in the view. Add some code to the view draw method to iterate the document’s list of items and draw them appropriately. Since we also have a “creation-color” associated with the view, we’ll draw a frame around the view with that color to give the user an indication of what color rectangle he’s going to get when he clicks in the view.
1.9 Now you should be able to compile, link and run; create documents, click around and create items; save and reopen documents and have a generally good MFC time playing around with little colored rectangles. If you’re following the tutorial from the ground up (as opposed simply to reading it and looking at the finished sample source code), now’s a good time to try running it.
1.10 Two more little details make this a perfect little MFC starter application. First, because I was negligent in the wizard and chose not to click the “Advanced” button, string resource ID 129 is not exactly as it should be. I had to modify it to be as follows:
"\nTIADoc\nTIA Document\nTIA Files (*.tia)\n.tia\nTIA.Document\nTIA Document"
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.
Now, let’s 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.
TIA’s object model will have only three objects in it: Application, Document and Rectangle. Figure 1 depicts this in standard UML static structure notation.
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 TIA’s 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 time—that kind of dependence is a bad thing to foster. As a developer, you need to understand what the code that’s being generated does. Otherwise, when there’s a problem, it becomes very difficult to figure out what the cause of the problem is. It’s 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 what’s 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 you’ll see BSTR in the drop down list of variable types, whereas other times you’ll 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 that’s 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. There’s essentially a one-to-one mapping with dual interface methods and it’s 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 isn’t 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:
long nLeft, long nTop, long nWidth, long nHeight, long nRGB
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, let’s 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 it’s 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 Don’t pollute your “Component Gallery” with this—it’s specific to this application
2.6 Add these properties and methods to app:
"" for new blank document
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 Don’t pollute your “Component Gallery” with this—it’s specific to this application
2.9 Add these properties and methods to CTIARectAut:
long nLeft, long nTop, long nWidth, long nHeight
long *pnLeft, long *pnTop, long *pnWidth, long *pnHeight
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, you’ve laid the groundwork for the simple object model previously illustrated in Figure 1.
OK, now you’re 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 TIA—look up the method that you’re interested in to see how it’s 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 app’s 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 shouldn’t 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, you’ll 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 we’ve made it this far, let’s take it one step further and add dual interfaces to each of the Automation objects.
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 don’t 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. MFC’s 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 doesn’t derive from IDispatch then it’s not dual, it’s 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, you’ll 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, don’t worry about all that proxy/stub stuff—COM will take care of you. Read Don Box to understand more about marshaling in depth.
Hint: Don’t declare two [out] parameters on a propget and make one of them the retval—MIDL doesn’t like this and it doesn’t 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 they’ll 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. There’s 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, it’s 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:
// 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);
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:
3.6.4 Then add the implementation macro for supporting ISupportErrorInfo.
// Implement ISupportErrorInfo for this dual interface:
3.6.5 Then add an implementation of each method declared in the dual interface.
STDMETHODIMP CTIADoc::XDual_TIA_Doc::get_Name(BSTR FAR* pbstrName)
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, you’re 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 CCmdTarget’s 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! You’ve 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…
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 there’s 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.
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)
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 that’s 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 types—oRectInternal 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 document’s AddTIARect method. The only way to destroy one is to destroy the document. The rectangles go away when the document’s 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. Here’s 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 can’t communicate with the real implementation beyond the implementation object’s 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++ object’s 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.
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 you’re doing something slow inside the method itself, adding dual interfaces is not going to buy you much in the way of performance improvement. However, it’s 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 you’ll see will vary depending on what the methods actually do. If they’re 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.
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, it’s nice not to have to redraw immediately—if you’re getting one call through Automation, you’re likely to get many calls. When a programmer writes a script against your object model, he’s 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 it’s true and we’re 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 view’s draw method will get called. It merely defers posting updates to the document’s views when Automation methods are the cause of the document change.
There’s 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 app’s 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.
The MFC specific advice regarding how to make the app invisible and how to close documents via Automation is no special advice—you 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 I’ve 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 you’ve previously worked through this problem. This article and the sample code attempt to protect you from the pain.
There’s a little more to do than calling ShowWindow—see CTIADoc::OnIdle to understand why… OnShowViews should only be called when the actual visible state of the document changes, not merely when the document’s frame window’s visible bit changes. Because we are allowing the app to be visible or invisible also, we need to take it’s 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 window’s visibility and reports it as its own.
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 it’s 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 it’s going to disturb the user to close a document out from under him or her.
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 doesn’t 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, wouldn’t it? I’m 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 uses—make your Type Library match it by making sure the odl file matches it.
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 you’d 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 application’s 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, there’s 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 doesn’t seem to handle the dependencies properly. I’m not sure I understand why because it’s 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 MIDL’s 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 I’ve 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 pointer’s *%!$^@ !* 0x00000000—you know it’s time for a “Rebuild All”…
It’s 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 10—one 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 computer’s registry…
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 it’s 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. It’s 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 you’ll be debugging MFC COM objects.
So get to work! What are you sitting there for? You’ve got some interfaces to implement…
The areas where TIA lacks, which I think would make good next steps for further tutorials are:
Step 4. Support IEnumVARIANT for the Application’s documents and for the Document’s rectangles. (Allows For/Each syntax usage in VB/A…)
Step 5. Add delete of rectangles—as it stands, once a rectangle is in there, it’s in there for good
Step 6. Add undo support
Step 7. Make embedding support work correctly—needs a real rectangle for items in the document
Step 8. Add DocObject support, too
Let me know if you’d like to see another step in this tutorial. Comments and feedback always welcome.
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 he’s not working on his day job at Visio Corporation, writing articles for CodeGuruon 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 [email protected].
- ACDUAL, the MFC sample app demonstrating how to add dual interfaces to existing MFC classes—search for it on your VC CD or on the web at http://www.microsoft.com. Also see MFC Technical Note 65.
- Inside OLE, Second Edition by Kraig Brockschmidt. Microsoft Press, Redmond, WA, 1995.
- Essential COM by Don Box. Addison Wesley, Reading, MA, 1998.
- Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. Addison Wesley, Reading, MA, 1995. The Adapter, Bridge, Decorator and Proxy patterns are all worth a look—you’ll find nuggets in each that remind you of the thin COM wrapper idea.
- Any issue of MSJ—columns are quite often useful; sometimes the main articles are good, too!
Back to Table of Contents