Tag Archives: MDI

SubclassWindow() method issues in projects base on MFC Feature Pack

The Problem
Trying to paint a background image into client area of a MDI application build in VC++ 6.0 to VC++ 2005 IDE it’s not a difficult task.
In case you need, you can find easily good references. For instance, there are two references from Microsoft (KB129471 and KB103786) and one I prefer: a FAQ wrote by a friend of mine.

Unfortunately things are changing radically in case you’re following the same steps in a Visual C++ IDE that has MFC Feature Pack support. If you’re building from the scratch a VC++ 2008/VC++ 2010 a MDI project that has MFC Feature Pack support and you’re trying to apply sub-classing steps, you will have a big surprise in the moment you’re starting your application in debug mode. Effectively your application will crash in the moment you are trying to call SubclassWindow() in CMainFrame::OnCreate().

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
  if (CMDIFrameWndEx::OnCreate(lpCreateStruct) == -1)
    return -1;

  // ---- code ---

  // BANG! IN VS 2010 or VS 2008 with MFC Feature Pack
  // m_wndMDIClient.SubclassWindow(m_hWndMDIClient);
  // where m_wndMDIClient is an instance of CMDIClientWnd
  // (http://support.microsoft.com/kb/129471)

  // ------ code ----

  return 0;
}

Problem details
Starting with MFC Feature Pack CMDIFrameWndEx is the new CMainFrame’s parent class instead of CMDIFrameWnd and the problem acts inside of Attach() method:
because CWnd::FromHandlePermanent(HWND hWnd) looks up into a permanent handle map and in returns existing CWnd pointer.

Wnd* PASCAL CWnd::FromHandlePermanent(HWND hWnd) {
  CHandleMap* pMap = afxMapHWND();
  CWnd* pWnd = NULL;
  if (pMap != NULL) {
    // only look in the permanent map - does no allocations
    pWnd = (CWnd*)pMap->LookupPermanent(hWnd);
    ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd);
  }
  return pWnd;
}

CHandleMap is the wrapper that implements the mapping mechanism between the pointers of MFC wrapped classes and the Windows object handles. Internally, this class has to dictionaries (m_permanentMap and m_temporaryMap) implemented as CMapPtrToPtr, m_nHandles – the number of handles, m_nOffset – the offset of handles in the object and it has a m_pClass pointer of CRuntimeClass (a run time class associated with all MFC classes).
In case you’re interest in more details, you can find more information here.

We have a pointer to a CHandleMap instance that is assigned with the returned pointer of a handle map returned by afxMapHWND(). The returned pointer pWnd it’s assigned with the result returned by pMap->LookupPermanent(hWnd). LookupPermanet() effectively search into a the permanent hash map for exiting HANDLEs and in our case it find it.

inline CObject* CHandleMap::LookupPermanent(HANDLE h) {
  return (CObject*)m_permanentMap.GetValueAt((LPVOID)h);
}

where

void* CMapPtrToPtr::GetValueAt(void* key) const {
  // find value (or return NULL -- NULL values not different as a result)

  ENSURE(this);

  if (m_pHashTable == NULL)
    return NULL;

  UINT nHash = HashKey(key) % m_nHashTableSize;

  // see if it exists
  CAssoc* pAssoc;
  for (pAssoc = m_pHashTable[nHash]; pAssoc != NULL; pAssoc = pAssoc->pNext) {
    if (pAssoc->key == key)
      return pAssoc->value;
  }
  return NULL;
}

If the item having nHash key was found into m_pHashTable then the condition if (pAssoc->key == key) is TRUE because the attribute m_hWndMDIClient of CMDIFrameWnd is used yet.
So, effectively what LookupPermanent() has found in m_permanentMap map is m_hWndMDIClient. And because pMap->SetPermanent(m_hWnd = hWndNew, this) is one of the next call into Attach() method those ASSERTs are a must.
Even if those ASSERT() calls from Attach() are available only in debug mode (because of ASSERT() macro behavior) a release build would not save the situation. Soon or later you’ll get conflicts and the application will crash.

Trying to find where this has happened is not so complicated as long as we take in consider our CMainFrame class it’s derived from CMDIFrameWndEx a class that extends CMDIFrameWnd. If we are looking into CMDIFrameWndEx class implementation (AfxMDIClientAreaWnd.cpp) we will see that into this class SubclassWindow() method it’s called jet:

