Click here to Skip to main content
15,860,943 members
Articles / Desktop Programming / MFC

The SBJ MVC Framework - The Model, from Abstraction to Realization

Rate me:
Please Sign up or sign in to vote.
5.00/5 (19 votes)
20 Mar 2009CPOL19 min read 108.4K   1.3K   51   32
A Model-View-Controller Framework that integrates with the MFC Doc/View architecture
SbjDevSrc201

For Those Of You Who Are Interested...

I have posted the following article in the series describing the MVC Framework.

Table of Contents

Introduction

Before reading this article, you'll need to read the introductory article, An Introduction to a Model-View-Controller Implementation for MFC, I posted last month. In it, I presented the underlying MVC Framework classes that integrate with the MFC Doc/View architecture, and discussed the basics of the Framework message handling and event architecture.

In this article, I plan to describe in detail how the Model ties into the MFC CDocument class, and how it is abstracted so that the actual data source is transparent to the application and the rest of the Framework.

To be honest, the Model abstraction wasn't present in the first article, rather XML specific extensions to the Framework were implemented. As I stated then, I attempt to generalize as much code as possible, though "sometimes, I don't see the generalities in the code that can be factored down to the lower levels". The Model abstraction is an example of code that I needed to write specifically before I could see how to generalize it. For those of you who are interested, you can compare the XmlMvc.dll code from the first article to the abstracted code that now exists as part of the MVC Framework in the SbjCore.dll. The current XmlMvc.dll contains only code that provides file IO and the creation, getting, and setting of XML elements and attributes. All other code has been factored down to the MVC Framework itself.

The Source Code

The source code provided with this article is contained in a VS2008 solution, SbjDev, with three projects. Although the three projects were present in the first article, the contents of the projects have changed dramatically due to the refactoring of the Model abstraction. Of course, the Shapes.exe sample application has been rewritten to take advantage of this refactoring.

I neglected to mention in the first article that the naming of the DLLs reflects their version. I use a convention that seems to be common among third-party vendors of MFC extensions:

<Project Name><Major/Minor Version><VC++ Major/Minor Version>.dll

Below, I list the current filenames that are produced by each of the projects:

  • SbjCore - SbjCore2090.dll - The foundation DLL including the MVC Framework
  • XmlMvc - XmlMvc2090.dll - The DLL that contains the concrete XML Model implementation
  • Shapes - Shapes.exe - The sample EXE

The Abstract Model

Overview

The MVC Framework does not declare a class Model. Instead, the Model is considered to be the domain or application specific data that will be presented to the user of the application. The source of the data, whether it be a database, an XML document, or some other source, will typically be accompanied by a program or service for accessing that data. In the case of the XML Model provided with this article, it is the MSXML6 implementation of the Document Object Model (DOM).

The Framework declares an abstract interface through which it communicates in a common way with any given data source's access program. For each supported data source, a concrete implementation of the interface must be supplied. The MSXML6 DOM interface is implemented in the XmlMvc2090.dll accompanying this article.

Regardless of the concrete Model implementation, the Abstract Model provides two very important services.

  • Modification notification
  • Undo-Redo support

I'll discuss these further when discussing the implementation.

The Model Structure

The Framework identifies the basic concept of record/field, row/column, element/attribute as ModelItem/Attribute for no better reason than I was trying to be as generic as possible in my naming convention and not tie the names too closely to an existing Model implementation.

The Type of a ModelItem or Attribute is identified by a literal string. For instance, the type of a ModelItem might be:

"Person"

and a ModelItem of type "Person" might have an Attribute of:

"lastName"

A unique instance of a ModelItem is identified by a unique ModelItemHandle (again a generic term) which is implemented as, you guessed it, a HANDLE data type. The reason for this is that it causes no new dependency or coupling since it is part of the Windows API, and I can assume any concrete implementation of a Model will be able to cast whatever actual unique identifier it uses to a HANDLE.

The Model demands that ModelItems are hierarchical, so that any ModelItem may have child ModelItems associated with it. Even if the actual concrete Model contains only a list of ModelItems and none of those have children, the Abstract Model declares a ModelItemRoot from which all other ModelItems are descended. The value of the ModelItemRoot HANDLE is defined as 0xFFFFFFFF. How the ModelItemRoot is interpreted in the concrete Model is dependent on the implementation. In the XML Model implementation, it obviously is interpreted as the XMLDocumentElement.

All of the components of the Abstract Model are contained in the namespace SbjCore::Mvc::Model.

Class Model::Controller

In the MVC Framework, the interface to the Model data is through the abstract base Model::Controller class. Unlike other Controller classes in the Framework, the Model::Controller class does not directly serve a Controlled CmdTarget or CWnd. Instead, it is a base for the Doc::Controller class described later in this article.

Model::Controller declares a set of private pure virtual data access methods for accessing and modifying the Model. These pure virtual methods are what must be implemented in a concrete Model::Controller derivative for a specific data source.

As you can see from the following listing, there is a lot more to the Model::Controller than just declaring the pure virtual methods. There is the static method GetItemRoot, which as discussed in the Overview, returns the hard coded ModelItemRoot HANDLE. In addition, there is a method that returns an SbjCore::UndoRedo::Manager, methods for managing the currently selected ModelItems, and the public data access methods that call the corresponding private virtual methods implemented in the derived classes. I'll discuss each of these further, following the Model::Controller listing.

C++
//Project Location: SbjCore/Mvc/ModelController.h

namespace Model
{
  struct ControllerImpl;

  class AFX_EXT_CLASS Controller : public CmdTargetController
  {
    typedef CmdTargetController t_Base;

    DECLARE_DYNAMIC(Controller)
  public:
    Controller();
    virtual ~Controller();
    
  public:
    static HANDLE GetItemRoot();

  public:
    SbjCore::UndoRedo::Manager* GetUndoRedoMgr() const;
    
    int GetSelectedItems(ItemHandles& selItems) const;
    
    void SetSelectedItems(const ItemHandles& selItems);
    
    void ClearSelectedItems();

