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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read