Table of Contents
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 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.
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
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
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.
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
.
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).
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.
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;
};
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.
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
.
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
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
.
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 )
{
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
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
#pragma once
struct ShapesViewImpl;
class ShapesView : public SbjCore::Mvc::ControlledView
{
typedef SbjCore::Mvc::ControlledView t_Base;
protected: ShapesView();
DECLARE_DYNCREATE(ShapesView)
public:
ShapesDoc* GetDocument() const;
public:
public:
virtual void OnDraw(CDC* pDC); 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);
public:
virtual ~ShapesView();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
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
namespace localNS
{
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()
{
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() :
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!
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.
- 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
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.
class MyClass
{
public:
MyClass() {}
virtual ~MyClass() {}
void Init()
{
OnInit();
}
void DoIt()
{
OnDoIt();
}
protected:
virtual void OnInit()
{
}
private:
virtual void OnDoIt
{
}
}
class MyDerivedClass : public MyClass
{
protected:
virtual void OnInit()
{
MyClass::OnInit();
}
private:
virtual void OnDoIt
{
}
}
class MyFinalClass : public MyDerivedClass
{
protected:
virtual void OnInit()
{
MyDerivedClass::OnInit();
}
private:
virtual void OnDoIt
{
}
}
int main()
{
MyFinalClass myFinalClass;
myFinalClass.Init();
myFinalClass.DoIt();
}
Most classes use the Pimpl idiom to hide implementation details.
struct MyClassImpl;
class MyClass
{
public:
MyClass();
virtual ~MyClass();
void APublicMethod();
private:
struct MyClassImpl* const m_pImpl;
}
struct MyClassImpl
{
void APrivateImpl()
{
}
}
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.
namespace localNS
{
void ALocalFunction()
{
}
}
void MyClass::APublicMethod()
{
localNS::ALocalFunction();
}
- 2009 Mar 19 - Original article submitted.