  public:
    HANDLE CreateItem(LPCTSTR lpszItemType);
    
    bool InsertChild(
      const HANDLE hChild,
      const HANDLE hParent,
      const HANDLE hAfter = NULL,
      bool bAddToUndoRedo = true);

    bool RemoveChild(
      const HANDLE hChild
      bool bAddToUndoRedo = true);

    _variant_t GetItemAttrValue(
      const HANDLE hItem, 
      const CString& sAttrName) const;

    bool SetItemAttrValue(
      const HANDLE hItem, 
      const CString& sAttrName, 
      const _variant_t& val,
      bool bAddToUndoRedo = false, 
      bool bFireEvents = false);

    CString AssureUniqueItemAttrValue(
      const HANDLE hItem, 
      const CString& sAttrName, 
      const _variant_t& val) const;

    CString CookAttrName(const CString& sAttrName) const;

    CString GetItemTypeName(HANDLE hItem) const;
    
    HANDLE GetParentItem(const HANDLE hItem) const;

    int GetItemChildren(HANDLE hItem, SbjCore::Mvc::Model::ItemHandles& items) const;

    int GetItemAttrNames(HANDLE hItem, 
	SbjCore::Mvc::Model::ItemAttrNames& attrNames) const;
    
    CString GetItemAttrName(HANDLE hItem, int nAttrIndex) const;

  private:
    virtual CString OnCookAttrName(const CString& sAttrName) const;

  private:
    virtual HANDLE OnCreateItem(LPCTSTR lpszItemType) = 0;

    virtual bool OnInsertChild(
      const HANDLE hChild,
      const HANDLE hParent,
      const HANDLE hAfter) = 0;

    virtual bool OnRemoveChild(
      const HANDLE hChild) = 0;

    virtual _variant_t OnGetItemAttrValue(
      const HANDLE hItem, 
      const CString& sAttrName) const = 0;

    virtual bool OnSetItemAttrValue(
      const HANDLE hItem, 
      const CString& sAttrName, 
      const _variant_t& val) = 0;

    virtual CString OnAssureUniqueItemAttrValue(
      const HANDLE hItem, 
      const CString& sAttrName, 
      const _variant_t& val) const = 0;

    virtual CString OnGetItemTypeName(HANDLE hItem) const = 0;
    
    virtual HANDLE OnGetParentItem(const HANDLE hItem) const = 0;

    virtual int OnGetItemChildren(HANDLE hItem, 
            SbjCore::Mvc::Model::ItemHandles& items) const = 0;

    virtual int OnGetItemAttrNames(HANDLE hItem, 
            SbjCore::Mvc::Model::ItemAttrNames& attrNames) const = 0;

  private:
    struct ControllerImpl* const m_pImpl;
  };

  AFX_EXT_API Controller* GetCurController();
}

Currently Selected ModelItems

I think the concept of "currently selected" ModelItems is straightforward (i.e., a user clicks on a representation of an item or group of items in one of the Views). A vector is maintained by the Model::Controller, and is accessed through the following three methods by the various Framework Controller classes.

  • GetSelectedItems
  • SetSelectedItems
  • ClearSelectedItems

Data Access Methods

The function of most of the data access methods is fairly self evident; however, below is a list with a short description of their purpose. Of course, the actual implementation is provided by the derived classes through their corresponding virtual methods.

  • CreateItem - returns a HANDLE to an newly created ModelItem
  • InsertChild - inserts a child ModelItem
  • RemoveChild - removes a child ModelItem
  • GetItemAttrValue - gets an Attribute value
  • SetItemAttrValue - sets an Attribute value
  • AssureUniqueItemAttrValue - assures that an Attribute value is unique among its siblings
  • GetItemTypeName - returns a ModelItem's type
  • GetParentItem - returns the HANDLE of an ModelItem's parent
  • GetItemChildren - returns a list of child ModelItem HANDLEs
  • GetItemAttrNames - returns a list of Attribute names for a ModelItem
  • GetItemAttrName - returns an Attribute name by index
  • CookAttrName - provides a formatted version of an Attribute type name (e.g., lastName is returned as Last Name)

In addition to calling their corresponding pure virtual methods, each of the three data access methods that modify the Model provide optional Undo-Redo support. The three methods are:

  • InsertChild
  • RemoveChild
  • SetItemAttrValue

Whereas InsertChild and RemoveChild always fire events (as discussed in the first article) indicating changes to the Model, SetItemAttrValue supports the ability to optionally fire events. This allows multiple Attributes to be modified under the firing of one Event. Each of the methods follows the same general format, so rather than discuss each one separately, I'll use the SetItemAttrValue method for illustrative purposes, since it contains code for both optional supports. After outlining the implementation of the method, I'll go into more detail describing Undo-Redo support and Event Firing.

C++
//Project Location: SbjCore/Mvc/ModelController.cpp

bool Controller::SetItemAttrValue( 
  const HANDLE hItem, 
  const CString& sAttrName, 
  const _variant_t& val, 
  bool bAddToUndoRedo /*= false*/, 
  bool bFireEvents /*= false*/)
{
  _variant_t vAfter = val;
  _variant_t vBefore;
  
  if (bAddToUndoRedo)
  {
    vBefore = GetItemAttrValue(hItem, sAttrName);
  }
  
  bool bRslt = OnSetItemAttrValue(hItem, sAttrName, val);

  if (bRslt)
  {
    if (bAddToUndoRedo)
    {
      class UndoRedoHandler : public SbjCore::UndoRedo::Handler
      {
        CString sActionName;
        Controller* pTheCtrlr;
        const HANDLE hItem;
        CString sAttrName;
        _variant_t vBefore;
        _variant_t vAfter;
      public:
        UndoRedoHandler(
          Controller* p,
          const HANDLE h, 
          CString a, 
          _variant_t vB, 
          _variant_t vA) :
          pTheCtrlr(p),
          hItem(h),
          sAttrName(a),
          vBefore(vB),
          vAfter(vA)
        {
          CString s(pTheCtrlr->CookAttrName(sAttrName));
          sActionName.Format(_T("%s change"), s);
        }

        virtual bool OnHandleUndo()
        {
          return pTheCtrlr->SetItemAttrValue(hItem, sAttrName, 
                                        vBefore, false, true);
        }

        virtual bool OnHandleRedo()
        {
          return pTheCtrlr->SetItemAttrValue(hItem, sAttrName, 
                                         vAfter, false, true);
        }

        virtual LPCTSTR OnGetHandlerName() const
        {
          return sActionName;
        }
        
      protected:
        virtual ~UndoRedoHandler()
        {
        }  
      };
      UndoRedoHandler* pUndoRedoHandler = new UndoRedoHandler(this, 
                                hItem, sAttrName, vBefore, vAfter);
      m_pImpl->theUndoRedoMgr.Push(pUndoRedoHandler);
    }
    if (bFireEvents)
    {
      Model::Events::ItemChange eventItemChanged(Model::Events::EVID_ITEM_CHANGED, 
                                                 this, hItem, sAttrName);
      Doc::Events::DocModified eventDocModified(true);
    }
  }

  return bRslt;
}

