Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

The SBJ MVC Framework - The Design View, Responding to Model Change

4.87/5 (22 votes)
19 Mar 2009CPOL13 min read 85.3K   2.4K  
A Model-View-Controller Framework that integrates with the MFC Doc/View architecture.

SbjDevSrc202

Table of Contents

Introduction

This is the third article I've posted describing the MVC Framework. The first, An Introduction to a Model-View-Controller Implementation for MFC, introduced the underlying classes that integrate the Framework with the MFC Doc/View architecture. The second, The SBJ MVC Framework - The Model, from Abstraction to Realization, went into detail describing the Model and how it ties into the MFC CDocument class to provide an abstract interface to the Model data source. You'll need to be familiar with the concepts presented in the first two articles before reading this one.

In each of these articles, and included with this one, is an AppWizard created MFC application, Shapes.exe, illustrating the Framework integration. For its Model, Shapes reads an XML file that contains elements that define a series of graphical Rectangle and Ellipse objects. As you can see from the graphics at the beginning of the article, the Shapes application presents three different Views of the Model.

  • Design View - the main CView of the application which displays the shapes in their graphical form
  • Explorer View - a CTreeCtrl contained in an CDockingPane listing the shapes by name
  • Properties View - a CMFCPropertyGridCtrl contained in an CDockingPane listing the current shape's properties

I originally planned to discuss all three views in this article; however, when the article grew to over 50 printed pages and even I couldn't bear to read through it, I decided to split it into at least two, if not three separate articles.

In this article, I plan to go into detail describing the first of the views, the Design View, and how it is driven by Model change and how the Framework generalizes most of its functionality so that it can be leveraged with a minimum of additional coding at the application level.

The Source Code

The source code provided with this article is contained in a VS2008 solution, SbjDev, with three projects. The three projects are the same as were presented in the second article, with only a few minor bug fixes.

  • 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.

Note: Please see the appendix at the end of the article outlining the General Notes on the Source for information pertaining to coding style.

The Model Revisited

There is one more piece of the Model architecture that I waited until now to present because the Design and Explorer Views provide examples of its use. In the namespace SbjCore::Mvc::Model, you'll find two classes, Model::Action and Model::ActionHandler.

The Model::Action class provides a generic recursive traversal of the Model. During the traversal, if a ModelItem has an associated Model::ActionHandler class mapped to its Type, an instance of the handler will be created and called to apply whatever action it implements.

Because of the recursive nature of the traversal, the handler implements two methods which handle the ModelItem. The first, Model::ActionHandler::BeginHandling, is called when the ModelItem is first encountered. The second, Model::ActionHandler::EndHandling, is called after the ModelItem's children are processed. In this way, the Model::ActionHandler can maintain state while processing the child ModelItems. Although the Design View discussed in this article doesn't need to take advantage of this feature, the value will become clear in the next article when you see how the Explorer View handles insertions into its CTreeCtrl.

Class Model::Action
C++
// Project Location: SbjCore/Mvc/Model/ModelAction.h

class AFX_EXT_API Action
{
public:
  typedef enum
  {
    kStop     = (-1),
    kRejected = ( 0),
    kContinue = ( 1)
  } Results;

  typedef std::map<CString, CRuntimeClass*> HandlerMap;

public:
  Action(const Controller* pCtrlr, HandlerMap* pMap = NULL, 
         CRuntimeClass* pDefaultHandler = NULL);
  virtual ~Action();

  void SetDefaultHandler(CRuntimeClass* pDefault);

  int Apply(HANDLE hItem);
  
  const Controller* GetModelController();

private:
  struct ActionImpl* const m_pImpl;
};

The typedef enum Results declares values which are returns from the Model::ActionHandler handling methods. The intent of kStop and kContinue should be self-evident. kReject can be returned to short circuit the traversal of a branch of the Model.

The Model::Action class is constructed for a given Model::Controller. Two optional parameters may be provided as well; the first, a HandleMap, and the second, a default CRuntimeClass pointer to be used for any ModelItemTypes that are not specifically mapped.

