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

MFC模型-视图-控制器实现导论

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (41投票s)

2008 年 10 月 20 日

CPOL

13分钟阅读

viewsIcon

111919

downloadIcon

4877

一个与 MFC 文档/视图架构集成的 MVC 框架简介

Shapes_small.png

对于那些感兴趣的人...

我已发布了以下系列文章,描述了 MVC 框架。

目录

引言

在模型-视图-控制器(MVC)架构模式中,模型代表应用程序数据,视图代表向用户呈现数据的可视化组件,控制器管理用户与各种输入设备的交互,并解释用户交互如何影响模型。一旦模型被改变,模型会通知视图需要更新其对用户的呈现。这是一个非常简化的解释;但是,如果您想要一个更完整的解释,有很多优秀的文章和书籍解释了 MVC 模式及其设计动机。您可以在维基百科 [^] 中找到一个非常好的来源。

任何有 MFC 经验的人都知道,其文档/视图架构是 MVC 模式的一种变体。在 MFC 中,CDocument 类代表模型,而 CView 类同时代表视图和控制器。显然,本文介绍的 Model-View-Controller 框架将把视图和控制器的分离带回 MFC 应用程序。

我已将 MVC 框架的某些方面用于具有多年遗留代码的现有 MFC 应用程序,以及使用新的 MFC Feature Pack AppWizard 创建的应用程序。我想强调的是,该框架不会破坏现有的文档/视图架构,并且可以随着时间的推移以不显眼的方式实现。旧代码可以逐步重构以使用新的 MVC 架构,同时仍然使用现有的文档/视图架构。

我并不认为该框架已完全成熟,正如您将在文章底部 TODO 部分看到的;但是,我已经完成了足够多的内容来阐述基本概念。本文通过介绍构成与 MFC 文档/视图架构集成的框架基础的类来介绍该框架。

源代码

本文提供的源代码包含在一个 VS2008 解决方案 SbjDev 中,该解决方案包含三个项目

  • SbjCore - 基础 DLL
  • XmlMvc - 包含支持 XML 模型的 MVC 框架扩展的 DLL
  • Shapes - 示例 EXE

我的方法是,任何可以用于任何应用程序的代码都应该放在 SbjCore 中。任何特定于 MVC 框架的 XML 实现的代码都应该放在 XmlMvc 中,只有特定于应用程序的代码才应放在应用程序中。诚然,我并不总是成功,有时我无法看到可以分解到较低级别的代码的通用性,但这始终是我的意图。

SbjCore.dll

MVC 框架是基础 DLL SbjCore.dll 的一部分。代码全部进行了命名空间处理,并且在一定程度上,命名空间的结构在它所属的 Visual Studio 项目结构中有所体现。毫不奇怪,根是命名空间 SbjCore,所有 MVC 代码都在 SbjCore::Mvc 中。除了 MVC 框架之外,SbjCore 还包含大量实用组件,其中许多受到 CodeProject 文章的启发,用于处理各种常见的编程任务,如内存、字符串、图像、剪贴板等。它还包含实现事件架构、拖放、撤销/重做等的组件。正如我所说,任何可以用于任何应用程序的内容都放在这里。

XmlMvc.dll

XmlMvc.dll 使用 Document Object Model (DOM) 的 MSXML6 实现,它为处理 XML 模型提供了理想的通用例程基础。所有 MVC 框架的 XML 扩展都包含在这个单独的 DLL 中。总的来说,XmlMvc 模型假设 XML 元素类似于数据库中的记录,而 XML 属性类似于记录的字段。通过这种方式,它能够将许多可能成为应用程序特定代码的代码进行通用化。

用于实现模型、你会使用的其他技术,如 ADO、SQL 或专有数据库,也将以同样的方式处理。

Shapes.exe

Shapes.exe 是一个使用 MFC Feature Pack AppWizard 创建的简易绘图程序,它将 MVC 框架和 XmlMvc 模型扩展与基础的 MFC 文档/视图架构集成。

提供了一个现有的 Shapes.xml 文件,以说明 Shapes 应用程序期望的 XML 文件格式。从下面的列表可以看出,在 Shapes 文档元素下有一个名为 Drawing 的元素,其中包含 RectangleEllipse 类型的元素。这些元素都有一些属性,描述了它们在应用程序主视图中显示时的外观。