Before calling the corresponding pure virtual method OnSetItemAttrValue, SetItemAttrValue checks the value of the bAddToUndoRedo parameter, and if true, saves the current value of the named Attribute so it can be used to undo the Attribute value change. If the pure virtual returns true, and again based on the true value of bAddToUndoRedo, SetItemAttrValue declares and implements a derivative of the SbjCore::UndoRedo::Handler class. Notice that the UndoRedoHandler class calls SetItemAttrValue to implement the Undo and Redo functionality, except that here, the bAddToUndoRedo parameter is set to false. It then dynamically allocates an instance of the UndoRedoHandler class, and pushes it onto the undo stack maintained by the SbjCore::UndoRedo::Manager.

Finally, two Event classes are optionally fired; one indicating the specific change to the Model, and a second, indicating the general condition of the modification of the CDocument class, which by default calls CDocument::SetModifiedFlag with the value of the Doc::Events::DocModified parameter.

Undo-Redo Support

The Undo-Redo architecture is actually not part of the MVC Framework. Like the Event architecture described in the first article, it too is generic, and can be used without any Framework dependencies. The two components of the architecture, Manager and Handler, are declared under the namespace SbjCore::UndoRedo. I'll discuss the UndoRedo::Handler first since you've already been introduced to it in the last section.

Class UndoRedo::Handler

UndoRedo::Handler is the pure virtual base class from which each of the UndoRedoHandler classes defined in the Model::Controller data access methods is derived. The derivatives handle the actual details of performing any undo or redo of an action. Derivatives should make their destructor protected to force dynamic allocation, as these are pushed onto the UndoRedo::Manager's stack, and once there, the manager handles deletion once the UndoRedo::Handler is no longer needed as part of the Undo-Redo process.

Notice that the UndoRedo::Handler has a method for returning a HandlerName. This name is added to a list of handler descriptions maintained by the UndoRedo::Manager class, which can be queried to provide the user with a list of multiple actions to undo or redo.

C++
//Project Location: SbjCore/UndoRedo/UndoRedoHandler.h

class AFX_EXT_CLASS Handler
{
public:
  virtual ~Handler(void);
  
public:
  bool HandleUndo();
  bool HandleRedo(); 

  LPCTSTR GetHandlerName() const;

private:
  virtual bool OnHandleUndo() = 0;
  virtual bool OnHandleRedo() = 0;

  virtual LPCTSTR OnGetHandlerName() const = 0;
  
};

Class UndoRedo::Manager

The UndoRedo::Manager class maintains two internal stacks, one for Undo and one for Redo. To provide Undo-Redo handling for an action or command, an UndoRedo::Handler derivative is allocated on the heap and pushed onto the Manager's undo stack with a call to Push. When a call to Manager::Undo is made, the UndoRedo::Handler is popped off the undo stack, its Undo method is executed, and the UndoRedo::Handler is pushed onto the redo stack. If a subsequent call to Redo is made, the Handler is popped off the redo stack and its Redo method is executed, and the UndoRedo::Handler is returned to the undo stack. If a new call to Push is made, the redo stack is cleared.

C++
//Project Location: SbjCore/UndoRedo/UndoRedoMgr.h

class AFX_EXT_CLASS Manager
{
public:
  Manager();
  virtual ~Manager();
  
public:
  void Push(Handler* p);

  bool Undo(int nCount);
  bool Redo(int nCount);
  
  void ClearUndo();
  void ClearRedo();

  CStringList& GetUndoList() const;
  CStringList& GetRedoList() const;
  
  bool EnableUndo() const;
  bool EnableRedo() const;
  
  void SetUndoButton(CMFCRibbonUndoButton* p);
  CMFCRibbonUndoButton* GetUndoButton() const;

  void SetRedoButton(CMFCRibbonUndoButton* p);
  CMFCRibbonUndoButton* GetRedoButton() const;
  
private:
  struct ManagerImpl* const m_pImpl;
}; 

As mentioned in the discussion of UndoRedo::Handler, a list of descriptions of the Handler objects on each stack can be retrieved through calls to Manager::GetUndoList or Manager::GetRedoList. The MFC Feature Pack CMFCRibbonUndoButton class takes advantage of this through its dropdown listbox, providing a method for users to process more than one action at a time. Complementing this, the Manager::Undo and Manager::Redo methods can be passed a count of actions to process. Methods are provided for attaching instances of CMFCRibbonUndoButton for both undo and redo. More on the CMFCRibbonUndoButton implementation later.

The Model::Controller instantiates an instance of a SbjCore::UndoRedo::Manager which is accessed through the following method:

C++
//Project Location: SbjCore/Mvc/ModelController.cpp
SbjCore::UndoRedo::Manager* Controller::GetUndoRedoMgr() const;

