65.9K
CodeProject 正在变化。 阅读更多。
Home

SBJ MVC 框架 - 模型,从抽象到实现

starIconstarIconstarIconstarIconstarIcon

5.00/5 (18投票s)

2008年11月24日

CPOL

19分钟阅读

viewsIcon

117468

downloadIcon

1382

一个与 MFC 文档/视图架构集成的模型-视图-控制器框架

SbjDevSrc201

致所有感兴趣的读者...

我已发布本系列中描述 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,这仅仅是因为我试图在命名约定中尽可能通用,不将名称与现有的模型实现联系得太紧密。

ModelItemAttribute类型由一个字符串字面量标识。例如,一个 ModelItem 的类型可能是

"Person"

而一个类型为“Person”的 ModelItem 可能有一个属性

"lastName"

ModelItem 的唯一实例由一个唯一的 ModelItemHandle(又是一个通用术语)来标识,它被实现为,你猜对了,一个 HANDLE 数据类型。这样做的原因在于它不会引入新的依赖或耦合,因为它是 Windows API 的一部分,并且我可以假设任何模型的具体实现都能够将其使用的任何实际唯一标识符转换为 HANDLE

模型要求 ModelItems 是分层的,因此任何 ModelItem 都可以有关联的子 ModelItems。即使实际的具体模型只包含一个 ModelItems 列表,并且这些项都没有子项,抽象模型也会声明一个 ModelItemRoot,所有其他 ModelItems 都派生自它。ModelItemRoot HANDLE 的值定义为 0xFFFFFFFFModelItemRoot 在具体模型中如何解释取决于实现。在 XML 模型实现中,它显然被解释为 XMLDocumentElement

抽象模型的所有组件都包含在命名空间 SbjCore::Mvc::Model 中。

类 Model::Controller

在 MVC 框架中,与模型数据的接口是通过 abstract 基类 Model::Controller 实现的。与框架中其他 Controller 类不同,Model::Controller 类不直接服务于受控的 CmdTargetCWnd。相反,它是本文后面描述的 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 - 返回一个新创建的 ModelItemHANDLE
  • InsertChild - 插入一个子 ModelItem
  • RemoveChild - 移除一个子 ModelItem
  • GetItemAttrValue - 获取一个属性
  • SetItemAttrValue - 设置一个属性
  • AssureUniqueItemAttrValue - 确保一个属性值在其同级中是唯一的
  • GetItemTypeName - 返回一个 ModelItem 的类型
  • GetParentItem - 返回一个 ModelItem 的父级的 HANDLE
  • GetItemChildren - 返回一个子 ModelItem HANDLE 的列表
  • GetItemAttrNames - 返回一个 ModelItem属性名称列表
  • GetItemAttrName - 按索引返回一个属性名称
  • CookAttrName - 提供一个属性类型名称的格式化版本(例如,lastName 返回为 Last Name)

除了调用它们相应的纯虚方法外,三个修改模型的数据访问方法中的每一个都提供可选的撤销-重做支持。这三个方法是:

  • InsertChild
  • RemoveChild
  • SetItemAttrValue

InsertChildRemoveChild 总是触发事件(如第一篇文章中所讨论的)来指示模型的更改,而 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,并且同样基于 bAddToUndoRedotrue 值,SetItemAttrValue 会声明并实现 SbjCore::UndoRedo::Handler 类的一个派生类。请注意,UndoRedoHandler 类调用 SetItemAttrValue 来实现撤销和重做功能,只是在这里,bAddToUndoRedo 参数被设置为 false。然后,它动态分配 UndoRedoHandler 类的一个实例,并将其推入由 SbjCore::UndoRedo::Manager 维护的撤销堆栈中。

最后,有两个 Event 类被选择性地触发;一个指示模型的具体变化,第二个指示 CDocument 类被修改的一般情况,默认情况下,它会调用 CDocument::SetModifiedFlag 并传入 Doc::Events::DocModified 参数的值。

撤销-重做支持