As stated earlier, the Model::Action class creates Model::ActionHandler objects as necessary during the traversal. The HandlerMap is the map of ModelItemTypes to the CRuntimeClass object of Model::ActionHandler derivatives. If a HandlerMap is provided without a default, ModelItemTypes without a mapped handler will be assigned a base Model::ActionHandler object which does nothing but return kContinue. If no HandleMap is provided, obviously a default must be provided, since the base Model::ActionHandler object does nothing. The SetDefaultHandler method can be used to switch defaults at any time during the traversal.

The Apply method is the actual traversal, and may be passed any ModelItem HANDLE as a starting point.

Class Model::ActionHandler
C++
// Project Location: SbjCore/Mvc/Model/ModelActionHandler.h

class ActionHandler : public CCmdTarget
{
  DECLARE_DYNCREATE(ActionHandler)
public:
  virtual ~ActionHandler();

  void Initialize(Action* pAction, HANDLE hItem);

  Action* GetAction() const;

  HANDLE GetItem() const;

  int BeginHandling();
  int EndHandling();

protected:
  ActionHandler();

private:
  virtual int OnBeginHandling();
  virtual int OnEndHandling();

private:
  struct ActionHandlerImpl* const m_pImpl;
};

The Model::ActionHandler should be fairly self-explanatory. The Model::Action::Apply method calls the handler's Initialize method with itself as the Action parameter and the current ModelItem HANDLE. By providing access to the Action, the ActionHandler can retrieve any state information maintained by the Action while processing the ModelItem HANDLE. Derivatives override the two virtual methods, OnBeginHandling and OnEndHandling, to implement the desired action.

The Design View

First of all, there is no actual DesignView class in the Framework; rather, there is a DesignView namespace. The application specific CView or derivative gains its functionality through the DesignView::Controller class and the associated components. The Shapes application implementation of the Design View is its ShapesView class which is derived from ControlledView.

C++
// Project Location: SbjCore/Mvc/Views/ControlledViews.h
typedef ControlledWndT<CView> ControlledView;

Before showing you the ShapesView class and how it utilizes the Framework, I'll explain the workings of the DesignView components.

Class DesignView::Item

Through the components in the DesignView namespace, the view draws a graphical representation of application specific ModelItems. The graphical representation for a ModelItem is defined in the Framework by the DesignView::Item class. An application will define an Item derivative for each ModelItemType that is to be displayed. The only thing the application specific Item class is required to provide is an implementation for its OnDraw method (the default does nothing).

C++
// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewItem.h

class AFX_EXT_CLASS Item : public CObject
{
  DECLARE_DYNCREATE(Item)
public:
  virtual ~Item();

  void SetController(Controller* pCtrlr);
  Controller* GetController() const;
  
  bool IsTrackable() const;

  RectTracker* GetRectTracker() const;

  bool IsTrackedItem(const RectTracker* p);

  void SetModelItemHandle(const HANDLE hModelItem);
  HANDLE GetModelItemHandle() const;

  void Draw(CDC* pDC);

protected:
  Item();

private:
  virtual void OnDraw(CDC* pDC);
  virtual bool OnIsTrackable() const;
  virtual SbjCore::Mvc::RectTracker* OnGetRectTracker() const;

private:
  struct ItemImpl* const m_pImpl;
};

Each DesignView::Item has an associated RectTracker object (derived from the MFC CRectTracker class of which I assume you are familiar) which defines the rectangle in the ControlledView Device Context where Item drawing should be restricted. The RectTracker object itself is only displayed when its associated Item is selected. This is illustrated in the graphics of the Shapes application at the beginning of this article where the shape named "Second Rectangle" is selected.

Like the CRectTracker, it interacts with the user to provide Item selection, movement, and sizing capabilities. Currently, the RectTracker is tied to its Item object's associated ModelItem HANDLE, and it updates the Model directly with the user's changes. The ability to select, move, and resize the associated Item can be turned off in a derived Item by overriding the OnIsTrackable virtual method and returning false. If an application specific Item requires a different implementation of the RectTracker class, this can be provided by overriding the OnGetRectTracker virtual method. An example of this might be if an application specific Item was allowed to move but not resize.