The UndoRedo::Manager is accessed by CmdMsgHandler classes for ID_EDIT_UNDO and ID_EDIT_REDO. The Model::ControllerImpl contains instances of these CmdMsgHandler classes, and they are attached to the Model::Controller in its constructor. Each CmdMsgHandler is essentially identical, the differences being which UndoRedo::Manager stack is accessed. For brevity's sake, I'll only list the CmdMsgHandler for ID_EDIT_UNDO.

C++
//Project Location: SbjCore/Mvc/ModelController.cpp

class OnUndoHandler : public SbjCore::Mvc::CmdMsgHandler
{
  virtual bool OnHandleCmd(UINT nID)
  {
    nID;
    bool bRslt = false;

    SbjCore::Mvc::Model::Controller* pCtrlr = 
      dynamic_cast<SbjCore::Mvc::Model::Controller*>(GetController());
    SbjCore::UndoRedo::Manager* pMgr = pCtrlr->GetUndoRedoMgr();
    if (pMgr != NULL)
    {
      CMFCRibbonUndoButton* pUndoBtn = pMgr->GetUndoButton();

      if (pUndoBtn != NULL)
      {
        int nActionNumber = pUndoBtn->GetActionNumber();
        int nCount = (nActionNumber > 0) ? nActionNumber : 1;
        pCtrlr->GetUndoRedoMgr()->Undo(nCount);
        bRslt = true;
      }
      else
      {
        pCtrlr->GetUndoRedoMgr()->Undo(1);
        bRslt = true;
      }
    }

    return bRslt;
  }

  virtual bool OnHandleCmdUI(CCmdUI* pCmdUI)
  {
    bool bRslt = false;
    bool bEnable = false;

    SbjCore::Mvc::Model::Controller* pCtrlr = 
      dynamic_cast<SbjCore::Mvc::Model::Controller*>(GetController());
    SbjCore::UndoRedo::Manager* pMgr = pCtrlr->GetUndoRedoMgr();
    if (pMgr != NULL)
    {
      bEnable = pMgr->EnableUndo();
      bRslt = true;
    }

    pCmdUI->Enable(bEnable);
    return bRslt;
  }

};

Accessing the UndoRedo::Manager for the assigned CMFCRibbonUndoButton, the OnUndoHandler gets the number of actions the user has chosen to undo, and calls the UndoRedo::Manager object's Undo method. Similarly, in the OnHandleCmdUI method, it queries the UndoRedo::Manager as to the enable state.

There's one more player in the Undo-Redo architecture, and that's the Ribbon::UndoRedoMenuHandler. It is attached to the SbjCore::Mvc::FrameWndExController class which acts as Controller for the Shapes application's Controlled CMainFrame. Below is the implementation of its OnHandleWndMsg method.

Class Ribbon::UndoRedoMenuHandler

C++
//Project Location: SbjCore/Mvc/Controls/Ribbon/UndoRedoMenuHandler.cpp

LRESULT UndoRedoMenuHandler::OnHandleWndMsg(WPARAM wParam, 
                             LPARAM lParam, LRESULT* pResult)
{
  wParam;
  *pResult = 0;
  LRESULT lRslt = 1;

  SbjCore::Mvc::Model::Controller* pCtrlr = 
                SbjCore::Mvc::Model::GetCurController();
  SbjCore::UndoRedo::Manager* pMgr = pCtrlr->GetUndoRedoMgr();

  if (pMgr != NULL)
  {
    CMFCRibbonBaseElement* pElem = (CMFCRibbonBaseElement*) lParam;
    ASSERT_VALID(pElem);

    if (pElem->GetID() == ID_EDIT_UNDO)
    {
      CMFCRibbonUndoButton* pUndo = dynamic_cast<CMFCRibbonUndoButton*>(pElem);
      ASSERT_VALID(pUndo);

      pMgr->SetUndoButton(pUndo);

      pUndo->CleanUpUndoList();

      CStringList& sUndoList = pMgr->GetUndoList();
      for (POSITION pos = sUndoList.GetHeadPosition (); pos != NULL;)
      {
        pUndo->AddUndoAction(sUndoList.GetNext(pos));
      }
    }
    else if (pElem->GetID() == ID_EDIT_REDO)
    {
      CMFCRibbonUndoButton* pUndo = dynamic_cast<CMFCRibbonUndoButton*>(pElem);
      ASSERT_VALID(pUndo);

      pMgr->SetRedoButton(pUndo);

      pUndo->CleanUpUndoList();

      CStringList& sRedoList = pMgr->GetRedoList();
      for (POSITION pos = sRedoList.GetHeadPosition (); pos != NULL;)
      {
        pUndo->AddUndoAction(sRedoList.GetNext(pos));
      }
    }
  }

  return lRslt;
}

The UndoRedoMenuHandler handles the AFX_WM_ON_BEFORE_SHOW_RIBBON_ITEM_MENU registered Windows message when the user clicks on the dropdown arrow portion of the button. Once called, it queries the CMFCRibbonBaseElement for the message ID, handling both the ID_EDIT_UNDO and ID_EDIT_REDO messages. Accessing the current Model::Controller, it gets the UndoRedo::Manager, assigns the button, and queries the UndoRedo::Manager for the list of UndoRedo::Handler descriptions, and fills its dropdown listbox. Once the user selects the number of actions to process, the appropriate ID_EDIT_UNDO or ID_EDIT_REDO CmdMsgHandler is called.

Event Firing Support

The namespace Model contains a number of predefined Event IDs and Event derivations for handling the notification of changes to the Model. An example was seen in the listing for the SetItemAttrValue method, earlier in the article. These are contained in the project location: SbjCore/Mvc/Model/ModelEvents.h. Below is a list of the available Model::Event IDs, along with a short description:

  • EVID_ITEM_INSERTING - fired by Model::Controller::InsertChild before an Item is inserted
  • EVID_ITEM_INSERTED - fired by Model::Controller::InsertChild after inserting an Item
  • EVID_ITEM_REMOVING - fired by Model::Controller::RemoveChild before an Item is removed
  • EVID_ITEM_REMOVED - fired by Model::Controller::RemoveChild after removing an Item
  • EVID_ITEM_CHANGING - fired by Model::Controller::SetItemAttrValue before an Item is changed
  • EVID_ITEM_CHANGED - fired by Model::Controller::SetItemAttrValue after changing an Item
  • EVID_SELITEM_CHANGED - fired by Model::Controller::SetSelectedItems when the Items selected have changed