BOOL CMDIFrameWndEx::OnCreateClient(LPCREATESTRUCT lpcs,
                                    CCreateContext* pContext) {
  if (!CMDIFrameWnd::OnCreateClient(lpcs, pContext)) {
    return FALSE;
  }

  if (m_bDoSubclass) {
    m_wndClientArea.SubclassWindow(m_hWndMDIClient);  // this is it!
  }

  return TRUE;
}

Subclassing a CWnd derived instance that has already a mapped HWND item is an error and these ASSERTs try to avoid this from development moment. Having two different CWnd-derived objects with the same HWND is not possible – the only exception is CDC instances that have 2 HWNDs (m_hDC and m_hAttribDC).
Related to my issue, according to Steve Horne from Microsoft, “anything that uses the MFC Feature Pack will be using CMDIFrameWndEx which is a very different beast. It has this feature built it as you’ve found out”.
The worst part is that “If you were able to subclass the Ex client area, you’d probably end up breaking a lot of the FluentUI features.”
The VS 2008 / VS 2010 wizard generates and use a lot of Feature Pack FluendUI items.

A bad solution
An approach might be trying to adapt sub-classing idea directly into CMainFrame class. So, the steps might be:

  • No CMDIClientWnd instance is needed (as in existing tutorials). So no more SubclassWindow() call in CMainFrame::OnCreate().
  • Handle WM_ERASEBKGND, WM_SIZE and WM_PAINT on CMainFrame.
void CMainFrame::OnPaint() {
  CWnd* pWnd =
      CWnd::FromHandle(m_hWndMDIClient);  // returns a MFC object pointer
  // for the given handle if it is valid
  CPaintDC dc(pWnd);

  // the rest of the code
}

CWnd::FromHandle() acquires a pointer to an MFC object pointer from CHandleMap via afxMapHWND().

BOOL CMainFrame::OnEraseBkgnd(CDC* pDC) {
  return FALSE;
}

void CMainFrame::OnSize(UINT nType, int cx, int cy) {
  CMDIFrameWndEx::OnSize(nType, cx, cy);

  Invalidate();
}

At the very first time everything looked nice. But unfortunately I have to admit Steve Horne’s observations. In different situations (most on resizing or moving messages) some of the FluentUI items were not correctly painted (some Ribbon items painting issues – different cases).

So, a better solution is needed.

A good but not perfect solution
In my research, for projects base on MFC Feature Pack, there is no perfect solution for this issue. I mean something similarly with the good solutions that I mentioned in the beginning of this article but acts fine until the first IDE that use MFC Feature Pack.
As we have seen on top trying to subclass a window with an already mapped is not a good idea.
The solution is based on Joseph M. Newcomery’s idea, a well-known book writer and Microsoft Visual C++ MVP. Joe proposes “temporary” remapping only for the case we need – in my case painting actions. For the rest of the action the mapping process inside of framework continues in the classic way. It’s a “gross and ugly” solution but until having a better solution from Microsoft or others I consider it fine for my needs.

  • First step is to define a class CMDIClientWnd derived from CWnd and add WM_PAINT and WM_ERASEBKGND handle methods.
BOOL CMDIClientWnd::OnEraseBkgnd(CDC* pDC) {
  return FALSE;  // let OnPaint() to paint, only
}

void CMDIClientWnd::OnPaint() {
  CPaintDC dc(this);

  // effective painting stuff
}
  • Catch the WM_PAINT message in CMainFrame via PreTranslateMessage() before the message is dispatched for execution and calling our redraw method.
BOOL CMDIClientWnd::OnEraseBkgnd(CDC* pDC) {
  return FALSE;  // let OnPaint() to paint, only
}

void CMDIClientWnd::OnPaint() {
  CPaintDC dc(this);

  // effective painting stuff
}

Here is the RedrawClientArea() public method.

void CMainFrame::RedrawClientArea() {
  CMDIClientWnd wnd_cl;

  wnd_cl.Attach(m_wndClientArea.Detach());
  wnd_cl.Invalidate();
  wnd_cl.UpdateWindow();
  m_wndClientArea.Attach(wnd_cl.Detach());
}

So we create locally an instance of CMDIClientWnd and we attach it internally to ChandleMap::m_permanetMap via Attach(), not before detaching m_wndClientArea (an CMDIClientAreaWnd instance, attribute in CMDIFrameWndEx and as we have seen before it subclass the CMDIFrameWndEx in CMDIFrameWndEx::OnCreateClient()).