Note: It occurs to me while writing this article that it might be better to decouple the RectTracker class from the MVC Framework altogether and have it communicate the user's changes through Event firings. You may see this change in a future revision of the Framework.

Class DesignView::Action and Class DesignView::DrawAction

In addition to the DesignView::Controller responsibility of maintaining the DesignView::Item state in response to Model changes, its main functions are to create the Item objects in response to the EVID_FILE_OPEN event, and to ask them to draw themselves in response to the WM_PAINT message sent to its ControlledView. Although the Controller supplies the routine that creates the Item objects, and Item objects are responsible for drawing themselves, it is two Controller created Model::Action derivatives that provide the transversal of the Model, and their associated ModelItem specific Model:ActionHandler classes that apply the actions to each Item.

The first class, DesignView::Action, is used as the action for the insertion process, and only adds an accessor to the assigned DesignView::Controller to the base Model::Action class. DesignView::Action also provides the base for DesignView::DrawAction, which additionally provides an accessor to the ControlledView Device Context.

C++
// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewAction.h

class AFX_EXT_CLASS Action : public SbjCore::Mvc::Model::Action
{
public:

  Action(const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    Controller* pCtrlr, 
    SbjCore::Mvc::Model::Action::HandlerMap* pMap = NULL, 
    CRuntimeClass* pDefaultHandler = NULL);

  virtual ~Action();
  
public:
  Controller* GetController() const;
  
private:
  struct ActionImpl* const m_pImpl;
};


// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewDrawAction.h
class DrawAction : public Action
{
public:

  DrawAction(SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    Controller* pCtrlr, CDC* pDC, 
    SbjCore::Mvc::Model::Action::HandlerMap* pMap = NULL, 
    CRuntimeClass* pDefaultHandler = NULL);

  virtual ~DrawAction();
  
public:
  CDC* GetDC() const;
  
private:
  struct DrawActionImpl* const m_pImpl;
};
Class DesignView::InsertActionHandler and Class DesignView::DrawActionHandler

All the two Model::ActionHandler derived classes do is override the base class OnBeginHandling method where the actual application of the action occurs.

C++
// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewInsertActionHandler.cpp

int InsertActionHandler::OnBeginHandling()
{
  Action* pAction = dynamic_cast<Action*>(GetAction());
  Controller* pCtrlr = pAction->GetController();
  const SbjCore::Mvc::Model::Controller* pModelCtrlr = pAction->GetModelController();
  HANDLE hItem = GetItem();
  (void)pCtrlr->CreateItem(pModelCtrlr, hItem);
  return Model::Action::kContinue;
}

As you can see in the InsertActionHandler::OnBeginHandling method, the handler retrieves the DesignView::Controller from the Action along with the Model::Controller and the HANDLE to the current ModelItem. Using these, it calls the DesignView::Controller method CreateItem.

C++
// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewDrawActionHandler.cpp

int DrawActionHandler::OnBeginHandling()
{
  DrawAction* pAction = dynamic_cast<DrawAction*>(GetAction());
  HANDLE hItem = GetItem();
  Controller* pCtrlr = pAction->GetController();
  CDC* pDC = pAction->GetDC();
  Item* pItem = pCtrlr->GetItem(hItem);
  if (pItem != NULL)
  {
    pItem->Draw(pDC);
  }
  return Model::Action::kContinue;
}

In the case of the DesignView::DrawActionHandler, the handler retrieves the DesignView::Controller from the Action along with the HANDLE to the current ModelItem and the Device Context for the ControlledView. Using these, it calls the DesignView::Controller method GetItem and then calls DesignView::Item::Draw.

Class DesignView::Controller
C++
// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewController.h

class AFX_EXT_CLASS Controller : public WndController
{
  DECLARE_DYNCREATE(Controller)

public:
  Controller();
  virtual ~Controller();
  
public:
  void MapItemRTCToModelTypeName(LPCTSTR lpszName, CRuntimeClass* pRTC);