I've never actually had an occasion to handle one of the events ending in "ING"; however, I can see where there might be a desire to short circuit one of these actions before it has actually been carried out. These events will be revisited in future articles when I discuss how they are handled by the various Views and Controls in the MVC Framework.

The Abstract Document

The first article discussed the concept of Controlled CmdTarget and CWnd classes. The Doc::ControlledDocument class derives from ControlledCmdTarget<CDocument>, and through its accompanying Model::Controller derived Doc::Controller class, provides the actual base for the concrete Model implementation. In the case of XML, the XmlMvc::XmlDoc::Controller class, which I will discuss in the next section.

You may have noticed that there was no mention of files when discussing the Abstract Model and its Model::Controller. Since files are something that are handled in MFC by the CDocTemplate and CDocument classes, it seemed more appropriate to introduce them in the Doc::Controller. This also implies that the Model can be used in other than file based situations.

In the MVC Framework, the actual creating, opening, and saving of files is delegated to the concrete Doc::Controller derived class. Passing the responsibility for these tasks to the Doc::Controller derivative is handled by Doc::ControlledDocument. It overrides the following CDocument methods:

C++
//Project Location: SbjCore/Mvc/Documents/ControlledDocument.h

virtual BOOL OnNewDocument();
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);

virtual void Serialize(CArchive& ar);

and calls Doc::Controller pure virtual methods of the same name. Of course, the concrete Doc::Controller derivative implements these methods appropriately for the underlying Model it supports. If the virtual methods return true, the Doc::ControlledDocument takes care of notifying the observers by firing the appropriate events listed below.

  • EVID_FILE_NEW - fired by Doc::ControlledDocument::OnNewDocument
  • EVID_FILE_OPEN - fired by Doc::ControlledDocument::OnOpenDocument
  • EVID_FILE_SAVE - fired by Doc::ControlledDocument::OnSaveDocument
  • EVID_DOC_MODIFIED - fired by all three with a parameter of false

The Concrete XML Model/Document

I assume you are familiar with the structure of XML documents and the MSXML DOM interfaces. The relationships to the Abstract Model are as follows.

  • ModelItemRoot - a specific IXMLDOMElement interface to the root DocumentElement
  • ModelItem - an IXMLDOMElement interface
  • Attribute - an IXMLDOMAttribute interface

Before going into how the XmlDoc::Controller implements the data and file access methods, I want to discuss how the XmlDoc::Controller implements the ModeItem HANDLE.

ModelItem HANDLE Creation and Assignment

As far as I know, there is no natural unique identifier assigned to each element in an XML document, so the XmlDoc::Controller has manufactured one. It does this by injecting an Attribute with a unique value into each element when it is created or first accessed. In addition to uniquely identifying each element, the controller must keep track of the next available unique value. It does this by injecting an Attribute into the XML DocumentElement to contain this value. The two Attributes are named respectively, "sbjHandle" and "nextSbjHandle". I figure these names have little chance of colliding with any real Attribute names. The sample Shapes.xml listed below illustrates how the Attributes appear. The "nextSbjHandle" Attribute has an initial value of 0xF0000001; however, it is displayed in the file as a decimal value, as are the "sbjHandle" Attributes.

XML
<!--Project Location: Shapes/Data/Shapes.xml-->

<?xml version="1.0" encoding="utf-8"?>
<Shapes nextSbjHandle="4026531845">
  <Drawing name="Test Drawing" sbjHandle="4026531841">
    <Rectangle label="The First Rectangle" left="88" top="50" right="361" bottom="315" 
    borderRGB="10526303" borderWidth="9" fillRGB="15130800" sbjHandle="4026531842"/>
    <Rectangle label="Second Rectangle" left="52" top="19" right="203" bottom="70" 
    borderRGB="25600" borderWidth="8" fillRGB="2263842" sbjHandle="4026531843"/>
    <Ellipse label="The First Ellipse" left="56" top="185" right="409" bottom="273" 
    borderRGB="4163021" borderWidth="25" fillRGB="6333684" sbjHandle="4026531844"/>
  </Drawing>
</Shapes>

IXMLDOMElement to HANDLE and HANDLE to IXMLDOMElement

To implement the data and file access methods, the XmlDoc::Controller needs to be able to retrieve an IXMLDOMElement interface given its HANDLE, and retrieve a HANDLE to a given IXMLDOMElement. The two methods provided for this in the controller's ControllerImpl are listed below:

C++
//Project Location: XmlMvc/Documents/XmlDocController.cpp

HANDLE GetHandleFromNode(MSXML2::IXMLDOMElementPtr sp)
{
  HRESULT hr = S_OK;
  UINT hNext = NULL;
  UINT hItem = NULL;
  try
  {
    (void)SbjCore::Utils::Xml::GetAttribute(sp, _T("sbjHandle"), hItem);
    if (NULL == hItem)
    {
      (void)SbjCore::Utils::Xml::GetAttribute(spTheDocElement, 
                                 _T("nextSbjHandle"), hNext);
      hItem = hNext;
      (void)SbjCore::Utils::Xml::SetAttribute(sp, _T("sbjHandle"), hItem);
      (void)SbjCore::Utils::Xml::SetAttribute(spTheDocElement, 
                                 _T("nextSbjHandle"), ++hNext);
      SbjCore::Mvc::Doc::Events::DocModified event(true);
    }
  }
  catch (_com_error& e)
  {
    ASSERT(FALSE);
    hr = e.Error();
  }
  return (HANDLE)hItem;
}