The idea is that our CMDIClientWnd instance temporary replace m_wndClientArea instance of CMDIClientAreaWnd right before effective WM_PAINT message is dispatched via PreTranslateMessage().

  • Include your new class header (ex. MDIClientWnd.h) in MainFrm.cpp and call RedrawClientArea() in CMainFrame::OnSize().
void CMainFrame::OnSize(UINT nType, int cx, int cy) {
  CMDIFrameWndEx::OnSize(nType, cx, cy);

  RedrawClientArea();  // repaint on WM_SIZE
}
  • If the child frames window is not tabbed style (when all client area is hidden) and the client area is still visible than we have to call RedrawClientArea() method from WM_MOVE and WM_SIZE handler of CChildFrame and we have to include MainFrm.h into ChildFrame.cpp.
void CChildFrame::OnMove(int x, int y) {
  CMDIChildWndEx::OnMove(x, y);

  CMainFrame* pMainFrame = (CMainFrame*)GetParentFrame();
  ASSERT(pMainFrame);

  pMainFrame->RedrawClientArea();
}

void CChildFrame::OnSize(UINT nType, int cx, int cy) {
  CMDIChildWndEx::OnSize(nType, cx, cy);

  CMainFrame* pMainFrame = (CMainFrame*)GetParentFrame();
  ASSERT(pMainFrame);

  pMainFrame->RedrawClientArea();
}
  • Additionally, in order to make sure the painting message is received by main frame at application’s starting moment and your image is correctly painted from the beginning, please call pMainFrame->Invalidate() after pMainFrame->UpdateWindow() in InitInstance() method of your application class. Otherwise, if your application it’s starting with no opened document (for instance new document), your picture will appear only in the moment a WM_PAINT message is generated in CMainFrame (for instance when you resize your application, select the menu, etc).

A disadvantage of this approach is that the interest message (WM_PAINT) is not handled inside the class of m_wndClientArea, but the good point is that the rest of the messages are left at the correct class of the framework and will work correctly.
Demo application (3862 downloads)

Versionable Object’s Serialization in MDI applications

This article represents a follow-up of the last article, “Versionable Object’s Serialization using MFC in non Document View applications”. In that article I presented to you a way to solve incompatibility issues between different file versions of the same application, based on MFC serialization into a dialog base application.
But, the dialog base applications are not the best way to use and apply MFC serialization.
Applications base on document view architecture (MDI or SDI) are the best solution when we want to develop MFC application with serialization support.
Document View MFC architecture offers support for automatic save and load document to/from a file on a storage area, using a serialization mechanism. MDI (Multiple Document Interface) and SDI (Single Document Interface) application offers default serialization basic mechanism.

SerAddressBookMDI Main Window

The serialization is customizable. It’s important to define the right binary elements format, file version and element count. Finally, we have to complete the serialize method.
Into a document view application some document class’s methods are mapped over New, Open, Save and Save As items, available in File menu. The application’s user can use these commands in order to create or open files, tracking document status changes and serialize data to/from a file.
MDI applications create a CDocument derived class instance for each open document. SDI applications reuse the same single CDocument derived class instance for each open file.
In a MDI application CDocument class and the classes derived from this are responsible with internal objects serialization control. This class tracks each change that appears in our document. In this way, our application knows that some changes have been made when we accidentally want to close the application, without saving last changed data.
When a document is loaded, a CArchive instance is created for reading file internal data. When we create a new document, a CArchive store instance is created and this instance is used for store to a file process.
CArchive routines are strongly optimized in order to provide a viable store/load mechanism, even if we are serializing a huge number of small items.

In my demo application, I used the same idea as in my last article: an address book with two versions.

In current application the serialization process it’s very different then the old application. The serialization process is realized by a CDocument class instance that interaction with the rest application classes. CAddressBook class place was taken by the document class CSerAddressBookMDIDoc.

In a real application it’s recommended to use unique identifier (UID) in order to “detect” the right object. For simplicity, in my demo application this unique identifier was defined “name” attribute. For instance, I’m using it for a contact update process.

Document class – CSerAddressBookMDIDoc

The interface of document class looks like this:

class CSerAddressBookMDIDoc : public CDocument
{
  protected: // create from serialization only
  CSerAddressBookMDIDoc();
  DECLARE_DYNCREATE(CSerAddressBookMDIDoc)

  // Attributes
  public:

  // Operations
  public:

  // Overrides
  public:
  virtual BOOL OnNewDocument();
  virtual void Serialize(CArchive& ar);

  void SetFileVersion(UINT nFV) { m_nFileVersionSchema = nFV; }
  UINT GetFileVersion() const { return m_nFileVersionSchema; }

  const ContactList& GetContacts() const {return m_cContactsList;}
  bool AddContact(const Contact& contact);
  bool RemoveContact(const CString& firstname, const CString& lastname);
  POSITION FindContact(const CString& firstname) const;
  bool FindContact(const CString& firstname, Contact& contact) const;
  bool UpdateContact(const CString& firstname, Contact& contact);

  // Implementation
  public:
  virtual ~CSerAddressBookMDIDoc();
  BOOL DoSave(LPCTSTR lpszPathName, BOOL bReplace); // need to override this
  virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);

  #ifdef _DEBUG
  virtual void AssertValid() const;
  virtual void Dump(CDumpContext& dc) const;
  #endif

  ContactList m_cContactsList;

  // Generated message map functions
  protected:
  DECLARE_MESSAGE_MAP()

  private:
  INT m_nFileVersionSchema;
};

As you can see, this time I’m using DECLARE_DYNCREATE() macro. This macro allows dynamic document objects runtime creation (a MDI application’s requirement).
In this class I reused some CAddressBook’s methods. These methods handle objects from m_cContactsList list.
ContactList is an alias for our Contacts MFC list:
[cpp]typedef CList ContactList;[/cpp]

The serialization data method of this document class is listed down:

void CSerAddressBookMDIDoc::Serialize(CArchive& ar)
{
  if (ar.IsStoring()) // storing
  {
    ar << m_nFileVersionSchema; // write the number of contacts
    ar << (int)m_cContactsList.GetCount(); 
    Contact::SetCurrentContactVersion(m_nFileVersionSchema); // write all the contacts 
    POSITION pos = m_cContactsList.GetHeadPosition(); 
    while(pos != NULL)
    { 
      Contact contact = m_cContactsList.GetNext(pos);
      contact.Serialize(ar); 
    } 
  } else { // loading
    ar >> m_nFileVersionSchema;

    m_cContactsList.RemoveAll();

    int count = 0;
    ar >> count;

    // read the number of contacts
    while (count-- > 0)
    {
      Contact contact;
      contact.Serialize(ar);

      m_cContactsList.AddTail(contact);
    }
  }
  
  UpdateAllViews(NULL);
}

This method read (load) or write (store) serialized Contact class’s object using a CArchive object at runtime. If the code flow runs over true branch all information is saving from our list to our file. If code flow choose else branch it means that we are loading an existing file and all file data is loaded in our list.

In order to store data in a file, initially I save file version (m_nFileVersionSchema) and items count. Then I iterate over all m_cContactsList items (Contact type item) and I serialize this data in order to store in my new file.

If I want to load data from a file, I am reading file version, I clean my list, I get Contact stored items count and as long as this count variable value is positive I serialized with load flag all Contact file data.
All serialized Contact entities go to m_cContactsList list. Each time we want to display our files data we have to iterate over this list.

Internal serialized class – Contact

As you have seen, in CSerAddressBookMDIDoc::Serialize() method, for both situations (store/load) a new Contact instance is created and this object is passed to Contact::Serialize() for load/store operation.
The serializing Contact items method looks like this:

void Contact::Serialize( CArchive& ar )
{
  if (ar.IsStoring())
  {
    CRuntimeClass* pruntime = Contact::GetRuntimeClass();
    int oldnr = pruntime->m_wSchema;
    pruntime->m_wSchema = CURRENT_VERSION;

    ar.SerializeClass(pruntime);

    switch (CURRENT_VERSION)
    {
     case 1:
       ar << m_strFirstName << m_strLastName << m_strAddress << m_strPhone; 
     break; 
     case 2:
       ar << m_strFirstName << m_strLastName << m_strAddress << m_strPhone << m_strMobilePhone << m_strEmail; break;
     default: // unknown version for this object 
       AfxMessageBox(_T("Unknown file version."), MB_ICONSTOP);
      break; 
    }
    
    pruntime->m_wSchema = oldnr;
  } else // loading code
    {
    ar.SerializeClass(RUNTIME_CLASS(Contact));

    UINT nVersion = ar.GetObjectSchema();

    switch (nVersion)
    {
    case 1:
      ar >> m_strFirstName >> m_strLastName >> m_strAddress >> m_strPhone;
      m_strMobilePhone = _T("");
      m_strEmail = _T("");
    break;

    case 2:
      ar >> m_strFirstName >> m_strLastName >> m_strAddress >> m_strPhone >> m_strMobilePhone >> m_strEmail;
      break;

    default:
     // unknown version for this object
     AfxThrowArchiveException(CArchiveException::badSchema);
     break;
    }
  }
}