  Item* CreateItem(const SbjCore::Mvc::Model::Controller* pModelCtrlr, HANDLE hModelItem);

  void LoadDesign(const SbjCore::Mvc::Model::Controller* pModelCtrlr, HANDLE hItem);

  void InsertDesign(const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hItem, HANDLE hAfter = NULL);

  virtual void DeleteItem(Item* pItem);

  void DeleteAllItems();

  Item* GetItem(const HANDLE hItem) const;

  Item* GetItemFromPoint(const CPoint& pt) const;

  SbjCore::Mvc::MultiRectTracker* GetTracker() const;

public:
  void Draw(CDC* pDC);

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

private:
  virtual void OnDraw(CDC* pDC);

private:
  struct ControllerImpl* const m_pImpl;
};

The DesignView::Controller maintains a map to associate the DesignView::Item class CRuntimeClass pointers to ModelItemType names that the application maps through a call to MapItemToModelTypeName. As explained in the previous section, the Controller uses this map to create the appropriate Item object for the ModelItems while traversing the Model during the insertion action. These Item objects are then kept in a map of ModelItem HANDLE to DesignView::Item pointer. The Controller uses this map to access the associated Item objects while traversing the Model during the draw action.

InsertDesign is the actual method that creates the Model::Action responsible for the insertion. LoadDesign adds the extra step of deleting all the previous DesignView::Item objects before calling InsertDesign. This way, the same InsertDesign method can be used whether inserting the initial Model or when adding new ModelItems to an existing one.

The Draw method calls the private virtual OnDraw method which, you guessed it, creates the Model::Action responsible for drawing the Model.

Other than the methods you would expect for the maintenance of the DesignView::Item objects, there is also an instance of MultiRectTracker. If you've ever created a Dialog with the AppStudio, I'm sure you're familiar with how multiselection works. The MultiRectTracker handles the coordination of selected RectTracker objects, and provides notification of changes in selection through its EVID_SELCOUNT_CHANGED event. Through the handling of this event, the Controller can update the Model's set of currently selected ModelItems.

Struct ControllerImpl

As with most classes in the Framework, DesignView::Controller delegates most of its implementation to its private member struct ControllerImpl* const m_pImpl.

C++
// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewController.cpp

struct ControllerImpl
{
  SbjCore::Mvc::MultiRectTracker theTracker;

  typedef std::map<HANDLE, Item*> HandleToItem;
  typedef HandleToItem::iterator HandleToItemIter;
  HandleToItem theItems;
  
  typedef std::map<CString, CRuntimeClass*> ItemRTCMap;
  typedef ItemRTCMap::iterator ItemRTCMapIter;
  ItemRTCMap theItemRTCMap;

  localNS::FileOpenEventHandler theFileOpenEventHandler;
  localNS::ItemInsertEventHandler theItemInsertEventHandler;
  localNS::SelCountChangedEventHandler theSelCountChangedEventHandler;
  localNS::ItemChangeEventHandler theItemChangedHandler;

  localNS::DocModifiedEventHandler theDocModifiedEventHandler;
  localNS::ItemRemoveEventHandler theItemRemoveEventHandler;

  localNS::OnCreateHandler theOnCreateHandler;
  localNS::OnEraseBkgndHandler theOnEraseBkgndHandler;
  localNS::LButtonDownMsgHandler theLButtonDownHandler;
  localNS::SetCursorMsgHandler theSetCursorHandler;
  localNS::SetFocusHandler theSetFocusHandler;
  localNS::KillFocusHandler theKillFocusHandler;

  ControllerImpl()
  {
  }

  virtual ~ControllerImpl()
  {
    DeleteAllItems();
  }

  void DeleteAllItems()
  {
    for (HandleToItemIter iter = theItems.begin(); iter != theItems.end(); iter++)
    {
      Item* pItem = iter->second;
      if (pItem != NULL)
      {
        if (pItem->IsTrackable())
        {
          theTracker.Remove(pItem->GetRectTracker());
        }
        delete pItem;
      }
    }
    theItems.clear();
    theTracker.RemoveAll();
  }

