For Those Of You Who Are Interested...
I have posted the following article in the series describing the MVC Framework.
Table of Contents
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 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
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.
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 ModelItemInsertChild
- inserts a child ModelItemRemoveChild
- removes a child ModelItemGetItemAttrValue
- 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 HANDLE
sGetItemAttrNames
- returns a list of Attribute names for a ModelItemGetItemAttrName
- 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.
bool Controller::SetItemAttrValue(
const HANDLE hItem,
const CString& sAttrName,
const _variant_t& val,
bool bAddToUndoRedo ,
bool bFireEvents )
{
_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.
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.
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:
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
.
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
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 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:
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
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.
="1.0"="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:
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.
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
.
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)".
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
#pragma once
struct ShapesDocImpl;
class ShapesDoc : public SbjCore::Mvc::ControlledDocument
{
typedef SbjCore::Mvc::ControlledDocument t_Base;
protected: ShapesDoc();
DECLARE_DYNCREATE(ShapesDoc)
public:
virtual ~ShapesDoc();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
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
#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()
{
}
};
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.
namespace localNS
{
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.
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.
- 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
- 2008 Nov. 24 - Original article submitted
- 2009 Mar. 19 - Added link to follow up article