Shapes.xml
<?xml version="1.0" encoding="utf-8"?>
<Shapes>
  <Drawing name="Test Drawing">
    <Rectangle label="The First Rectangle" left="250" top="250" right="750" bottom="750"
      borderRGB="255" borderWidth="9" fillRGB="128"/>
    <Rectangle label="Blue Rectangle" left="100" top="90" right="200" bottom="200"
      borderRGB="16711680" borderWidth="1" fillRGB="16711680"/>
    <Ellipse label="The First Ellipse" left="300" top="200" right="800" bottom="400"
      borderRGB="65280" borderWidth="1" fillRGB="65280"/>
  </Drawing>
</Shapes>

正如您所料,Shapes 应用程序为用户提供了添加、删除、修改和移动单个或多个选定的矩形和椭圆形的能力。正如本文开头截图所示,该应用程序包含主设计视图和两个停靠窗格:绘图的 Explorer 树视图和显示所选形状属性的属性网格。

MVC 框架

如果您使用过 MFC,您应该熟悉 CDocument 类如何通过调用每个 CView::OnUpdate 方法来通知其 CView 类列表需要更新。您还应该熟悉 CCmdTargetCWnd 类如何通过其消息映射和处理程序方法来处理消息。因此,我将不详细介绍 MFC 的实现,而是概述框架及其基础组件,重点介绍它们如何与文档/视图架构集成。

控制 CCmdTarget 和 CWnd

集成关键在于能够选择性地拦截发送到 CCmdTargetCWnd 类的 Windows 消息,然后在它们被默认的 MFC 消息映射和路由实现处理之前进行处理。这通过两个类来实现。

template<class _baseClass>
class ControlledCmdTargetT : public _baseClass
项目位置:SbjCore/Mvc/ControlledCmdTargetT.h
template<class _baseClass>
class ControlledWndT : public ControlledCmdTargetT<_baseClass>
项目位置:SbjCore/Mvc/ControlledWndT.h

ControlledCmdTargetT 类覆盖了其 CCmdTarget 派生类 _baseClassOnCmdMsg 方法,而 ControlledWndT 类则额外覆盖了其 CWnd 派生类 _baseClassOnWndMsg 方法。这些覆盖会将传入的消息首先定向到分配的 Controller 类进行处理,然后再进行标准的 MFC 消息映射处理。

通过使用模板基类,这些类可以分别派生自任何 CCmdTargetCWnd 派生类。例如...

class ControlledDocument : public ControlledCmdTargetT<CDocument>

class ControlledView : public ControlledWndT<CView>

消息处理程序类

Windows 消息的处理方式是 MFC 文档/视图架构与 MVC 框架之间的主要区别。在框架中,消息处理程序不是 CCmdTargetCWnd 类的成员方法(如在文档/视图中),而是它们自己的类。有三种基本变体,都派生自基类 MessageHandler,每种都处理不同类型的 Windows 消息。

  • MessageHandler
    • CmdMsgHandler - WM_COMMAND
    • NotifyMsgHandler - WM_NOTIFY
    • WndMsgHandler - 其他 WM_XXX

CmdMsgHandlerNotifyMsgHandlerWndMsgHandler 都是纯虚函数,而这些函数总是提供实际具体处理程序的基类。MessageHandler 基类从不直接派生。此外,消息 ID 不直接分配给处理程序,而是在添加处理程序到 Controller 时映射到处理程序。

类 MessageHandler

MessageHandlerCmdMsgHandlerNotifyMsgHandlerWndMsgHandler 类提供基础。除了为派生类提供通用基类外,MessageHandler 还提供了一个链接机制,以便派生的 Controller 可以为同一消息 ID 提供 MessageHandler 类,从而允许它们访问先前分配的 MessageHandler 类。这类似于 MFC 方法式消息处理程序访问基类方法处理程序的方式。当分配给 Controller 时,Controller 会使用控制器本身作为 Controller 以及(如果存在)最后一个先前分配的 MessageHandler 调用 MessageHandler::InitializeCmdMsgHandlerNotifyMsgHandlerWndMsgHandler 提供调用先前已分配处理程序的相应方法。

class AFX_EXT_CLASS MessageHandler
{
public:
  MessageHandler();

  virtual ~MessageHandler();

public:
  void Initialize(Controller* pCtrlr, MessageHandler* pPrevHandler);

Controller* GetController() const;

protected:
  MessageHandler* GetPrevHandler() const;

private:
  struct MessageHandlerImpl* const m_pImpl;
};
项目位置:SbjCore/Mvc/MessageHandler.h
类 CmdMsgHandler