  void DeleteItem(Item* pItem)
  {
    if (pItem != NULL)
    {
      if (pItem->IsTrackable())
      {
        theTracker.Remove(pItem->GetRectTracker());
        HANDLE hItem = pItem->GetModelItemHandle();
        theItems.erase(hItem);
      }
      delete pItem;
    }
  }

  Item* GetItemFromPoint(const CPoint& pt) const
  {
    pt;
    return NULL;
  }
  
  Item* CreateItem(Controller* pCtrlr, 
    const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hModelItem )
  {
    Item* pItem = NULL;
    CString sItemType = pModelCtrlr->GetItemTypeName(hModelItem);
    if (!sItemType.IsEmpty())
    {
      CRuntimeClass* pRTC = theItemRTCMap[sItemType];
      if (pRTC != NULL)
      {
        pItem = dynamic_cast<Item*>(pRTC->CreateObject());
        pItem->SetController(pCtrlr);
        pItem->SetModelItemHandle(hModelItem);
        theItems[hModelItem] = pItem;
        if (pItem->IsTrackable())
        {
          theTracker.Add(pItem->GetRectTracker());
        }
      }
    }
    return pItem;
  }
  

  void LoadDesign(Controller* pCtrlr, 
    const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hItem )
  {
    DeleteAllItems();
    InsertDesign(pCtrlr, pModelCtrlr, hItem);
  }

  void InsertDesign(Controller* pCtrlr, 
    const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hItem, HANDLE hAfter /*= NULL*/)
  {
    hAfter;
    Action theAction(pModelCtrlr, pCtrlr, NULL, RUNTIME_CLASS(InsertActionHandler));
    theAction.Apply(hItem);
  }


  void OnDraw(Controller* pCtrlr, CDC* pDC)
  {
    SbjCore::Utils::GDI::CMemDC dc(pDC);
    DrawAction theAction(SbjCore::Mvc::Model::GetCurController(), pCtrlr, &dc, NULL, 
      RUNTIME_CLASS(DrawActionHandler));
    if (theItems.size() > 0)
    {
      theAction.Apply(theItems.begin()->first);
      theTracker.Draw(&dc);
    }
  }

  SbjCore::Utils::Menu::ItemRange OnPrepareCtxMenu( CMenu& ctxMenu )
  {
    SbjCore::Utils::Menu::ItemRange menuItems;
    SbjCore::Mvc::MultiRectTracker::t_RectTrackers rts;
    int nCount = theTracker.GetSelectedRectTrackers(rts);
    if (nCount > 0)
    {

      menuItems.nFirst = (int)ctxMenu.GetMenuItemCount()-1;
      menuItems.nLast = menuItems.nFirst; 

      if (SbjCore::Utils::Menu::InsertSeparator(ctxMenu, menuItems.nLast))
      {
        menuItems.nLast++;
      }

      (void)ctxMenu.InsertMenu(menuItems.nLast, MF_BYPOSITION, ID_SBJCORE_CTX_DELETE, 
        _T("Delete Selected"));
    }
    return menuItems;
  }

};

In addition to containing the MultiRectTracker, the map collections, and the Item maintenance methods, ControllerImpl also contains the EventHandler and MsgHandler classes necessary to communicate respectively with the Model and the User. Rather than go into detail and show the source for each of these, I'll give a brief explanation of what each does and leave it to you to examine the source in detail if you so desire.