If I want to store my data to a file, I obtain a runtime pointer to my serialized class, in order to set my file version schema. Then, depending on file version I serialize right object data and finally I reassign initial version schema value of my runtime class.

If I am loading a file, I call Contact::Serialize() method, I get file version schema and depending on schema value, I add right data to my document class.

View class – CSerAddressBookMDIView
This class is responsible with document class content (loaded file’s data) graphical representation. In my demo application, the view class is derived from CListView and has REPORT flag set in order to display data into a grid like.

Main responsible method with client window’s list control population is CSerAddressBookMDIView::PopulateList() and is listed down:

void CSerAddressBookMDIView::PopulateList()
{
  CSerAddressBookMDIDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);

  CreateViews(pDoc->GetFileVersion());

  CListCtrl *pListCtrl = &GetListCtrl();
  ASSERT_VALID(pListCtrl);

  // get a reference to the contacts list
  const ContactList& contacts = pDoc->GetContacts();

  // iterate over all contacts add add them to the list
  int nCurrentItem = 0;
  POSITION pos = contacts.GetHeadPosition();
  while(pos != NULL)
  {
    const Contact contact = contacts.GetNext(pos);

    nCurrentItem = pListCtrl->InsertItem(nCurrentItem, contact.GetFirstName());
    pListCtrl->SetItemText(nCurrentItem, 1, contact.GetLastName());
    pListCtrl->SetItemText(nCurrentItem, 2, contact.GetAddress());
    pListCtrl->SetItemText(nCurrentItem, 3, contact.GetPhoneNumber());

    switch(pDoc->GetFileVersion())
    {
      case 1:
      break;
      case 2:
      pListCtrl->SetItemText(nCurrentItem, 4, contact.GetMobileNumber());
      pListCtrl->SetItemText(nCurrentItem, 5, contact.GetEmail());
      break;
      default:
      break;
    }
  }
}

First, we obtain a pointer to our current document. Then, we call the method that inserts right list control columns, depending on file version (CreateViews() method).
We obtain a CListView pointer and an object reference to fist contact from contacts list. Then, as long as we have elements, we iterate over list’s elements (in a while() loop) and insert data to our list control.

PopulateList() method is called from overwrite CSerAddressBookMDIView::OnUpdate() method. OnUpdate() method is called by MFC framework as long as a document is changed.
The original OnUpdate() method is called by CDocument::UpdateAllViews() and is implemented in CView class.

In order to add/remove/update records from our documents I created a special dialog, launched from my Menu menu.
Display modal dialog method is listed down:

void CSerAddressBookMDIView::OnMymenuChangedata()
{
  CSerAddressBookMDIDoc *pDoc = GetDocument();
  ASSERT_VALID(pDoc);

  CManipulateDataDlg dlg;

  dlg.SetAddressDocument(pDoc);

  if (IDOK == dlg.DoModal())
  {
    PopulateList();
  }
}

Because I have to interact from my dialog window with contacts list of current document, I have to pass the pointer of my document class ( dlg.SetAddressDocument(pDoc) ) to my dialog window class. If the dialog is closed using Exit button (IDOK id) then the view is refilled, using PopulateList() call.

CManipulateDataDlg class

This class is responsible with the management of document contact list items. The difference between this dialog class and the dialog class of last article is that this class is not responsible with load/store process. This role was taken by document view architecture.

Dialog’s control list population method looks like this:

void CManipulateDataDlg::PopulateList()
{
  // delete all current members
  m_cList.DeleteAllItems();

  // get a reference to the contacts list
  const ContactList& contacts = m_pAddressDoc->GetContacts();

  // iterate over all contacts add add them to the list
  int nCurrentItem = 0;
  POSITION pos = contacts.GetHeadPosition();
  while(pos != NULL)
  {
    const Contact contact = contacts.GetNext(pos);

    nCurrentItem = m_cList.InsertItem(nCurrentItem, contact.GetFirstName());
    m_cList.SetItemText(nCurrentItem, 1, contact.GetLastName());
    m_cList.SetItemText(nCurrentItem, 2, contact.GetAddress());
    m_cList.SetItemText(nCurrentItem, 3, contact.GetPhoneNumber());

    switch(m_pAddressDoc->GetFileVersion())
    {
      case 1:
      break;
      case 2:
      m_cList.SetItemText(nCurrentItem, 4, contact.GetMobileNumber());
      m_cList.SetItemText(nCurrentItem, 5, contact.GetEmail());
      break;
    }
  }
}