CmdMsgHandler 是一个用于处理 WM_COMMAND 消息的 抽象 类。CmdTargetController 调用 CmdMsgHandlerpublic Handle 方法来处理 CmdMsg 变体以及 CmdUI 变体的消息。private OnHandleCmd 方法是纯虚函数,必须在派生类中实现。OnHandleCmdUI 方法有一个默认实现,为传入的 CCmdUICCmdUI::Enable 方法返回 true。为了支持派生自其基类 MessageHandler 类的处理程序链接功能,派生的 OnHandleCmdOnHandleCmdUI 方法应分别调用 public 方法 HandleCmdPrevHandleCmdUIPrev

class AFX_EXT_CLASS CmdMsgHandler : public MessageHandler
{
public:
  virtual ~CmdMsgHandler();

  CmdTargetController* GetController() const;
public:
  bool HandleCmd(EventID nID);
  bool HandleCmdUI(CCmdUI* pCmdUI);
  bool HandleCmdPrev(EventID nID);
  bool HandleCmdUIPrev(CCmdUI* pCmdUI);
private:
  virtual bool OnHandleCmd(EventID nID) = 0;
  virtual bool OnHandleCmdUI(CCmdUI* pCmdUI);
};
项目位置:SbjCore/Mvc/CmdMsgHandler.h
类 NotifyMsgHandler

NotifyMsgHandler 是一个用于处理 WM_NOTIFY 消息的 抽象 类。private OnHandleNotify 方法是纯虚函数,必须在派生类中实现。为了支持派生自其基类 MessageHandler 类的处理程序链接功能,派生的 OnHandleNotify 方法应分别调用 public 方法 HandleNotifyPrev

class AFX_EXT_CLASS NotifyMsgHandler : public MessageHandler
{
public:
  virtual ~NotifyMsgHandler();

  CmdTargetController* GetController() const;
public:
  bool HandleNotify(NMHDR* pNMHDR, LRESULT* pResult);
  bool HandleNotifyPrev(NMHDR* pNMHDR, LRESULT* pResult);
private:
  virtual bool OnHandleNotify(NMHDR* pNMHDR, LRESULT* pResult) = 0;
};
项目位置:SbjCore/Mvc/NotifyHandler.h
类WndMsgHandler

WndMsgHandler 是一个用于处理所有其余 Windows 消息的 抽象 类。私有的 OnHandleWndMsg 方法是纯虚函数,必须在派生类中实现。为了支持派生自其基类 MessageHandler 类的处理程序链接功能,派生的 OnHandleWndMsg 方法应分别调用 public 方法 HandleWndMsgPrev。为此提供了调用 MFC 默认消息处理的机制。OnCallDefaultFirst 方法默认返回 false;但是,派生类可以在适当的情况下覆盖此方法以返回 true

class AFX_EXT_CLASS WndMsgHandler : public MessageHandler
{
public:
  virtual ~WndMsgHandler();

  WndController* GetController() const;
public:
  LRESULT HandleWndMsg(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
  bool HandleWndMsgPrev(WPARAM wParam, LPARAM lParam, LRESULT* pResult);

  bool CallDefaultFirst();
private:
  virtual LRESULT OnHandleWndMsg(WPARAM wParam,
                  LPARAM lParam, LRESULT* pResult) = 0;
  virtual bool OnCallDefaultFirst();
};
项目位置:SbjCore/Mvc/WndMsgHandler.h

控制器类

在 MVC 框架中,Controller 的角色之一是作为分配给它的 ControlledCmdTargetTControlledWndT 的消息管理器,它通过维护一个 MessageHandler 对象映射来做到这一点。当 ControlledCmdTargetTControlledWndT 类将消息传递给其 Controller 类时,Controller 使用消息 ID 在其映射中查找处理程序,如果找到,则给处理程序一个响应的机会。如果没有找到处理程序,或者处理程序希望消息继续被处理,则将 Controlled 类的基类给予处理机会。在大多数情况下,这将是一个具有标准 MFC 消息映射实现的 CmdTargetCWnd 派生类。正是通过这种方式,该框架才能与现有的 MFC 实现无缝集成。

MessageHandler 类一样,存在一个基类 Controller,在这种情况下,只有两个基派生类:CmdTargetControllerWndController

  • 控制器 (Controller)
    • CmdTargetController
    • WndController
类 Controller

ControllerCmdTargetControllerWndController 类提供了基础。它负责维护其受控类的 MessageHandler 类的映射。它提供了添加和删除处理程序到其映射的方法,以及一个用于获取给定 ID 当前处理程序的访问器。这在内部用于链接相同 ID 的处理程序。

您会注意到一个 PrepareCtxMenu 方法。派生的 Controller 类通过此方法响应 WM_CONTEXTMENU 消息,询问应将哪些菜单项添加到当前上下文菜单的调用中。

class AFX_EXT_CLASS Controller
{
public:
  Controller();