Event Handlers

  • SbjCore::Mvc::Doc::Events
    • localNS::FileOpenEventHandler - calls DesignView::Controller::LoadDesign to load the design indicated by the root HANDLE passed by the FileOpen event
    • localNS::DocModifiedEventHandler - calls CView::Invalidate in response to the DocModified event
  • SbjCore::Mvc::Model::Events
    • localNS::ItemInsertEventHandler - inserts the ModelItem indicated by the HANDLE passed by the ItemInsert event
    • localNS::ItemChangeEventHandler - updates the ModelItem RectTracker indicated by the HANDLE passed by the ItemChange event
    • localNS::ItemRemoveEventHandler - removes the ModelItem indicated by the HANDLE passed by the ItemRemove event
  • SbjCore::Mvc::MTE (MultiRectTracker Events)
    • localNS::SelCountChangedEventHandler - calls SbjCore::Mvc::Model::Controller::SetSelectedItems with the ModelItem HANDLE list referenced by the MultiRectTracker object's currently selected RectTracker objects

Message Handlers

  • localNS::OnCreateHandler - calls the DesignView::Controller::Initialize which in turn initializes the event handlers
  • localNS::OnEraseBkgndHandler - disables default processing of WM_ERASEBKGND in support of the DesignView implementation of double-buffering
  • localNS::LButtonDownMsgHandler - calls the DesignView::Controller object's MultiRectTracker::Track method
  • localNS::SetCursorMsgHandler - sets the cursor for the DesignView::Controller object's MultiRectTracker
  • localNS::SetFocusHandler - calls CView::Invalidate
  • localNS::KillFocusHandler - calls the DesignView::Controller object's MultiRectTracker::DeselectAll method and then calls CView::Invalidate

Applying the DesignView to the Application

Again, as described in the previous article about the Model implementation, integrating the DesignView to the ShapesView class takes very few modifications.

Note: Modifications to the original are marked in bold.

ShapesView.h
C++
#pragma once

struct ShapesViewImpl;

class ShapesView : public SbjCore::Mvc::ControlledView
{
  typedef SbjCore::Mvc::ControlledView t_Base;
  
protected: // create from serialization only
  ShapesView();
  DECLARE_DYNCREATE(ShapesView)

// Attributes
public:
  ShapesDoc* GetDocument() const;

// Operations
public:

// Overrides
public:
  virtual void OnDraw(CDC* pDC);  // overridden to draw this view
  virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:
  virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
  virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
  virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);

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

protected:

// Generated message map functions
protected:
  afx_msg void OnFilePrintPreview();
  afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
  afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);
  DECLARE_MESSAGE_MAP()
private:
  struct ShapesViewImpl* const m_pImpl;
};

Only three changes have been made; ShapesView is now derived from ControlledView; the OnRButtonUp and the OnContextMenu methods have been removed, and the class has been outfitted with a Pimpl struct ShapesViewImpl* const m_pImpl member.

Next, I'll show the changes to ShapesView.cpp. Since even the AppWizard generated file is rather large, I'll limit the display to those areas that have been modified.

ShapesView.cpp
C++
namespace localNS
{
  // DesignViewItems //////////////////////////////////////
  
  // invisible root item (not drawn)
  class DrawingItem : public SbjCore::Mvc::DesignView::Item
  {
    DECLARE_DYNCREATE(DrawingItem)
  
    virtual bool OnIsTrackable() const
    {
      return false;
    }
  };
  IMPLEMENT_DYNCREATE(DrawingItem, SbjCore::Mvc::DesignView::Item)
  
  class ShapeItem : public SbjCore::Mvc::DesignView::Item
  {
  public:
    ShapeItem() :
      r(0,0,0,0),
      nBorderWidth(1),
      clrBorder(RGB(0,0,0)),
      clrFill((COLORREF)-1)
    {
    }
    virtual ~ShapeItem()
    {
    }
  protected:
    CRect r;
    int nBorderWidth;
    COLORREF clrBorder;
    COLORREF clrFill;
  private:
    DECLARE_DYNCREATE(ShapeItem)
    
