SBJ MVC 框架 - 模型,从抽象到实现
一个与 MFC 文档/视图架构集成的模型-视图-控制器框架

致所有感兴趣的读者...
我已发布本系列中描述 MVC 框架的后续文章。
目录
引言
在阅读本文之前,您需要阅读我上个月发布的介绍性文章《MFC 的模型-视图-控制器实现简介》。在那篇文章中,我介绍了与 MFC 文档/视图架构集成的底层 MVC 框架类,并讨论了框架消息处理和事件架构的基础知识。
在本文中,我计划详细描述模型如何与 MFC CDocument
类关联,以及它如何被抽象化,从而使实际数据源对应用程序和框架的其余部分保持透明。
老实说,模型抽象在第一篇文章中并不存在,而是实现了针对 XML 的特定框架扩展。正如我当时所说,我试图尽可能地泛化代码,但“有时,我看不到代码中可以分解到更低层次的通用性”。模型抽象就是一个例子,我需要先具体地编写代码,然后才能看到如何将其泛化。对于感兴趣的读者,可以比较第一篇文章中的 XmlMvc.dll 代码与现在作为 MVC 框架一部分存在于 SbjCore.dll 中的抽象代码。当前的 XmlMvc.dll 仅包含提供文件 IO 以及创建、获取和设置 XML 元素和属性的代码。所有其他代码都已被分解到 MVC 框架本身。
源代码
本文提供的源代码包含在一个名为 SbjDev 的 VS2008 解决方案中,其中有三个项目。虽然这三个项目在第一篇文章中就已存在,但由于模型抽象的重构,项目内容已发生巨大变化。当然,示例应用程序 Shapes.exe 也已重写以利用此重构。
我在第一篇文章中忘了提及,DLL 的命名反映了它们的版本。我使用了一种似乎在第三方 MFC 扩展供应商中很常见的约定。
<Project Name><Major/Minor Version><VC++ Major/Minor Version>.dll
下面,我列出了每个项目生成的当前文件名:
- SbjCore - SbjCore2090.dll - 包含 MVC 框架的基础 DLL
- XmlMvc - XmlMvc2090.dll - 包含具体 XML 模型实现的 DLL
- Shapes - Shapes.exe - 示例 EXE
抽象模型
概述
MVC 框架没有声明一个名为 Model
的类。相反,模型被认为是将呈现给应用程序用户的领域或特定于应用程序的数据。数据源,无论是数据库、XML 文档还是其他来源,通常都会附带一个用于访问该数据的程序或服务。对于本文提供的 XML 模型,它是文档对象模型(DOM)的 MSXML6 实现。
框架声明了一个 abstract
接口,通过它以一种通用的方式与任何给定数据源的访问程序进行通信。对于每个受支持的数据源,必须提供该接口的具体实现。MSXML6 DOM 接口在本文附带的 XmlMvc2090.dll 中实现。
无论具体的模型实现如何,抽象模型都提供两个非常重要的服务。
- 修改通知
- 撤销-重做支持
在讨论实现时,我将进一步讨论这些内容。
模型结构
框架将记录/字段、行/列、元素/属性等基本概念标识为 ModelItem/Attribute,这仅仅是因为我试图在命名约定中尽可能通用,不将名称与现有的模型实现联系得太紧密。
ModelItem 或 Attribute 的类型由一个字符串字面量标识。例如,一个 ModelItem 的类型可能是
"Person"
而一个类型为“Person
”的 ModelItem 可能有一个属性为
"lastName"
ModelItem 的唯一实例由一个唯一的 ModelItemHandle(又是一个通用术语)来标识,它被实现为,你猜对了,一个 HANDLE
数据类型。这样做的原因在于它不会引入新的依赖或耦合,因为它是 Windows API 的一部分,并且我可以假设任何模型的具体实现都能够将其使用的任何实际唯一标识符转换为 HANDLE
。
模型要求 ModelItems 是分层的,因此任何 ModelItem 都可以有关联的子 ModelItems。即使实际的具体模型只包含一个 ModelItems 列表,并且这些项都没有子项,抽象模型也会声明一个 ModelItemRoot,所有其他 ModelItems 都派生自它。ModelItemRoot HANDLE
的值定义为 0xFFFFFFFF
。ModelItemRoot 在具体模型中如何解释取决于实现。在 XML 模型实现中,它显然被解释为 XMLDocumentElement
。
抽象模型的所有组件都包含在命名空间 SbjCore::Mvc::Model
中。
类 Model::Controller
在 MVC 框架中,与模型数据的接口是通过 abstract
基类 Model::Controller
实现的。与框架中其他 Controller
类不同,Model::Controller
类不直接服务于受控的 CmdTarget
或 CWnd
。相反,它是本文后面描述的 Doc::Controller
类的基类。
Model::Controller
声明了一组用于访问和修改模型的 private
纯虚数据访问方法。这些纯虚方法必须在针对特定数据源的 конкретный Model::Controller
派生类中实现。
从下面的列表中可以看到,Model::Controller
的内容远不止声明纯虚方法那么简单。它有一个 static
方法 GetItemRoot
,如概述中所讨论的,它返回硬编码的 ModelItemRoot HANDLE
。此外,还有一个返回 SbjCore::UndoRedo::Manager
的方法,用于管理当前选定的 ModelItems 的方法,以及调用派生类中实现的相应 private
虚方法的 public
数据访问方法。在 Model::Controller
列表之后,我将进一步讨论这些内容。
//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();
}
当前选定的 ModelItems
我认为“当前选定”的 ModelItems 的概念是直观的(例如,用户在某个视图中点击了一个项目或一组项目的表示)。Model::Controller
维护一个 vector
,框架中的各种 Controller
类通过以下三个方法访问它。
GetSelectedItems
SetSelectedItems
ClearSelectedItems
数据访问方法
大多数数据访问方法的功能都相当不言自明;然而,下面是一个列表,附有对其用途的简短描述。当然,实际的实现由派生类通过其相应的虚方法提供。
CreateItem
- 返回一个新创建的 ModelItem 的HANDLE
InsertChild
- 插入一个子 ModelItemRemoveChild
- 移除一个子 ModelItemGetItemAttrValue
- 获取一个属性值SetItemAttrValue
- 设置一个属性值AssureUniqueItemAttrValue
- 确保一个属性值在其同级中是唯一的GetItemTypeName
- 返回一个 ModelItem 的类型GetParentItem
- 返回一个 ModelItem 的父级的HANDLE
GetItemChildren
- 返回一个子 ModelItemHANDLE
的列表GetItemAttrNames
- 返回一个 ModelItem 的属性名称列表GetItemAttrName
- 按索引返回一个属性名称CookAttrName
- 提供一个属性类型名称的格式化版本(例如,lastName 返回为 Last Name)
除了调用它们相应的纯虚方法外,三个修改模型的数据访问方法中的每一个都提供可选的撤销-重做支持。这三个方法是:
InsertChild
RemoveChild
SetItemAttrValue
InsertChild
和 RemoveChild
总是触发事件(如第一篇文章中所讨论的)来指示模型的更改,而 SetItemAttrValue
支持选择性地触发事件。这允许在单个 Event
触发下修改多个属性。每个方法都遵循相同的通用格式,因此我不会分别讨论每一个,而是使用 SetItemAttrValue
方法作为示例,因为它包含了两种可选支持的代码。在概述该方法的实现之后,我将更详细地描述撤销-重做支持和事件触发。
//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;
}
在调用相应的纯虚方法 OnSetItemAttrValue
之前,SetItemAttrValue
会检查 bAddToUndoRedo
参数的值,如果为 true
,则保存指定属性的当前值,以便用于撤销该属性值的更改。如果纯虚方法返回 true
,并且同样基于 bAddToUndoRedo
的 true
值,SetItemAttrValue
会声明并实现 SbjCore::UndoRedo::Handler
类的一个派生类。请注意,UndoRedoHandler
类调用 SetItemAttrValue
来实现撤销和重做功能,只是在这里,bAddToUndoRedo
参数被设置为 false
。然后,它动态分配 UndoRedoHandler
类的一个实例,并将其推入由 SbjCore::UndoRedo::Manager
维护的撤销堆栈中。
最后,有两个 Event
类被选择性地触发;一个指示模型的具体变化,第二个指示 CDocument
类被修改的一般情况,默认情况下,它会调用 CDocument::SetModifiedFlag
并传入 Doc::Events::DocModified
参数的值。
撤销-重做支持
撤销-重做架构实际上并非 MVC 框架的一部分。与第一篇文章中描述的事件架构一样,它也是通用的,并且可以在没有任何框架依赖的情况下使用。该架构的两个组件,Manager
和 Handler
,在命名空间 SbjCore::UndoRedo
下声明。我将首先讨论 UndoRedo::Handler
,因为您已在上一节中接触过它。
类 UndoRedo::Handler
UndoRedo::Handler
是一个纯虚基类,在 Model::Controller
数据访问方法中定义的每个 UndoRedoHandler
类都派生自它。这些派生类处理执行任何撤销或重做操作的实际细节。派生类应将其析构函数设为 protected
,以强制动态分配,因为这些对象会被推入 UndoRedo::Manager
的堆栈中,一旦进入,管理器会在 UndoRedo::Handler
不再作为撤销-重做过程的一部分需要时处理其删除。
请注意,UndoRedo::Handler
有一个返回 HandlerName 的方法。此名称被添加到由 UndoRedo::Manager
类维护的处理程序描述列表中,可以查询该列表,为用户提供可供撤销或重做的多个操作列表。
//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;
};
类 UndoRedo::Manager
UndoRedo::Manager
类维护两个内部堆栈,一个用于撤销,一个用于重做。要为某个操作或命令提供撤销-重做处理,需要在堆上分配一个 UndoRedo::Handler
派生类的实例,并通过调用 Push
将其推入管理器的撤销堆栈。当调用 Manager::Undo
时,UndoRedo::Handler
会从撤销堆栈中弹出,其 Undo
方法被执行,然后 UndoRedo::Handler
被推入重做堆栈。如果随后调用 Redo
,则 Handler
会从重做堆栈中弹出,其 Redo
方法被执行,并且 UndoRedo::Handler
会被返回到撤销堆栈。如果发出了新的 Push
调用,则重做堆栈将被清空。
//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;
};
正如在讨论 UndoRedo::Handler
时提到的,可以通过调用 Manager::GetUndoList
或 Manager::GetRedoList
来检索每个堆栈上 Handler
对象的描述列表。MFC Feature Pack 中的 CMFCRibbonUndoButton
类利用了这一点,通过其下拉列表框,为用户提供一次性处理多个操作的方法。作为补充,Manager::Undo
和 Manager::Redo
方法可以传递一个要处理的操作数量。还提供了用于附加 CMFCRibbonUndoButton
实例的方法,分别用于撤销和重做。关于 CMFCRibbonUndoButton
的实现稍后会详细介绍。
Model::Controller
实例化了一个 SbjCore::UndoRedo::Manager
的实例,可以通过以下方法访问:
//Project Location: SbjCore/Mvc/ModelController.cpp
SbjCore::UndoRedo::Manager* Controller::GetUndoRedoMgr() const;
UndoRedo::Manager
由用于 ID_EDIT_UNDO
和 ID_EDIT_REDO
的 CmdMsgHandler
类访问。Model::ControllerImpl
包含这些 CmdMsgHandler
类的实例,并在其构造函数中将它们附加到 Model::Controller
。每个 CmdMsgHandler
基本相同,区别在于访问哪个 UndoRedo::Manager
堆栈。为简洁起见,我只列出 ID_EDIT_UNDO
的 CmdMsgHandler
。
//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;
}
};
OnUndoHandler
访问分配给 CMFCRibbonUndoButton
的 UndoRedo::Manager
,获取用户选择要撤销的操作数量,并调用 UndoRedo::Manager
对象的 Undo
方法。类似地,在 OnHandleCmdUI
方法中,它查询 UndoRedo::Manager
的启用状态。
在撤销-重做架构中还有另一个参与者,那就是 Ribbon::UndoRedoMenuHandler
。它附加到 SbjCore::Mvc::FrameWndExController
类,该类充当 Shapes 应用程序的受控 CMainFrame
的 Controller
。下面是其 OnHandleWndMsg
方法的实现。
类 Ribbon::UndoRedoMenuHandler
//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;
}
当用户点击按钮的下拉箭头部分时,UndoRedoMenuHandler
会处理 AFX_WM_ON_BEFORE_SHOW_RIBBON_ITEM_MENU
这个已注册的 Windows 消息。一旦被调用,它会查询 CMFCRibbonBaseElement
的消息 ID,同时处理 ID_EDIT_UNDO
和 ID_EDIT_REDO
消息。通过访问当前的 Model::Controller
,它获取 UndoRedo::Manager
,分配按钮,并向 UndoRedo::Manager
查询 UndoRedo::Handler
描述列表,然后填充其下拉列表框。一旦用户选择要处理的操作数量,相应的 ID_EDIT_UNDO
或 ID_EDIT_REDO CmdMsgHandler
就会被调用。
事件触发支持
命名空间 Model
包含许多预定义的事件 ID 和事件派生,用于处理模型变更的通知。在本文前面 SetItemAttrValue
方法的清单中已经看到了一个例子。这些都包含在项目位置:SbjCore/Mvc/Model/ModelEvents.h。下面是可用的 Model::Event
ID 列表,以及简短描述:
EVID_ITEM_INSERTING
- 由Model::Controller::InsertChild
在插入项之前触发EVID_ITEM_INSERTED
- 由Model::Controller::InsertChild
在插入项之后触发EVID_ITEM_REMOVING
- 由Model::Controller::RemoveChild
在移除项之前触发EVID_ITEM_REMOVED
- 由Model::Controller::RemoveChild
在移除项之后触发EVID_ITEM_CHANGING
- 由Model::Controller::SetItemAttrValue
在更改项之前触发EVID_ITEM_CHANGED
- 由Model::Controller::SetItemAttrValue
在更改项之后触发EVID_SELITEM_CHANGED
- 由Model::Controller::SetSelectedItems
在所选项发生变化时触发
我从未实际处理过以 "ING" 结尾的事件;但是,我可以预见到在这些操作实际执行之前,可能会有希望中断它们的需求。这些事件将在未来的文章中重新讨论,届时我将讨论它们如何由 MVC 框架中的各种视图和控件来处理。
抽象文档
第一篇文章讨论了受控 CmdTarget
和 CWnd
类的概念。Doc::ControlledDocument
类派生自 ControlledCmdTarget<CDocument>
,并通过其附带的、派生自 Model::Controller
的 Doc::Controller
类,为具体的模型实现提供了实际的基础。对于 XML 的情况,这个基础是 XmlMvc::XmlDoc::Controller
类,我将在下一节中讨论。
您可能已经注意到,在讨论抽象模型及其 Model::Controller
时,没有提到文件。由于文件在 MFC 中是由 CDocTemplate
和 CDocument
类处理的,因此在 Doc::Controller
中引入它们似乎更合适。这也意味着该模型可以用于非基于文件的情况。
在 MVC 框架中,实际的创建、打开和保存文件操作都委托给了具体的 Doc::Controller
派生类。将这些任务的责任传递给 Doc::Controller
派生类是由 Doc::ControlledDocument
处理的。它重写了以下 CDocument
方法:
//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);
并调用同名的 Doc::Controller
纯虚方法。当然,具体的 Doc::Controller
派生类会根据其支持的底层模型适当地实现这些方法。如果虚方法返回 true
,Doc::ControlledDocument
会通过触发下面列出的相应事件来通知观察者。
EVID_FILE_NEW
- 由Doc::ControlledDocument::OnNewDocument
触发EVID_FILE_OPEN
- 由Doc::ControlledDocument::OnOpenDocument
触发EVID_FILE_SAVE
- 由Doc::ControlledDocument::OnSaveDocument
触发EVID_DOC_MODIFIED
- 由以上三者触发,参数为false
具体的 XML 模型/文档
我假设您熟悉 XML 文档的结构和 MSXML DOM 接口。与抽象模型的关系如下:
- ModelItemRoot - 指向根
DocumentElement
的特定IXMLDOMElement
接口 - ModelItem - 一个
IXMLDOMElement
接口 - Attribute - 一个
IXMLDOMAttribute
接口
在深入探讨 XmlDoc::Controller
如何实现数据和文件访问方法之前,我想讨论一下 XmlDoc::Controller
如何实现 ModeItem HANDLE
。
ModelItem HANDLE 的创建与分配
据我所知,XML 文档中的每个元素都没有天然的唯一标识符,因此 XmlDoc::Controller
制造了一个。它通过在每个元素被创建或首次访问时,向其注入一个具有唯一值的属性来实现。除了唯一标识每个元素外,控制器还必须跟踪下一个可用的唯一值。它通过向 XML DocumentElement
注入一个属性来包含此值。这两个属性分别命名为 "sbjHandle
" 和 "nextSbjHandle
"。我认为这些名称与任何实际的属性名称冲突的可能性很小。下面的示例 Shapes.xml 展示了这些属性的样子。"nextSbjHandle
" 属性的初始值为 0xF0000001;但是,它在文件中显示为十进制值,"sbjHandle
" 属性也是如此。
<!--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 到 HANDLE 和 HANDLE 到 IXMLDOMElement
为了实现数据和文件访问方法,XmlDoc::Controller
需要能够根据给定的 HANDLE
检索 IXMLDOMElement
接口,并根据给定的 IXMLDOMElement
检索 HANDLE
。控制器在其 ControllerImpl
中为此提供的两个方法如下所列:
//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;
}
GetHandleFromNode
方法查询传入的 MSXML2::IXMLDOMElementPtr
的 "sbjHandle
" 属性。如果未找到,则返回并注入 DocumentElement
的 "nextSbjHandle
" 属性的值,最后 "nextSbjHandle
" 属性的值会递增。
从 HANDLE
检索 IXMLDOMElement
则要复杂一些。如你所见,通常情况下,可以使用 XPath 查询文档来找到元素;然而,当创建一个新元素时,它会被赋予一个 HANDLE
。但是,由于它尚未插入到文档中,XPath 查询将会失败。事实上,OnInsertChild
方法需要从 HANDLE
中检索元素才能真正插入它。为了实现这一点,控制器维护了一个从 HANDLE
到 MSXML2::IXMLDOMElementPtr
的映射。我不确定是否将所有元素都放入映射中会更好,以避免使用 XPath 查询的开销,但目前我打算保持现状。这在未来可能会改变。
实现 Model::Controller 数据和 Doc::Controller 文件访问方法
我不会逐一介绍每个方法,因为它们大多遵循相同的形式:包装对 DOM 的调用,并在 IXMLDOMElement
和 HANDLE
之间进行转换。为作说明,我将列出 OnInsertChild
方法。
//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);
}
注意 _com_error
被捕获了。所有智能指针 DOM 例程在遇到错误时都会抛出它。catch (...)
主要作为开发工具存在,用于调查任何意外的异常。
另一个值得关注的数据访问方法是 OnAssureUniqueItemAttrValue
。
//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;
}
当一个属性值作为唯一标识符呈现给用户时,会使用此方法。例如,在 Shapes.exe 应用程序中,当创建一个新的矩形时,它有一个默认的“label
”属性,值为“New Rectangle”。如果用户在未更改第一个矩形的默认“label
”的情况下创建了第二个矩形,该方法将推断出“New Rectangle”已被使用,并将第二个矩形的“label
”更改为“New Rectangle (2)”。
将模型应用于应用程序
至此,我想您将开始看到 MVC 框架的真正好处。要将模型应用于 Shapes 应用程序,只需对原始由 MFC AppWizard 生成的 ShapesDoc
类进行少量修改。
第一步是控制派生自 CDocument
的 ShapesDoc
类。以下是 ShapesDoc.h 文件的列表。请注意,对原始文件的修改以粗体标记。
ShapesDoc.h
#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;
};
您会注意到,CDocument
基类已被替换为 SbjCore::Mvc::ControlledDocument
。如第一篇文章所讨论的,这使得分配的 Controller
类能够优先处理发送给 ShapesDoc
的任何 WM_COMMAND
消息。typedef SbjCore::Mvc::ControlledDocument t_Base
只是为了方便,这样在 .cpp 文件中对基类的引用就是 t_Base
,如果实际基类发生变化,它会自动更新。OnNewDocument
和 Serialize
被移除(实际上被移交给了 SbjCore::Mvc::ControlledDocument
),并添加了 private struct ShapesDocImpl*
。这个 private struct
是隐藏实现细节的常用方法,您几乎会在每个 SbjCore
和 XmlMvc
类中看到它。正如您将在下一步替换 Shapes.cpp 代码时看到的,它还兼作 ShapesDoc
的 Controller
类。
接下来,必须修改 ShapesDoc.cpp 代码。我将分两步展示:首先是与 XML 模型实现的基本附加,然后是一些特定于应用程序的附加代码。
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()
{
}
};
// 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
结构体 ShapesDocImpl
被声明为 XmlMvc::XmlDoc::Controller
的派生类。在其构造函数中,调用了 XmlMvc::XmlDoc::Controller::SetDocElementName
,告诉控制器 DocumentElement
类型应该是什么,在本例中是 "Shapes
"。这用于验证现有文件,并在新文件中创建 DocumentElement
。这是应用程序中对 XmlMvc::XmlDoc::Controller
的唯一引用。所有其他引用都是针对底层的模型抽象 SbjCore::Mvc::Model::Controller
。
在 ShapesDoc
类中,已向构造函数和析构函数添加了代码,以创建和删除 m_pImpl
实例(即 ShapesDocImpl
结构体),并将其控制器指定为 ShapesDoc
构造函数。
第二组修改是针对 ShapesDocImpl
控制器以及添加特定于 Shapes 应用程序的 CmdMsgHandler
类。
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;
}
};
由于 XML 模型只知道 DocumentElement
的类型是 "Shapes
",而对模型的其余内容一无所知,因此添加了一个 CmdMsgHandler
来处理对类型为 "Rectangle
" 和 "Ellipse
" 的新 ModelItems 的请求。然后,它被添加到 ShapesDocImpl
控制器中,每种类型一次。第二个添加是重写 OnNewDocument
,以创建 ModelItem "Drawing",它是所有 "Rectangle
" 和 "Ellipse
" ModelItem 子项的容器。
结论
在本文中,我讨论了 MVC 框架如何在隐藏数据源的同时向应用程序呈现一个模型抽象的细节。当然,本文并未涵盖应用程序视图以及 MVC 框架如何支持它们(这是未来文章的主题),但存在类似的支持,可以最大限度地减少对相关应用程序类的更改和添加。尽管我尚未涉及这些问题,但本文附带的代码包含了框架当前实现和示例 Shapes 应用程序所需的所有代码。再次,正如我在第一篇文章结尾所说,请运行 Shapes 应用程序,探索代码,并欢迎您提供任何反馈。
待办事项
- 添加对所有通用控件的支持
- 添加对第三方控件的支持
- 添加对
CView
派生类的支持 - 添加对 GDI+ 以及可能的 OpenGL 的支持
- 为
CDockablePane
实现CDocTemplate
派生类 - 并且,显然,继续泛化和重构
历史
- 2008年11月24日 - 提交原始文章
- 2009年3月19日 - 添加了后续文章的链接