MSXML2::IXMLDOMElementPtr GetNodeFromHandle(HANDLE hItem)
{
  HRESULT hr = S_OK;
  MSXML2::IXMLDOMElementPtr spRslt = NULL;
  try
  {
    if (hItem != SbjCore::Mvc::Model::Controller::GetItemRoot())
    {
      CString sXPath;
      sXPath.Format(_T("descendant::*[@sbjHandle = %u]"), hItem);
      spRslt = spTheDocElement->selectSingleNode((LPCTSTR)sXPath);
      if (NULL == spRslt)
      {
        spRslt = theHandleMap[hItem];
      }
    }
    else
    {
      spRslt = spTheDocElement;
    }
  }
  catch (_com_error& e)
  {
    ASSERT(FALSE);
    hr = e.Error();
  }

  return spRslt;
}

The GetHandleFromNode method queries the passed MSXML2::IXMLDOMElementPtr for its "sbjHandle" Attribute. If it is not found, the value of the "nextSbjHandle" Attribute from the DocumentElement is returned and is injected into the element, and finally the "nextSbjHandle" Attribute is bumped.

Retrieving the IXMLDOMElement from a HANDLE is a bit more complex. As you can see, normally, it is possible to use an XPath query on the document to find the element; however, when a new element is created, it is given a HANDLE. But, since it has yet to be inserted into the document, the XPath query will fail. As a matter of fact, the OnInsertChild method needs to retrieve the element from the HANDLE to actually insert it. To allow for this, the controller keeps a map of HANDLE to MSXML2::IXMLDOMElementPtr. I'm not sure if it wouldn't be better to put all the elements in the map, to avoid having the overhead of using the XPath query, but for now, I'm going to leave it as how it is. This may change in the future.

Implementing the Model::Controller Data and the Doc::Controller File Access Methods

I'm not going to go through every method, because most of them follow the same form; wrapping calls to the DOM, and marshaling between IXMLDOMElement and HANDLE. For illustrative purposes, I'll list the OnInsertChild method.

C++
//Project Location: XmlMvc/Documents/XmlDocController.cpp

bool Controller::OnInsertChild(
  const HANDLE hChild, 
  const HANDLE hParent, 
  const HANDLE hAfter)
{
  HRESULT hr = S_OK;

  try
  {
    MSXML2::IXMLDOMElementPtr spTheChild = m_pImpl->GetNodeFromHandle(hChild); 
    MSXML2::IXMLDOMElementPtr spTheParent = m_pImpl->GetNodeFromHandle(hParent); 
    MSXML2::IXMLDOMElementPtr spTheAfter = m_pImpl->GetNodeFromHandle(hAfter); 

    if (spTheAfter != NULL)
    {
      spTheParent->insertBefore(spTheChild, _variant_t(spTheAfter.GetInterfacePtr()));
    }
    else
    {
      spTheParent->appendChild(spTheChild);
    }
  }
  catch (_com_error& e) 
  {
    ASSERT(FALSE);
    hr = e.Error();
  }
  catch (...)
  {
    ASSERT(FALSE);
  }
  return (S_OK == hr);
}

Note that the _com_error is caught. All of the smart pointer DOM routines throw it when an error is encountered. The catch (...) is mainly there as a development tool, to investigate any unexpected exceptions.

One other data access method is of interest, OnAssureUniqueItemAttrValue.

C++
//Project Location: XmlMvc/Documents/XmlDocController.cpp

CString Controller::OnAssureUniqueItemAttrValue( 
  const HANDLE hItem, 
  const CString& sAttrName, 
  const _variant_t& val) const
{
  HRESULT hr = NULL;
  MSXML2::IXMLDOMNodeListPtr spNodeList = NULL;
  CString sVal;
  try
  {
    sVal = (LPCTSTR)(_bstr_t)val;
    CString sXPath;
    sXPath.Format(_T("*[starts-with(@%s, '%s')]"), sAttrName, sVal);
    MSXML2::IXMLDOMElementPtr spElement = m_pImpl->GetNodeFromHandle(hItem); 
    spNodeList = spElement->selectNodes((LPCTSTR)sXPath);
  }
  catch (_com_error& e) 
  {
    hr = e.Error();
  }
  catch (...)
  {
    ASSERT(FALSE);
  }

  if (spNodeList != NULL)
  {
    int nCount = spNodeList->Getlength();

    if (nCount > 0)
    {
      nCount++;
      sVal.Format(_T("%s (%d)"), (LPCTSTR)(_bstr_t)val, nCount);
    }
  }

  return sVal;
}

This method is used when an Attribute value is being presented to the user as a unique identifier. For instance, in the Shapes.exe application, when a new Rectangle is created, it has a default "label" Attribute of "New Rectangle". If the user creates a second Rectangle without changing the default "label" of the first, the method will deduce that "New Rectangle" is already in use and change the second Rectangle's "label" to "New Rectangle (2)".

Applying the Model to the Application

At this point, I think you'll begin to see the true benefit of the MVC Framework. To apply the Model to the Shapes application takes only a few modifications to the original MFC AppWizard generated ShapesDoc class.

The first step is to control the CDocument derived ShapesDoc class. The following listing is of the ShapesDoc.h file. Note that modifications to the original are marked in bold.

ShapesDoc.h

C++
#pragma once

struct ShapesDocImpl;

class ShapesDoc : public SbjCore::Mvc::ControlledDocument 
{
  typedef SbjCore::Mvc::ControlledDocument t_Base;

protected: // create from serialization only
  ShapesDoc();
  DECLARE_DYNCREATE(ShapesDoc)

public:
  virtual ~ShapesDoc();
#ifdef _DEBUG
  virtual void AssertValid() const;
  virtual void Dump(CDumpContext& dc) const;
#endif

// Generated message map functions
protected:
  DECLARE_MESSAGE_MAP()
  
private:
  struct ShapesDocImpl* const m_pImpl;
  
};