  virtual ~Controller();

public:

  void Initialize();

public:
  void AddHandler(EventID nID, MessageHandler* p);
  void AddHandler(EventID nFirstID, EventID nLastID, MessageHandler* p);
  void RemoveHandler(EventID nID);
  void RemoveHandler(EventID nFirstID, EventID nLastID);

  MessageHandler* GetHandler(EventID nID) const;

  SbjCore::Utils::Menu::ItemRange PrepareCtxMenu(CMenu& ctxMenu) const;

private:
  virtual void OnInitialize();
  virtual SbjCore::Utils::Menu::ItemRange OnPrepareCtxMenu(CMenu& ctxMenu) const;

private:
  struct ControllerImpl* const m_pImpl;
};
项目位置:SbjCore/Mvc/Controller.h
类 CmdTargetController

CmdTargetController 类控制 ControlledCmdTargetT 类。它处理 CmdMsgHandlerNoftifyMsgHandler 类的映射。当 ControlledCmdTargetT 收到消息时,其 OnCmdMsg 方法会以与 MFC 相同的方式确定消息的类型。然后,它调用适当的 CmdTargetController::HandleCmdMsgCmdTargetController::HandleCmdUIMsgCmdTargetController::HandleNotifyMsg 方法。如果消息未被处理,它将调用 CmdController::RoutCmdMsg,让控制器有机会覆盖默认的 MFC 路由。如果消息仍未处理,或者需要进一步处理,则将 ControlledCmdTargetT 的基类给予处理。

在此类中,您将看到 GetUndoRedoMgr 方法。这的解释实际上超出了本文的范围;但是,派生自 CmdTargetControllerSbjCore::Mvc::DocControllerSbjCore::UndoRedo::Manager 分配给任务,该任务在 Shapes.exe 应用程序中完全实现。

class AFX_EXT_CLASS CmdTargetController : public Controller
{
public:
  CmdTargetController();

  virtual ~CmdTargetController();

public:

  void SetCmdTarget(CCmdTarget* p);

  CCmdTarget* GetCmdTarget() const;

public:

  bool RoutCmdMsg(EventID nID, int nCode, void* pExtra,
                  AFX_CMDHANDLERINFO* pHandlerInfo);

  UndoRedo::Manager* GetUndoRedoMgr() const;

public:

  bool HandleCmdMsg(EventID nID);
  bool HandleCmdUIMsg(EventID nID, CCmdUI* pCmdUI);
  bool HandleNotifyMsg(NMHDR* pNMHdr, LRESULT* pResult);

private:
  virtual bool OnRoutCmdMsg(EventID nID,
  int nCode,
  void* pExtra,
  AFX_CMDHANDLERINFO* pHandlerInfo);

 virtual UndoRedo::Manager* OnGetUndoRedoMgr() const;

private:
 struct CmdTargetControllerImpl* const m_pImpl;
};
项目位置:SbjCore/Mvc/CmdTargetController.h
类WndController

WndController 类控制 ControlledWndT 类。它处理 WmdMsgHandler 类的映射。当然,由于它派生自 CmdTargetController,它也可以处理 CmdMsgHandlerNotifyHandler 类。

正如您在 MFC 中处理 Windows 消息时所知,有时在处理程序中处理 Windows 消息之前调用基类处理程序是合适的。为了实现这一点,当 ControlledWndT 收到消息时,其 OnWndMsg 方法会调用 WndMsgController::CallDefaultFirst 方法,该方法会查询处理程序映射以查找条目,如果找到,则询问处理程序是否应在调用处理程序之前进行默认处理。当然,如果消息未被处理,或者需要进一步处理,则将 ControlledWndT 的基类给予处理。

class AFX_EXT_CLASS WndController :  public CmdTargetController
{
public:
  WndController();

  virtual ~WndController();

public:
  void SetWnd(CWnd* p);

  CWnd* GetWnd() const;

public:

  BOOL HandleWndMsg(EventID message, WPARAM wParam,
                    LPARAM lParam, LRESULT* pResult);