撤销-重做架构实际上并非 MVC 框架的一部分。与第一篇文章中描述的事件架构一样,它也是通用的,并且可以在没有任何框架依赖的情况下使用。该架构的两个组件,ManagerHandler,在命名空间 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::GetUndoListManager::GetRedoList 来检索每个堆栈上 Handler 对象的描述列表。MFC Feature Pack 中的 CMFCRibbonUndoButton 类利用了这一点,通过其下拉列表框,为用户提供一次性处理多个操作的方法。作为补充,Manager::UndoManager::Redo 方法可以传递一个要处理的操作数量。还提供了用于附加 CMFCRibbonUndoButton 实例的方法,分别用于撤销和重做。关于 CMFCRibbonUndoButton 的实现稍后会详细介绍。

Model::Controller 实例化了一个 SbjCore::UndoRedo::Manager 的实例,可以通过以下方法访问:

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

UndoRedo::Manager 由用于 ID_EDIT_UNDOID_EDIT_REDOCmdMsgHandler 类访问。Model::ControllerImpl 包含这些 CmdMsgHandler 类的实例,并在其构造函数中将它们附加到 Model::Controller。每个 CmdMsgHandler 基本相同,区别在于访问哪个 UndoRedo::Manager 堆栈。为简洁起见,我只列出 ID_EDIT_UNDOCmdMsgHandler

//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 访问分配给 CMFCRibbonUndoButtonUndoRedo::Manager,获取用户选择要撤销的操作数量,并调用 UndoRedo::Manager 对象的 Undo 方法。类似地,在 OnHandleCmdUI 方法中,它查询 UndoRedo::Manager 的启用状态。

在撤销-重做架构中还有另一个参与者,那就是 Ribbon::UndoRedoMenuHandler。它附加到 SbjCore::Mvc::FrameWndExController 类,该类充当 Shapes 应用程序的受控 CMainFrameController。下面是其 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_UNDOID_EDIT_REDO 消息。通过访问当前的 Model::Controller,它获取 UndoRedo::Manager,分配按钮,并向 UndoRedo::Manager 查询 UndoRedo::Handler 描述列表,然后填充其下拉列表框。一旦用户选择要处理的操作数量,相应的 ID_EDIT_UNDOID_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 框架中的各种视图和控件来处理。

抽象文档

第一篇文章讨论了受控 CmdTargetCWnd 类的概念。Doc::ControlledDocument 类派生自 ControlledCmdTarget<CDocument>,并通过其附带的、派生自 Model::ControllerDoc::Controller 类,为具体的模型实现提供了实际的基础。对于 XML 的情况,这个基础是 XmlMvc::XmlDoc::Controller 类,我将在下一节中讨论。

您可能已经注意到,在讨论抽象模型及其 Model::Controller 时,没有提到文件。由于文件在 MFC 中是由 CDocTemplateCDocument 类处理的,因此在 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 派生类会根据其支持的底层模型适当地实现这些方法。如果虚方法返回 trueDoc::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 中检索元素才能真正插入它。为了实现这一点,控制器维护了一个从 HANDLEMSXML2::IXMLDOMElementPtr 的映射。我不确定是否将所有元素都放入映射中会更好,以避免使用 XPath 查询的开销,但目前我打算保持现状。这在未来可能会改变。

实现 Model::Controller 数据和 Doc::Controller 文件访问方法

我不会逐一介绍每个方法,因为它们大多遵循相同的形式:包装对 DOM 的调用,并在 IXMLDOMElementHANDLE 之间进行转换。为作说明,我将列出 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 类进行少量修改。

第一步是控制派生自 CDocumentShapesDoc 类。以下是 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,如果实际基类发生变化,它会自动更新。OnNewDocumentSerialize 被移除(实际上被移交给了 SbjCore::Mvc::ControlledDocument),并添加了 private struct ShapesDocImpl*。这个 private struct 是隐藏实现细节的常用方法,您几乎会在每个 SbjCoreXmlMvc 类中看到它。正如您将在下一步替换 Shapes.cpp 代码时看到的,它还兼作 ShapesDocController 类。

接下来,必须修改 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日 - 添加了后续文章的链接
© . All rights reserved.