    virtual void OnDraw(CDC* pDC)
    {
      pDC;
      HANDLE hItem = GetModelItemHandle();
      SbjCore::Mvc::Model::Controller* pModelCtrlr = SbjCore::Mvc::Model::GetCurController();
      r = SbjCore::Mvc::Model::Rect::GetItemValue(pModelCtrlr, hItem);
      nBorderWidth = pModelCtrlr->GetItemAttrValue(hItem, _T("borderWidth"));
      clrBorder = pModelCtrlr->GetItemAttrValue(hItem, _T("borderRGB"));
      clrFill = pModelCtrlr->GetItemAttrValue(hItem, _T("fillRGB"));

    }
    
  };
  IMPLEMENT_DYNCREATE(ShapeItem, SbjCore::Mvc::DesignView::Item)

  
  
  class RectangleItem : public ShapeItem
  {
    DECLARE_DYNCREATE(RectangleItem)
    virtual void OnDraw(CDC* pDC)
    {
      ShapeItem::OnDraw(pDC);
      
      CPen pen(PS_SOLID, nBorderWidth, clrBorder);
      SbjCore::Utils::GDI::Object<cpen> objPen(pDC, &pen);
      LOGBRUSH lb = {BS_HOLLOW, 0, 0};
      CBrush brHollow;
      brHollow.CreateBrushIndirect(&lb);
      SbjCore::Utils::GDI::Object<cbrush> objBrush(pDC, &brHollow);

      if (clrFill != (COLORREF)-1)
      {
        CBrush brush;
        brush.CreateSolidBrush(clrFill);
        pDC->FillRect(r, &brush);
      }
      pDC->Rectangle(r);
    }
  };
  IMPLEMENT_DYNCREATE(RectangleItem, ShapeItem)


  class EllipseItem : public ShapeItem
  {
    DECLARE_DYNCREATE(EllipseItem)
    virtual void OnDraw(CDC* pDC)
    {
      ShapeItem::OnDraw(pDC);
      CPen pen(PS_SOLID, nBorderWidth, clrBorder);
      SbjCore::Utils::GDI::Object<cpen> objPen(pDC, &pen);
      CBrush br;
      br.CreateSolidBrush(clrFill);
      SbjCore::Utils::GDI::Object<cbrush> objBrush(pDC, &br);

      pDC->Ellipse(r);
    }
  };
  IMPLEMENT_DYNCREATE(EllipseItem, ShapeItem)
}

struct ShapesViewImpl : public SbjCore::Mvc::DesignView::Controller
{
  ShapesViewImpl()
  {
   // must have a root for the design but it isn't drawn
    MapItemRTCToModelTypeName(_T("Drawing"), RUNTIME_CLASS(localNS::DrawingItem));
    MapItemRTCToModelTypeName(_T("Rectangle"), RUNTIME_CLASS(localNS::RectangleItem));
    MapItemRTCToModelTypeName(_T("Ellipse"), RUNTIME_CLASS(localNS::EllipseItem));
  }
  
  virtual ~ShapesViewImpl()
  {
  }
  
  virtual SbjCore::Utils::Menu::ItemRange OnPrepareCtxMenu( CMenu& ctxMenu )
  {
    SbjCore::Utils::Menu::ItemRange menuItems;

    menuItems.nFirst = (int)ctxMenu.GetMenuItemCount()-1;
    menuItems.nLast = menuItems.nFirst; 

    if (SbjCore::Utils::Menu::InsertSeparator(ctxMenu, menuItems.nLast))
    {
      menuItems.nLast++;
    }

    (void)ctxMenu.InsertMenu(0, MF_BYPOSITION, ID_CMDS_NEWELLIPSE, _T("New &Ellipse"));
    (void)ctxMenu.InsertMenu(0, MF_BYPOSITION, ID_CMDS_NEWRECTANGLE, _T("New &Rectangle"));
    

    menuItems = SbjCore::Mvc::DesignView::Controller::OnPrepareCtxMenu(ctxMenu);

    return menuItems;
  }
  
  
};

// ShapesView ///////////////////////////////////////////////////////

ShapesView::ShapesView() :
  m_pImpl(new ShapesViewImpl)
{
  SetController(m_pImpl);
}

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

void ShapesView::OnDraw(CDC* pDC)
{
  ShapesDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  if (!pDoc)
  {
    return;
  }
  m_pImpl->Draw(pDC);
}