You'll notice that the CDocument base class has been replaced by SbjCore::Mvc::ControlledDocument. As discussed in the first article, this allows an assigned Controller class first crack at any WM_COMMAND message sent to ShapesDoc. The typedef SbjCore::Mvc::ControlledDocument t_Base is just a convenience so references to the base class in the .cpp file are t_Base, and are automatically updated should the actual base class change. OnNewDocument and Serialize are removed (actually relegated to SbjCore::Mvc::ControlledDocument), and the private struct ShapesDocImpl* has been added. This private struct is a common way of hiding implementation details, and you'll see it in almost every SbjCore and XmlMvc class. As you'll see, in the next step when we replace the Shapes.cpp code, it also doubles as the Controller class for ShapesDoc.

Next, the ShapesDoc.cpp code must be modified. I'm going to show this in two steps: first the basic attachment to the XML Model implementation, and then some additional application specific code.

ShapesDoc.cpp

C++
#include "stdafx.h"
#include "Shapes.h"
#include "ShapesDoc.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

struct ShapesDocImpl : public XmlMvc::XmlDoc::Controller
{
	HANDLE hDrawing;

	ShapesDocImpl() :
    	hDrawing(NULL)
    {
    	SetDocElementName(_T("Shapes"));
    }

	virtual ~ShapesDocImpl()
    {
    }
    
};

// ShapeDoc //////////////////////////////////////////////////////////

IMPLEMENT_DYNCREATE(ShapesDoc, CDocument)

BEGIN_MESSAGE_MAP(ShapesDoc, CDocument)
END_MESSAGE_MAP()

ShapesDoc::ShapesDoc() :
  m_pImpl(new ShapesDocImpl)
{
  SetController(m_pImpl);
}

ShapesDoc::~ShapesDoc()
{

  try
  {
    delete m_pImpl;
  }
  catch(...)
  {
    ASSERT(FALSE);
  }
  
}

#ifdef _DEBUG
void ShapesDoc::AssertValid() const
{
  CDocument::AssertValid();
}
void ShapesDoc::Dump(CDumpContext& dc) const
{
  CDocument::Dump(dc);
}
#endif //_DEBUG

The struct ShapesDocImpl has been declared as a derivative of XmlMvc::XmlDoc::Controller. In their constructor, there is a call to XmlMvc::XmlDoc::Controller::SetDocElementName telling the controller what the DocumentElement type should be, in this case "Shapes". This is used to validate existing files, and to create the DocumentElement in new files. This is the only reference to the XmlMvc::XmlDoc::Controller in the application. All other references are to the underlying Model abstraction, SbjCore::Mvc::Model::Controller.

In the ShapesDoc class, code has been added to the constructor and destructor to create and delete the m_pImpl instance of struct ShapesDocImpl, and to assign the ShapesDoc constructor to be its Controller.

The second set of modifications is to the ShapesDocImpl Controller and the addition of CmdMsgHandler classes specific to the Shapes application.

C++
namespace localNS
{

  // Message Handlers //////////////////////////////////////////
  class InsertShapeHandler : public SbjCore::Mvc::CmdMsgHandler
  {
    CString sShapeType;
  public:
    InsertShapeHandler(LPCTSTR lpszShapeType) :
      sShapeType(lpszShapeType)
    {
    
    }
  private:
    virtual bool OnHandleCmd(UINT nID)
    {
      nID;
      bool bRslt = false;
      SbjCore::Mvc::Model::Controller* pModelCtrlr = 
        dynamic_cast<SbjCore::Mvc::Model::Controller*>(GetController());
      SbjCore::Mvc::Model::ItemHandles theItems;
      int nCount = pModelCtrlr->GetItemChildren(
                   SbjCore::Mvc::Model::Controller::GetItemRoot(), 
                   theItems);
      if (1 == nCount)
      {
        HANDLE hDrawing = theItems[0];
        HANDLE hItem = pModelCtrlr->CreateItem(sShapeType);
        if (hItem != NULL)
        {
          CString sFmt;
          sFmt.Format(_T("New %s"), sShapeType);
          CString sLabel(pModelCtrlr->AssureUniqueItemAttrValue(hDrawing, 
                         _T("label"), (LPCTSTR)sFmt));
          (void)pModelCtrlr->SetItemAttrValue( hItem, 
                               _T("label"), (LPCTSTR)sLabel);
          
          DWORD dw = ::GetMessagePos();
          CPoint pt(GET_X_LPARAM((LPARAM)dw), GET_Y_LPARAM((LPARAM)dw));
          CRect r(pt.x, pt.y, pt.x + 350, pt.y + 200);
          SbjCore::Mvc::Model::Rect::SetItemValue(pModelCtrlr, hItem, r);
          
          (void)pModelCtrlr->SetItemAttrValue( hItem, _T("borderRGB"), RGB(0,0,0));
          (void)pModelCtrlr->SetItemAttrValue( hItem, _T("borderWidth"), 5);
          (void)pModelCtrlr->SetItemAttrValue( hItem, _T("fillRGB"), RGB(255,255,255));
          
          bRslt = pModelCtrlr->InsertChild(hItem, hDrawing, NULL);
        }
      }
      return bRslt;
    }
  
    virtual bool OnHandleCmdUI(CCmdUI* pCmdUI)
    {
      bool bRslt = true;
  
      pCmdUI->Enable(true);
  
      return bRslt;
    };
  
  };

}

struct ShapesDocImpl : public XmlMvc::XmlDoc::Controller
{
  HANDLE hDrawing;
  localNS::InsertShapeHandler theInsertRectangleHandler;
  localNS::InsertShapeHandler theInsertEllipseHandler;  

  ShapesDocImpl() :
    hDrawing(NULL),
    theInsertRectangleHandler(_T("Rectangle")),
    theInsertEllipseHandler(_T("Ellipse"))
  {
    SetDocElementName(_T("Shapes"));
    AddHandler(ID_CMDS_NEWRECTANGLE, &theInsertRectangleHandler);
    AddHandler(ID_CMDS_NEWELLIPSE, &theInsertEllipseHandler);
  }
  virtual ~ShapesDocImpl()
  {
  }