  bool CallDefaultFirst(EventID message);
private:
  struct WndControllerImpl* const m_pImpl;
};

模型和事件

MVC 框架不使用 CView::OnUpdate 方法,而是使用事件架构。每次模型更改时,都会触发一个事件,指示已发生更改的类型。为给定类型的更改注册事件处理程序,并在事件触发时进行响应。与 MFC 文档/视图架构不同,事件架构不限于 CView 及其派生类,任何对模型更改感兴趣的对象都可以使用它。

唯一识别事件

事件通过唯一的 ID 值来标识,这些 ID 值是使用 Joseph M. Newcomer 的 RegisterWindowMessage 技术 [^] 创建的,我一直都在使用。

DECLARE_EVENT_ID 宏
#define DECLARE_EVENT_ID(name, guid) DECLARE_USER_MESSAGE(name, guid)
项目位置:SbjCore/EventMgr/EventMgr.h
命名空间 EventMgr

Event 架构实际上不是 MVC 框架的一部分,因为它可以独立使用。相反,命名空间 SbjCore::EventMgr 包含构成 Event 架构的函数和类。

EventMgr 维护一个已注册 EventHandler 类的私有映射,该映射以唯一 ID 为键。

typedef UINT EventID;

EventHandler 类在构造期间向 private EventMgr 处理程序映射注册自己,并在析构时取消注册。当触发具有匹配 ID 键的 Event 类时,其 NotifyHandlers 方法将调用每个已注册的 EventHandler::Handle 方法,并传递指向自身的指针。下面列出了 Event 架构的各种组件。

类 Event

Event 类非常直接。触发时,它会通知其处理程序。

class AFX_EXT_CLASS Event
{
public:
  Event();
  Event(EventID nEventID, bool bAutoFire = true);

  virtual ~Event();

public:
  void Init(EventID nEventID, bool bAutoFire = true);

  void NotifyHandlers();

private:
  struct EventImpl* const m_pImpl;
};
项目位置:SbjCore/EventMgr/EventMgr.h
类 EventT

EventT 类通过 Event 提供了一个通用的派生类,处理程序可以通过它接收处理事件所需的任何特定数据。

template <class T>
class EventT : public Event
{
  T theData;
public:
  EventT(EventID nEventID, T data, bool bAutoFire = true) :
    Event(nEventID, false), // must auto fire after fully constructed
    theData(data)
  {
    if (bAutoFire) // now we can auto fire
    {
      NotifyHandlers();
    }
  }

  virtual ~EventT()
  {
  }

  T GetData() const
  {
    return theData;
  }

};
项目位置:SbjCore/EventMgr/EventT.h
Fire 函数

Fire 函数提供了一种方便的方式来触发事件,当处理程序唯一需要的信息是事件已发生的事实时。

AFX_EXT_API void Fire(EventID nEventID);
项目位置:SbjCore/EventMgr/EventMgr.h
类 EventHandler

EventHandler 是一个 抽象 基类。派生类必须实现 private 虚拟方法 OnHandle,该方法从 public 方法 Handle 调用。EventHandler 类在构造时将自己注册到 EventMgr 处理程序映射,并在析构时取消注册。

class AFX_EXT_CLASS EventHandler
{
public:
  EventHandler(EventID nEventID);

  virtual ~EventHandler();

public:
  void Handle(Event* pEvent);

private:
  virtual void OnHandle(Event* pEvent) = 0;

private:
  struct EventHandlerImpl* const m_pImpl;
};
项目位置:SbjCore/EventMgr/EventMgr.h
AbortException

AbortException 提供了一种让处理程序短路触发机制的方式。当处理程序抛出此异常时,Event::NotifyHandlers 方法将停止将事件触发给后续处理程序。

typedef CUserException AbortException;
项目位置:SbjCore/EventMgr/EventMgr.h

结论

在前面的章节中,我介绍了提供 MVC 框架基础功能的类,并展示了它们如何与 MFC 文档/视图架构集成。从这些类出发,该框架通过提供 ControlledCmdTargetTControlledCWndT 派生类以及 MFC 应用程序主要组件的关联 Controller 类来扩展。在此基础上,XmlMvc.dll 提供了支持 XML 模型实现的功能扩展。如果有兴趣,未来的文章将探讨该框架在 CDockablePane 类型及其包含的控件中的具体应用,Shapes.exe 中使用的示例 DesignView 和 XML 模型,以及这些组件的可重用方面。所有这些都包含在本文提供的源代码中,所以,请运行 Shapes.exe 并探索应用程序和 DLL 中的代码。我认为您会惊讶于应用程序级别的代码实际上很少。

待办事项

  • 支持所有常用控件
  • 支持 MFC CView 派生类
  • CDockablePane 实现 DocTemplate 派生类
  • 基于 ModelItemHandle 的完全泛化

历史

  • 2008 年 10 月 20 日 - 提交原始文章
  • 2008 年 11 月 24 日 - 添加了后续文章的链接
  • 2009 年 3 月 19 日 - 添加了第二篇后续文章的链接
© . All rights reserved.