.
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.)
-
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. -
Select "New ATL Object" from the Insert menu. Allow the IDE to
add the required ATL support. -
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. -
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(); };
-
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()
-
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. -
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. -
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<>. -
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. -
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. -
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. -
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. -
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. -
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; }
-
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. -
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
-
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)
-
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. -
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;
- In the Project Settings, under C/C++ Language Settings, ensure the
-
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; }
-
To call this method, you’ll need to modify your client. This VB code works fine:
Set a = New AutoATLLib.Application a.ActiveDocument.Hello
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.
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.