Using ATL to Automate a MFC Application

.

The current release of the product I help develop exposes an Automation programming interface. Both the front end and the exposed COM objects are coded using MFC. As with any MFC-based COM object, our Automation objects rely on MFC's support for COM found inside CCmdTarget. When I first implemented these objects, I was discouraged by the amount of code I had to write to support fairly standard interfaces. It took a lot of work to implement a set of exposed objects/interfaces that resemble the standard Application-Documents-Document model found in Microsoft Office and other products. For the next release of our product we will use ATL to implement the exposed Automation objects. In this article I will explore the possibility of using ATL to implement COM interfaces on actual MFC derived classes.

Among the many headaches of using MFC to implement Automation objects is the fact that MFC support for event sourcing is built into COleControl, not CCmdTarget. Thus, if your CCmdTarget derived Automation object needs to source or sink events, you must implement the required interfaces yourself. (Actually, it is possible to swipe the MFC code implemented in COleControl, and add it to your own CCmdTarget derived class. Perhaps I'll describe a method for doing this in another article, if there is enough interest.) Of course, the ATL library provides default, reusable implementations of many core interfaces, including those related to event sourcing and sinking.

Personally, I also dislike the nested-class approach employed by MFC to implement multiple interfaces on a COM object. Code reuse suffers under this approach, and I feel it is more difficult to maintain than an inheritance approach (as utilized by ATL). The lack of support in MFC for dual interfaces is also painful, to say the least.

MSVC 6.0 introduced the ability to easily add support for ATL objects within an existing MFC application. COM objects are easily and rapidly coded using ATL, and offer many benefits over coding similar objects in MFC. Unfortunately, as generated by the MSVC IDE, these ATL COM objects are pretty much shielded from the MFC application itself. This is due to a convention employed by the IDE when it generates the object's method stubs. When the stub is written, an AFX_MANAGE_STATE(AfxGetStaticModuleState()) macro hooks up MFC to use state information that is not the same as used by the application running in the same module. (For more information on MFC state-handling, see Tech Note 58.) Thus, calling AfxGetApp() inside an ATL COM object method does not retrieve the "real" application that is running in the same module (in fact, no CWinApp object is returned at all) . The rational for this convention is likely due to issues of thread safety.

MFC-based objects (COM and otherwise) are only thread safe in that any particular object can only be safely manipulated on the same thread that created it. That is to say, you cannot create a CView on one thread then piddle with it from another thread. ATL-based COM objects are thread safe to the extent that the programmer that implemented the objects made them safe. Depending on the targeted use of the object, it may or may not be desirable to spend the effort in making it safely callable from multiple threads.

It would be undesirable for a free threaded ATL object to access and manipulate an MFC object from the wrong thread. The wrong thread being any thread on which the MFC object was not originally created. The only way to safely integrate two such objects is to ensure they always talk to each other on a single thread. This can be achieved using a STA (Single Threaded Apartment) model within the executable module. In fact, this is the only supported mode of operation for any MFC-based COM object.

In an executable, the threading model is enforced by the initial call to CoInitialize() or CoInitializeEx(). MFC employs AfxOleInit(), which ultimately calls CoInitializeEx() with the COINIT_APARTMENTTHREADED option. As long as we ensure that our ATL-based objects operate inside a STA (and ensure it is the same STA as the MFC application), we can make them operate in cooperation with the MFC objects also running in the module. If we are tricky, we might be able to create a half-breed object, which would be very useful in Automating our applications. For instance, it would be nice to expose an ATL implemented COM interface directly from the MFC CWinApp object, or from a CDocument or CView object.

In the event that you're beginning to yawn at all this prose, wake up. It's time to perform an experiment. The goal will be to wire up a MFC application that does not currently support automation, so that it exposes via Automation an Application object. The Application object is to be one in the same as the CWinApp derived object, but will use ATL instead of CCmdTarget support to implement it's COM interface. It won't be as easy as it sounds... (If you don't care to follow these steps, the resulting project is distributed with this article.)

  1. Start by generating a new MDI application using the MFC AppWizard. Call the project AutoATL, and accept all of the defaults in the Wizard (that is, do not select the "Automation" option). Build the project to get everything compiled.
  2. Select "New ATL Object" from the Insert menu. Allow the IDE to add the required ATL support.
  3. In the ATL Object Wizard, select "Simple Object", and specify the name "Application". On the attributes page, accept the Apartment threading model and Dual interface selections, and specify "No Aggregation" and "Support Connection Points".

    We now have what MSVC is willing to give us for free: a MFC application, and a COM object that is served from the same executable. Now, we'll attempt to rewire the CWinApp derived class, CAutoATLApp.

  4. Copy the following highlighted portions of the CApplication class header into the class header for CAutoATLApp, and change the occurrences of "CApplication" in the template arguments and macros to "CAutoATLApp", as shown:
    // CApplication
    class ATL_NO_VTABLE CApplication :
     public CComObjectRootEx<CCOMSINGLETHREADMODEL>,
     public CComCoClass<CAPPLICATION, &CLSID_Application>,
     public IConnectionPointContainerImpl<CAPPLICATION>,
     public IDispatchImpl<IAPPLICATION, 
            &LIBID_AutoATLLib &IID_IApplication,>
    {
    public:
     CApplication()
     {
     }
    
    DECLARE_REGISTRY_RESOURCEID(IDR_APPLICATION)
    DECLARE_NOT_AGGREGATABLE(CApplication)
    
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    
    BEGIN_COM_MAP(CApplication)
     COM_INTERFACE_ENTRY(IApplication)
     COM_INTERFACE_ENTRY(IDispatch)
     COM_INTERFACE_ENTRY(IConnectionPointContainer)
    END_COM_MAP()
    
    BEGIN_CONNECTION_POINT_MAP(CApplication)
    END_CONNECTION_POINT_MAP()
    // IApplication
    public:
    };
    
    
    // CAutoATLApp:
    class CAutoATLApp : public CWinApp, 
     public CComObjectRootEx<CCOMSINGLETHREADMODEL>,
     public CComCoClass<CAUTOATLAPP, &CLSID_Application>,
     public IConnectionPointContainerImpl<CAUTOATLAPP>,
     public IDispatchImpl<IAPPLICATION, 
            &LIBID_AutoATLLib &IID_IApplication,>
    {
    public:
     CAutoATLApp();
    
    DECLARE_REGISTRY_RESOURCEID(IDR_APPLICATION)
    DECLARE_NOT_AGGREGATABLE(CAutoATLApp)
    
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    
    BEGIN_COM_MAP(CAutoATLApp)
     COM_INTERFACE_ENTRY(IApplication)
     COM_INTERFACE_ENTRY(IDispatch)
     COM_INTERFACE_ENTRY(IConnectionPointContainer)
    END_COM_MAP()
    BEGIN_CONNECTION_POINT_MAP(CAutoATLApp)
    END_CONNECTION_POINT_MAP()
    
    // Overrides
     //{{AFX_VIRTUAL(CAutoATLApp)
     public:
     virtual BOOL InitInstance();
     virtual int ExitInstance();
     //}}AFX_VIRTUAL
    
    // Implementation
     //{{AFX_MSG(CAutoATLApp)
     afx_msg void OnAppAbout();
     //}}AFX_MSG
     DECLARE_MESSAGE_MAP()
    
    private:
     BOOL m_bATLInited;
    
    private:
     BOOL InitATL();
    };
    
  5. We're done with the CApplication class, so get it out of the way by removing the files (Application.cpp, Application.h) from the project. This can be done from the File View in the project workspace window. Then, in AutoATL.cpp, remove the #include for "Application.h", and update the object map:
    BEGIN_OBJECT_MAP(ObjectMap)
    OBJECT_ENTRY(CLSID_Application, CAutoATLApp)
    END_OBJECT_MAP()
    
  6. Attempt a compile (it will fail...)

    The compile fails with 22 errors and 47 warnings because we're doing something that we were not really supposed to do. Our code attempts to meld a CWinApp with a bunch of ATL classes by brute force. Unfortunately, the MFC CCmdTarget derived CWinApp is incompatible with the ATL CComObjectRootEx<>, specifically in that there are obvious naming conflicts. Examine the first error:

    : error C2385: 'CAutoATLApp::InternalAddRef' is ambiguous
    
    : warning C4385: could be the 'InternalAddRef' in base 
      'CCmdTarget' of base 'CWinThread' of base 'CWinApp' of 
       class 'CAutoATLApp'
    
    : warning C4385: or the 'InternalAddRef' in base 
      'CComObjectRootEx<class ATL::CComSingleThreadModel>' of 
      class 'CAutoATLApp'
    

    What joy!! The ATL developers in Redmond liked the MFC implementation so much, they adopted the same naming scheme. I'll tell you right now, there are five naming conflicts that will affect us here. In addition to the functions InternalAddRef(), InternalRelease(), and InternalQueryInterface(), the data members m_dwRef and m_pOuterUnknown conflict as well.

    Naming conflicts are typically resolved by using namespaces, but I have another trick in mind.

  7. Open the project's stdafx.h file, and add the following five lines before the line that #includes atlbase.h:
    #define m_dwRef                m_dwRefAtl
    #define m_pOuterUnknown        m_pOuterUnknownAtl
    #define InternalQueryInterface InternalQueryInterfaceAtl
    #define InternalAddRef         InternalAddRefAtl
    #define InternalRelease        InternalReleaseAtl
    
    #include <atlbase.h>
    

    ATL is a template library, and so it's implementation exists inside header files included by the modules that use it. We can use the preprocessor to rename symbols inside the ATL headers before the ATL code is ever compiled. This is what we've done here: our conflicting names are renamed so they no longer conflict.

  8. Attempt to compile again. It will still fail, but...

    Note that this time our naming conflict errors are gone. In their place are 2 errors and 12 warnings all focussed on the same line in AutoATLApp.cpp:

    CAutoATLApp theApp;
    
    : error C2259: 'CAutoATLApp' : cannot instantiate abstract 
      class due to following members:
    

    If you've written ATL objects before, you will understand what is happening here. Our component is incomplete; it can only be instantiated by wrapping it inside a CComObject<> (or one of it's cousins), because the IUnknown methods QueryInterface(), AddRef(), and Release() are virtual and are implemented by the CComObject<> family of classes. As this object is a global object, we'll actually use CComObjectGlobal<>.

  9. Change the global declaration for theApp to match the line below, then compile:
    CComObjectGlobal<CAUTOATLAPP> theApp;
    

    Voila, no errors! To celebrate, run the application and see it run normally.

    Unfortunately, we're not done yet. No errors in the compile phase doesn't mean we are without problems (how many times have you wished that were true?) The (perhaps not so obvious...) problem is that although a client application could create instances of our Application object, none of these instances would be the same as the globally instantiated object "theApp" declared in our AutoATL.cpp module. What we're after in this case is a singleton object that is unique to the process. Allowing two or more different CAutoATLApp objects to live simultaneously is unacceptable.

    My apologies, but the steps to take care of this little problem are a bit involved. Please follow along and I'll do my best to explain.

  10. What we're after is a singleton Application object. ATL permits objects to be singletons by adding a single line to the class declaration:
    class CAutoATLApp : public CWinApp,
    	public CComObjectRootEx<CCOMSINGLETHREADMODEL>,
    	public CComCoClass<CAUTOATLAPP, &CLSID_Application>,
    	public IConnectionPointContainerImpl<CAUTOATLAPP>,
    	public IDispatchImpl<IAPPLICATION, 
             &LIBID_AutoATLLib &IID_IApplication,>
    {
    public:
    	CAutoATLApp();
    
    DECLARE_CLASSFACTORY_SINGLETON(CAutoATLApp)
    

    This tells ATL to implement the class factory for our object such that it will create only one instance of the object, and return references to that object to all interested parties.

  11. The object we've globally instantiated to be our application object (step 9) is not created using the class factory, however. So, it's going to have to go: delete the line.

    We now have a perplexing problem. COM and ATL were both previously initialized in our CAutoATLApp's OnInitInstance() function, which will never be called because we haven't instantiated a CAutoATLApp object. Part of the COM initialization that took place was the registration of our class factory, and the creation of our singleton object. Somehow we need to call these same routines early in the module startup code so that our singleton CAutoATLApp object can get created.

    When the IDE added ATL support to our project (step 2), it added the global object _Module. _Module is of type CAutoATLModule, derived from CComModule, and exists primarily to serve up class factories for the module. It is also going to perform the COM and ATL initialization we need. We're going to alter it so that it automatically initializes everything, without being told to do so first.

  12. In stdafx.h, alter the class definition for CAutoATLModule to match the following:
    class CAutoATLModule : public CComModule
    {
    public:
     bool m_bATLInited;
     CAutoATLModule();
     ~CAutoATLModule();
    
     LONG Unlock();
     LONG Lock();
     LPCTSTR FindOneOf(LPCTSTR p1, LPCTSTR p2);
     DWORD dwThreadID;
    };
    

    Then, in the AutoATL.cpp file, add code for the new constructor and destructor: (You can copy most of the code directly from the InitATL() and ExitInstance() members of CAutoATLApp, but watch for my changes:

    
    CAuto3Module::CAuto3Module()
    {
     m_bATLInited = TRUE;
    
     // STA apartment
     HRESULT hRes = CoInitialize(NULL);
     if (FAILED(hRes))
      exit(1);
    
     Init(ObjectMap, GetModuleHandle(NULL));
     dwThreadID = GetCurrentThreadId();
    
     //this line necessary for _ATL_MIN_CRT
     LPTSTR lpCmdLine = GetCommandLine(); 
     TCHAR szTokens[] = _T("-/");
    
     BOOL bRun = TRUE;
     LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
    
     while (lpszToken != NULL)
     {
    
      if (lstrcmpi(lpszToken, _T("UnregServer"))==0)
      {
       UpdateRegistryFromResource(IDR_AUTO3, FALSE);
       UnregisterServer(TRUE); //TRUE means typelib is unreg'd
       bRun = FALSE;
       break;
      }
    
      if (lstrcmpi(lpszToken, _T("RegServer"))==0)
      {
       UpdateRegistryFromResource(IDR_AUTO3, TRUE);
       RegisterServer(TRUE);
       bRun = FALSE;
       break;
      }
    
      lpszToken = FindOneOf(lpszToken, szTokens);
     }
    
     if (!bRun)
     {
      Term();
      CoUninitialize();
      exit(0);
     }
    
     hRes = RegisterClassObjects(CLSCTX_LOCAL_SERVER, 
                                 REGCLS_MULTIPLEUSE);
    
     if (FAILED(hRes))
     {
      CoUninitialize();
      exit(1);
     }	
    }
    
    CAuto3Module::~CAuto3Module()
    {
     if (m_bATLInited)
     {
      RevokeClassObjects();
      Term();
      CoUninitialize();
     }
    }
    

    These changes have the effect of performing the initialization we're after immediately when the _Module object is constructed. Our Application class factory will be created, and it will create and register our singleton Application object. If you're interested, go ahead and figure out the magic that MFC uses to locate the CWinApp object, regardless of how it was created. Like most things in MFC, it's not so magic after you see it in action...

    Now that _Module is self-initializing, we need make minor alterations to CAutoATLApp.

  13. Remove the InitATL(), ExitInstance(), and m_bATLInited members of CAutoATLApp (don't forget to remove them from the header too!) Then remove the call to InitATL() from InitInstance(), replacing it with the following:
    BOOL CAutoATLApp::InitInstance()
    {
     if (false == _Module.m_bATLInited)
      return FALSE;
    
     _Module.UpdateRegistryFromResource(IDR_AUTOATL, TRUE);
     _Module.RegisterServer(TRUE);
    
     AfxEnableControlContainer();
    
     // ...
    

    That does it! In order to test our efforts lets add a method we can call from a client application.

  14. Expand the CAutoATLApp class entry in the Class View tree. Right click on the "spoon" icon and select "Add Method". Enter "Beep" as the method name, then click OK. Expand the "spoon" icon, and double click "Beep" to go to the function shell created for us by the IDE. Add a call to perform the beep:
    STDMETHODIMP CAutoATLApp::Beep()
    {
     AFX_MANAGE_STATE(AfxGetStaticModuleState())
    
     ::Beep(1000,1000);
    
     return S_OK;
    }
    
  15. Compile the application and run it. Running the application will have the effect of registering the server and its Application object in the system registry.
  16. Using Visual Basic, or whatever other tool you're comfortable with, write a simple client to invoke the Beep() method. The VB code would look like this:
    Dim a As AutoATLLib.Application
    Set a = New AutoATLLib.Application
    a.Beep
    
  17. Sixteen steps, whew! But now we have a program we can play with. Go ahead and run your client, and ensure that the program beeps as expected.

    There are still some pretty major questions that need addressing, however. The first we'll look at concerns the AFX_MANAGE_STATE(AfxGetStaticModuleState()) macro in the Beep() method. If you add a line of code following the macro to retrieve the CWinApp pointer, you'll see in the debugger that the return value is NULL.

    STDMETHODIMP CAutoATLApp::Beep()
    {
     AFX_MANAGE_STATE(AfxGetStaticModuleState())
    
     CWinApp* pApp = AfxGetApp();
     // pApp is NULL!
    
     ::Beep(1000,1000);
     return S_OK;
    }
    

    Of course, in the context of Beep(), the this pointer is the CWinApp, but that's not really the point. The point is that the macro blew away our capability to get at the state information the application uses when not in the context of a COM object method. Simply removing this macro call is not the answer either, because for many parts of MFC to work properly the context information needs to be correct.

    The reason the MFC context is out of whack in the first place is a bit complex. A very simplistic explanation is that COM has created a hidden window that retrieves messages from the thread message queue, just as MFC windows do. When this window receives a message from COM asking it to make a method call on an object, all MFC code is out of scope (because a non-MFC window is servicing the message queue), and the MFC context is undefined.

    Fortunately, many MFC objects store a pointer to the context information that is relevant to the object. Because our method call is a member of the MFC CWinApp class, we simply need to ask MFC to use the state information in our data member m_pModuleState:

    STDMETHODIMP CAutoATLApp::Beep()
    {
     AFX_MANAGE_STATE(m_pModuleState)
    

    This is essentially what MFC COM objects must do as well, although they use a special METHOD_PROLOGUE macro that also sets up the pThis pointer. I suggest defining and using the following macro:

    #define METHOD_PROLOGUE_ATL AFX_MANAGE_STATE(m_pModuleState);
    

    If you define the macro in stdafx.h (or some other global header), it could make your life much simpler. Just remember to use the macro at the top of any method of a MFC/ATL hybrid object.

    The next question we need to tackle is that of object creation and object lifetime. So far in our experiment, the single Application object will live as long as the module is loaded. And the module will remain loaded as long as there are clients who hold interfaces on the Application object. Fortunately MFC never attempts to delete the object (because it used to be a global stack based object). Note that we also were able to force the creation of the Application object through our class factory.

    But what will happen if we attempt to automate a MFC class that has a less stable lifetime? Take a CDocument, for example. CDocument's are allocated in the bowels of MFC, and destroy themselves in the OnCloseDocument() method. Fortunately, MFC is extensible enough that we can probably work around these problems. Let's attempt to give our CAutoATLDoc class a COM interface using ATL.

    Follow the same steps we took to add the ATL code to the CAutoATLApp object: * Insert a new ATL object named Document, and move the ATL code into the CAutoATLDoc class header. * Remember to update the references in the ATL template classes and macros from CDocument to CAutoATLDoc (if you forget, you could potentially end up with a bunch of confusing compiler errors, as our CDocument class will conflict name-wise with MFC's CDocument.) * Update the object map in AutoATL.cpp, and to remove the #include for "Document.h" * Remove the Document.h and Document.cpp files from the project.

    When you try to compile the resulting CAutoATLDoc class, you'll receive similar errors to the ones we saw earlier in step 8. The MFC macro, IMPLEMENT_DYNCREATE is expanding into code that tries to allocate a CAutoATLDoc object directly, without wrapping it in a CComObject<> class. Hence the "cannot instantiate abstract class" errors.

    But wait! These errors expose a potential solution to our dilemma of how to properly create and initialize our ATL COM object. MFC objects derived from CObject are typically created by a call to CObject::CreateObject(). CreateObject() is virtual, and can be overridden so that the programmer can control how the object is constructed. For instance, you may override CreateObject() if you want to allocate the object from an alternative heap. We can override CreateObject() to properly create and initialize the ATL COM object.

  18. The easy way to do this will be to create new macros to use instead of DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE. Inside stdafx.h, add the following macros:
    #define DECLARE_DYNCREATE_ATL(class_name) \
     DECLARE_DYNCREATE(class_name)
    
    #define IMPLEMENT_DYNCREATE_ATL(class_name, base_class_name) \
     CObject* PASCAL class_name::CreateObject() \
     { 
      CComObject<CLASS_name>* pObj = NULL; \
      HRESULT hr = CComObject<CLASS_name>::CreateInstance(&pObj); \
      if (FAILED(hr)) { AfxThrowOleException(hr); } \
       pObj->InternalAddRef(); \
       return pObj; \
     } \
     IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, \
      class_name::CreateObject)
    
  19. Replace the DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros in the AutoATLDoc.h and AutoATLDoc.cpp files with their new counterparts. The project should compile now without any errors.

    What we've accomplished is pretty cool, really. When MFC needs to allocate a new document, it will use our CreateObject() method which will properly create and initialize the ATL COM portion of the object. Note that the CreateObject() method increments the reference count of the object - this is important because MFC doesn't know that the object will free itself if it's reference count ever reaches zero. Which brings up yet another issue concerning the lifetime of the object. But we'll look at that later.

    Although we now have an externally creatable Document object, it is likely that a client application would like to gain access to already existing Document objects. Let's add this capability by implementing an ActiveDocument property on the Application object.

  20. Just as we added the Beep() method in step 14, add an ActiveDocument property to the Application object. This time, select "Add Property", and specify "IDocument*" as the property type, and "ActiveDocument" as the name. Uncheck the "Put Function" checkbox, as this will be a read-only property. Double click the "get_ActiveDocument" method and alter the stub as follows:
    STDMETHODIMP CAutoATLApp::get_ActiveDocument(IDocument **pVal)
    {
     METHOD_PROLOGUE_ATL
    
     CMDIChildWnd* pChild = 
      ((CMDIFrameWnd*)m_pMainWnd)->MDIGetActive();
    
     if (!pChild) 
      return E_FAIL;
    
     ASSERT_KINDOF(CMDIChildWnd, pChild);
    
     CComObject<CAUTOATLDOC>* pDoc = 
      dynamic_cast<CCOMOBJECT<CAUTOATLDOC>*>
      (pChild->GetActiveDocument());
    
     return pDoc->QueryInterface(IID_IDocument, 
                                 reinterpret_cast<void**>(pVal));
    }
    

    Now, perform two more pieces of housekeeping:

    • In the Project Settings, under C/C++ Language Settings, ensure the RTTI checkbox is checked. This is necessary because of my use of the dynamic_cast<> operator.
    • Forward declare the IDocument interface in the .IDL file, like this:
      import "oaidl.idl";
      import "ocidl.idl";
      
       interface IDocument;
      
      
  21. So that we can test this new functionality and see some results, let's add a method to the Document object. We could make it beep again, but perhaps a message box is better:
    STDMETHODIMP CAutoATLDoc::Hello()
    {
     METHOD_PROLOGUE_ATL
    
     ::MessageBox(NULL, 
                  GetTitle(), 
                  "Hello World", 
                  MB_OK | MB_ICONEXCLAMATION);
    
     return S_OK;
    }
    
  22. To call this method, you'll need to modify your client. This VB code works fine:
    Set a = New AutoATLLib.Application
    a.ActiveDocument.Hello
    

In order for this code to work, the application will have to be running, and a document needs to be open. Otherwise, the ActiveDocument property will fail because there will be no active document!

The remaining question then is how to properly ensure the object lives out its life to the proper extent. COM objects typically destroy themselves when the reference count reaches zero. But our hybrid MFC/ATL object doesn't follow all of the COM rules. For example MFC is free to manipulate the object, and potentially destroy the object, via the pointer it received when the object was created. In this case where we've automated a CDocument, we can breath a little easy because the only place where a CDocument is ever deleted by MFC is in the CDocument's OnCloseDocument() method. (If I happen to be wrong in that statement, I'm sure someone will let me know!) Since we are able to override OnCloseDocument(), we have the ability to keep the object alive in the event there are outstanding references even though MFC has determined the document should go away. Another possibility would be to set the m_bAutoDelete member of CDocument to FALSE. This undocumented member is typically used to keep a document around even if all it's views have been closed.

Of course, if you determine that OnCloseDocument() is called too late in the document destruction process, you could override CanCloseFrame(). CanCloseFrame() is called when the user is interactively attempting to close a document. A third option might be to play with overloading the delete operator for the class.

The problem that all this discussion is trying to resolve is that the user of the application, or MFC itself, might decide to make the Document object go away while there remain outstanding references on the Document object via Automation. It is because the object doesn't have full decision making power of when it is to be destroyed that we have an issue. Similar issues will be encountered when you attempt to automate a CView in this manner.

Interestingly enough this is an issue to be dealt with when using plain old MFC for automating an application as well. I solved the problem in our current application by creating proxy COM objects that have lifetimes separate from the MDI objects they represent. This solution works, but is more difficult to maintain than a set of MDI objects that expose their own COM interfaces.

Here are a couple ideas for what else could be done next with this sample:

  • Add event-sourcing capability to the Document and/or Application object. Note that if you followed my directions closely, you already have this capability and only need to define an event.
  • Add a strictly ATL COM object "Documents" to contain interface pointers to the set of open documents in the application. Implement an event-sink in this object that removes from the container any Document object that fires a Close event.

I don't mean to cop out early from the party, but I think I've given you enough to whet your appetite. I would be very interested in hearing your feedback, and what your views of this technique are.

Downloads

Download source - 40 KB


Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • VMware vCloud® Government Service provided by Carpathia® is an enterprise-class hybrid cloud service that delivers the tried and tested VMware capabilities widely used by government organizations today, with the added security and compliance assurance of FedRAMP authorization. The hybrid cloud is becoming more and more prevalent – in fact, nearly three-fourths of large enterprises expect to have hybrid deployments by 2015, according to a recent Gartner analyst report. Learn about the benefits of …

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

Most Popular Programming Stories

More for Developers

RSS Feeds