In the local namespace, you see four DesignView::Item derivatives. First, a DrawingItem class which acts as the required root of the design, and does nothing but override OnIsTrackable to prevent it from being selectable. Since it has no OnDraw override, it is never displayed either. The other three consist of a base ShapeItem class and two specific shapes, RectangleItem and EllipseItem. As was stated in the discussion on DesignView::Item, the only thing necessary of these derivatives is to provide an implementation of OnDraw.

Next is the ShapesViewImpl implementation of the DesignView::Controller. It has two functions: first it instantiates the DesignView::Item derivatives and maps their CRuntimeClass pointers to the appropriate ModelItem names, and second, it provides the OnPrepareCtxMenu override to add commands for creating new shapes.

Other than creating its Pimpl, the ShapesView class itself does nothing except call the ShapesViewImpl controller's base class method Draw from its OnDraw method.

That's it, everything else is handled by the Framework!

Conclusion

This article presented details regarding the MVC Framework Design View implementation and how an application can leverage its functionality by providing classes that fulfill only Model specific requirements. The Shapes application is simple by design; however, I think you can see that the Design View could support extremely complex Models through the techniques illustrated here. Future articles will cover the Explorer and Properties View where you will see that the MVC Framework again provides the same level of functional support, limiting the application to supplying only those details relevant to its specific Model domain. And, as I've said in the last two articles, all the code is included in the attachments accompanying this article, so run the Shapes application, explore the code, and please offer any feedback you'd like to contribute.

TODO

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

Appendix

General Notes on the Source

All components of the MVC Framework belong to the namespace SbjCore::Mvc.

I have tried to follow the rule of either making virtual methods private, or in the case where derivatives must call base implementations, protected, that are only accessed through a call to a public method declared in the base class.

C++
//MyClass.h

class MyClass 
{
public:
  MyClass() {} 
  virtual ~MyClass() {} 

  void Init()
  {
    OnInit();
  }
  
  void DoIt()
  {
    OnDoIt();
  }

protected:  
  virtual void OnInit()
  {
    // init base class
  }
  
private:
  virtual void OnDoIt
  {
    // do it the base class way
  }
}

class MyDerivedClass : public MyClass
{
protected:  
  virtual void OnInit()
  {
    MyClass::OnInit();
    // init this class
  }
private:
  virtual void OnDoIt
  {
    // do it the derived class way
  }
}

class MyFinalClass : public MyDerivedClass
{
protected:  
  virtual void OnInit()
  {
    MyDerivedClass::OnInit();
    // init this class
  }
private:
  virtual void OnDoIt
  {
    // do it the final class way
  }
}

int main()
{
  MyFinalClass myFinalClass;
  myFinalClass.Init();
  
  myFinalClass.DoIt();
}

Most classes use the Pimpl idiom to hide implementation details.

C++
//MyClass.h

struct MyClassImpl;

class MyClass 
{
public:
  MyClass(); 
  virtual ~MyClass(); 
  
  void APublicMethod();
  
private:
  struct MyClassImpl* const m_pImpl;
}

//MyClass.cpp

struct MyClassImpl
{
  void APrivateImpl()
  {
    // do something
  }
}

MyClass::MyClass() :
  m_pImpl(new MyClassImpl)
{
}

MyClass::~MyClass()
{
  delete m_pImpl;
}

void MyClass::APublicMethod()
{
    m_pImpl->APrivateImpl();
}

In the case of the ControlledCmdTargetT and ControlledWndT classes, the Pimpl often doubles as the Controller of the class.

Most .cpp files contain a local namespace called localNS; however, localNS is actually a macro defined in Platform.h, and resolves to an empty string. It is used purely as a documentation tool somewhat like MFC's afxmsg macro for indicating methods that act as message handlers.

C++
//MyClass.cpp

namespace localNS
{
  void ALocalFunction()
  {
    // do something
  }
}

void MyClass::APublicMethod()
{
  localNS::ALocalFunction();
}

History

  • 2009 Mar 19 - Original article submitted.

License

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