  virtual BOOL OnNewDocument()
  {
    XmlMvc::XmlDoc::Controller::OnNewDocument();
    HANDLE hItem = CreateItem(_T("Drawing"));
    SetItemAttrValue(hItem, _T("name"), _T("New Drawing"));
    BOOL bRslt = InsertChild(hItem, 
         SbjCore::Mvc::Model::Controller::GetItemRoot(), NULL, false);
    return bRslt;
  }
    
};

Since the XML Model knows only that the DocumentElement type is "Shapes", and knows nothing of the rest of the Model content, a CmdMsgHandler has been added to handle requests for new ModelItems of type "Rectangle" and "Ellipse". It is then added to the ShapesDocImpl controller, once for each type. The second addition is the OnNewDocument override to create the ModelItem "Drawing" which is a container for all the "Rectangle" and "Ellipse" ModelItem children.

Conclusion

In this article, I have discussed the details of how the MVC Framework presents a Model abstraction to the application while hiding the data source. Of course, this article hasn't covered the application Views and how the MVC Framework supports them (the topic of future articles), but similar support exists which minimize the changes and additions required to the associated application classes. Although I have yet to cover these issues, the code accompanying the article contains all the code necessary for the current implementation of the Framework and the sample Shapes application. Once again, as I said in concluding the first article, run the Shapes application, explore the code, and please offer any feedback you'd like to contribute.

TODO

  • Add support for all common controls
  • Add support for third-party controls
  • Add support for CView derivatives
  • Add support for GDI+ and perhaps OpenGL
  • Implement CDocTemplate derivatives for CDockablePane
  • And obviously, continue to generalize and refactor

History

  • 2008 Nov. 24 - Original article submitted
  • 2009 Mar. 19 - Added link to follow up article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
SBJ
United States United States
Real name is Steve Johnson. Programming since 1979. Started on a Heathkit Micro with a DEC LSI-11 and UCSD Pascal. Moved to PCs & DOS as soon as Turbo Pascal became available. Did some Assembly, ISR, TSR etc. All this while working for a Manufacturing Co. for 8 years. Had my own solo Co. doing barcode labeling software for 4 years (terrible business man, all I wanted to do was code). Since then working for various software companies. Moved to Windows around the time of 3.1 with Borland C then C++. Then on to VC++ and MFC, and just about anything I could get my hands on or had to learn for my job, and been at it ever since. Of course recently I've been playing with .NET, ASP, C#, WPF etc.

Comments and Discussions

 
GeneralProblems on XP with Redist SP1 Pin
SBJ17-Dec-08 12:51
SBJ17-Dec-08 12:51 
GeneralRe: Problems on XP with Redist SP1 Pin
SBJ6-Jan-09 10:07
SBJ6-Jan-09 10:07 
GeneralCannot run exe Pin
Jim Jim27-Nov-08 8:35
Jim Jim27-Nov-08 8:35 
GeneralRe: Cannot run exe Pin
SBJ27-Nov-08 11:58
SBJ27-Nov-08 11:58 
GeneralRe: Cannot run exe Pin
SBJ28-Nov-08 8:28
SBJ28-Nov-08 8:28 
GeneralRe: Cannot run exe Pin
Hans Dietrich3-Dec-08 5:42
mentorHans Dietrich3-Dec-08 5:42 
GeneralRe: Cannot run exe Pin
SBJ3-Dec-08 6:02
SBJ3-Dec-08 6:02 
GeneralRe: Cannot run exe Pin
Hans Dietrich3-Dec-08 6:13
mentorHans Dietrich3-Dec-08 6:13 
GeneralRe: Cannot run exe Pin
SBJ3-Dec-08 6:26
SBJ3-Dec-08 6:26 
GeneralRe: Cannot run exe Pin
Jim Crafton17-Dec-08 10:49
Jim Crafton17-Dec-08 10:49 
GeneralRe: Cannot run exe Pin
SBJ17-Dec-08 11:23
SBJ17-Dec-08 11:23 
GeneralRe: Cannot run exe Pin
Jim Crafton17-Dec-08 11:30
Jim Crafton17-Dec-08 11:30 
GeneralRe: Cannot run exe Pin
SBJ17-Dec-08 12:42
SBJ17-Dec-08 12:42 
GeneralRe: Cannot run exe Pin
SBJ17-Dec-08 12:53
SBJ17-Dec-08 12:53 
GeneralRe: Cannot run exe Pin
SBJ8-Feb-09 9:17
SBJ8-Feb-09 9:17 
GeneralBug - New Shapes are created but not seen Pin
SBJ25-Nov-08 3:57
SBJ25-Nov-08 3:57 
GeneralRe: Bug - New Shapes are created but not seen Pin
SBJ25-Nov-08 12:34
SBJ25-Nov-08 12:34 
GeneralNice stuff Pin
gediner25-Nov-08 3:05
gediner25-Nov-08 3:05 
GeneralRe: Nice stuff Pin
SBJ25-Nov-08 3:53
SBJ25-Nov-08 3:53 
GeneralI have been waiting for this !! Pin
arif.setiawan25-Nov-08 1:57
arif.setiawan25-Nov-08 1:57 
GeneralRe: I have been waiting for this !! Pin
SBJ25-Nov-08 3:53
SBJ25-Nov-08 3:53 
GeneralRe: I have been waiting for this !! Pin
Rome Singh7-Feb-09 3:47
Rome Singh7-Feb-09 3:47 
GeneralRe: I have been waiting for this !! Pin
SBJ7-Feb-09 8:30
SBJ7-Feb-09 8:30 
GeneralRe: I have been waiting for this !! Pin
Rome Singh7-Feb-09 9:25
Rome Singh7-Feb-09 9:25 
GeneralRe: I have been waiting for this !! Pin
SBJ8-Feb-09 9:15
SBJ8-Feb-09 9:15 
To make it easier to ask questions, here is my email, sbj@sbjcat.com. Of course if you think the questions would serve to augment the article, continue to post and I will post answers to them. If you truly do intend to use it in a large application, perhaps we could collaborate. If you are interested, please email me.

Thanks again,
Steve

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.