Each time we clean the contact list we obtain a reference to the beginning of document contacts list. Depending on file version schema (1 or 2), dialog’s controls are customized. Then, we iterate over contact list elements (ContactList) and I insert data into my control list.

MDI support for many file extension

Default MDI applications come with only one file support and only one file extension file format.
Sometimes, our applications need to support different file format and more file extensions. In my demo application is necessary to support two file format and two file version (version 1 (*.sab1) and version 2 (*.sab2)).
Same time, the application must support old file format conversion to new file format and vice versa.
You can find multi file support detailed information for document view MFC application to Microsoft KB 141921. Other useful reference you can find here.
Starting from these references my application support two file format. I figure some important changes that I made into my initialize method, CSerAddressBookMDIApp::InitInstance.

BOOL CSerAddressBookMDIApp::InitInstance()
{
  // ----------
  // MFC’s wizard generated code…
  // ----------
  SetRegistryKey(_T("Local AppWizard-Generated Applications"));
  LoadStdProfileSettings(4); // Load standard INI file options (including MRU)
  // Register the application's document templates. Document templates
  // serve as the connection between documents, frame windows and views

  m_pDocManager = new CMultiDocManager; // Silviu

  CMultiDocTemplate* pDocTemplate;
  pDocTemplate = new CMultiDocTemplate(IDR_SerAddressBookTYPE,
  RUNTIME_CLASS(CSerAddressBookMDIDoc),
  RUNTIME_CLASS(CChildFrame), // custom MDI child frame
  RUNTIME_CLASS(CSerAddressBookMDIView));
  if (!pDocTemplate)
  return FALSE;
  AddDocTemplate(pDocTemplate);

  //Silviu
  pDocTemplate = new CMultiDocTemplate(
  IDR_SerAddressBook2TYPE,
  RUNTIME_CLASS(CSerAddressBookMDIDoc),
  RUNTIME_CLASS(CChildFrame), // custom MDI child frame
  RUNTIME_CLASS(CSerAddressBookMDIView));
  if (!pDocTemplate)
  return FALSE;
  AddDocTemplate(pDocTemplate);

  //Silviu
  pDocTemplate = new CMultiDocTemplate(
  IDR_SerAddressBook3TYPE,
  RUNTIME_CLASS(CSerAddressBookMDIDoc),
  RUNTIME_CLASS(CChildFrame), // custom MDI child frame
  RUNTIME_CLASS(CSerAddressBookMDIView));
  if (!pDocTemplate)
  return FALSE;
  AddDocTemplate(pDocTemplate);

  // create main MDI Frame window
  CMainFrame* pMainFrame = new CMainFrame;
  if (!pMainFrame || !pMainFrame->LoadFrame(IDR_SerAddressBookTYPE))
  {
    delete pMainFrame;
    return FALSE;
  }

  // ----------
  // MFC’s wizard generated code…
  // ----------

  return TRUE;
}

First point that I should mention, after LoadStdProfileSettings() (function written by MFC wizard) call, is the initialization of m_pDocManager attribute (pointer to CDocManager class, used for document template management) with a new object pointer to CMultiDocManager (class defined be me according with Microsoft Knowledge Base 141921). CMultiDocManager class overwrites some methods from CDocManager: CreateNewDocument(), DoPromptFileName(), OnFileNew().

Then, besides default document application template (with resource ID IDR_SerAddressBookTYPE), I create two new templates for my two different files format.
All templates are added into my document template list (AddDocTemplate()). Last significant change from InitInstance() means the right frame window (IDR_SerAddressBookTYPE – contains Save and Save As options).

Conclusion:
Multiple Document Interface (MDI) architecture is the best for this kind of data container application. MFC framework offers stable and complete support for objects serialization: storing and loading process.
Many of Microsoft Office applications are based on this architecture.

Download demo application: SerAddressBookMDI (Visual C++ 2005 project)