Objective Grid : PART II Programmer’s Guide : Chapter 12 Advanced Design Overview : Objective Grid Integration With Document/View Architecture
Objective Grid Integration With Document/View Architecture
For those readers who are familiar with design patterns, the MFC document/view concept is very similar to the Observer pattern: A view is associated with a document, and the document maintains a list of pointers to all associated views. When a view changes data in the document, it can trigger a notification to all associated views that the document data has changed. In Objective Grid, we extended the Observer pattern in such a way that we systematically separated methods, which update the display from methods, which changes data.
The document-view architecture in MFC allows the programmer to separate data from the views. An MFC application can offer the user many ways to view the document (the data), and some views may present only a part of the data or results computed from the raw data. For instance, a statistical application could present the data as a grid of numbers, a chart, or a summary showing computed values such as the mean and standard deviation.
A common situation in complex applications is the need to propagate information to all views. For example, suppose four view windows show different views of the same document and the user changes data in one view. Now it's necessary to notify all views about the change. The views should be able to update themselves most efficiently. Note also that views can draw to devices other than the video display — e.g., on plotters or printers.
In MFC, a document object keeps a list of its views, provides member functions for adding and removing views, and supplies the UpdateAllViews() member function for letting multiple views know when the document's data has changed. Each view has an OnUpdate() member function which is called from UpdateAllViews() method for each view.
Let us explain how drawing works in a view.
With the exception of mouse drawing, nearly all drawing in your application occurs in the view's OnDraw() member function, which you must override in your view class. Your OnDraw() override:
1. Gets data by calling the document member functions you provide.
2. Displays the data by calling member functions of a device-context object that the framework passes to OnDraw().
When a document's data changes in some way, the view must be redrawn to reflect the changes. Typically, this happens when the user makes a change through a view on the document. In this case, the view calls the document's UpdateAllViews() member function to notify all views on the same document to update themselves. UpdateAllViews() calls each view's OnUpdate() member function. The default implementation of OnUpdate() invalidates the view's entire client area. You can override it to invalidate only those regions of the client area that map to the modified portions of the document.
The UpdateAllViews() member function of class CDocument and the OnUpdate() member function of class CView let you pass information describing what parts of the document were modified. This hint mechanism lets you limit the area that the view must redraw. OnUpdate() takes two hint arguments. The first, lHint, of type LPARAM, lets you pass any data you like, while the second, pHint, of type CObject*, lets you pass a pointer to any object derived from CObject.
When a view becomes invalid, Windows sends it a WM_PAINT message. The view's OnPaint() handler function responds to the message by creating a device-context object of class CPaintDC and calls your view's OnDraw() member function. You do not normally have to write an overriding OnPaint() handler function.
Your code for drawing in the view first retrieves a pointer to the document and then makes drawing calls through the device context. The following simple OnDraw() example illustrates the process:
 
void CMyView::OnDraw( CDC* pDC )
{
CMyDoc* pDoc = GetDocument();
CString s = pDoc->GetData(); // Returns a CString
CRect rect;
 
GetClientRect( &rect );
pDC->SetTextAlign( TA_BASELINE | TA_CENTER );
pDC->TextOut( rect.right / 2, rect.bottom / 2,
s, s.GetLength() );
}
In this example, you would define the GetData() function as a member of your derived document class.
The example prints whatever string it gets from the document, centered in the view. If the OnDraw() call is for screen drawing, the CDC object passed in pDC is a CPaintDC whose constructor has already called BeginPaint(). Calls to drawing functions are made through the device-context pointer.
This structure is illustrated in Figure 120, “Document - View Relationship.”
Figure 120 – Document - View Relationship
The CDocument class provides operations for adding and removing views from the document and functions to iterate through the views. UpdateAllViews() will loop though all views stored in m_viewList and call the Update() method of the CView class.
Here are some code snippets for the class declaration of CDocument and CView:
 
class CDocument : public CCmdTarget
{
// Operations
void AddView(CView* pView);
void RemoveView(CView* pView);
virtual POSITION GetFirstViewPosition() const;
virtual CView* GetNextView(POSITION& rPosition) const;
 
// Update Views (simple update - DAG only)
void UpdateAllViews(CView* pSender, LPARAM lHint = 0L,
CObject* pHint = NULL);
 
// Attributes
CPtrList m_viewList; // list of views
 
...
};
 
class CView : public CWnd
{
...
// General drawing/updating
virtual void OnUpdate(CView* pSender, LPARAM lHint,
CObject* pHint);
virtual void OnDraw(CDC* pDC) = 0;
};
 
Some points worth mentioning:
Who triggers the update? The document and its associated views rely on the notification mechanism to stay consistent. The MFC approach makes views responsible for calling UpdateAllViews() at the right time.
Dangling references to deleted subjects. Deleting a document should not produce dangling references in its views. The MFC approach ensures that documents are instantiated and deleted through the MFC class framework. The MFC framework will close and destroy all views before a document is destroyed.
The push and pull model. The MFC approach uses the push model: The document sends detailed information about the change (a hint) to all views, whether they want it or not. This approach works fine when a view triggers the update and other views understand the hint. If the document itself has to trigger an update, this method has the disadvantage that the document has to know implementation details of the attached views.
Push Model Details
In Objective Grid, we extended the push model concept. In the grid view class, We systematically separated user interactions and methods that update the display from methods which change data. Each method that updates the display is associated with an ID. This ID can be used as a hint to be sent among views.
For example, if the user does a specific interaction, such as typing text in a cell and moving the current cell, the view will call the SetStyleRange() operation in the grid to store the value of the current cell. All associated views have to update this cell. The grid-component uses the following scheme to keep all views up to date. As mentioned, a user interaction results in calling a command such as SetStyleRange(). For each command, the grid-component contains two further methods. One method (which gets called once) stores and actualizes the data. The other method (which gets called for each view) updates the display. Here we explain the scheme by example:
The command method SetStyleRange() gets called after the user has changed the cell:
 
BOOL CGXEditControl::Store()
{
// Calls SetStyleRange() and resets the modify flag
CString sValue;
if (GetModify() && GetValue(sValue))
{
return Grid()->SetStyleRange(
CGXRange(m_nRow, m_nCol),
sValue);
}
 
return TRUE;
}
SetStyleRange() calls the store method StoreStyleRowCol() for each cell in the range. StoreStyleRowCol() actualizes the data.
 
BOOL CGXGridCore::SetStyleRange(
const CGXRange& range,
const CGXStyle* pStyle)
{
for each nRow, nCol in range
// store style information for the specific cell
StoreStyleRowCol(nRow, nCol, style);
 
// update the view (bCreateHint = TRUE)
UpdateStyleRange(range, pStyle, TRUE);
}
SetStyleRange() calls the update method UpdateStyleRange(), telling the method to create a hint and send it to the document. UpdateStyleRange() updates the window and creates a hint. Each update method in the grid is associated with an integer id. This integer is is used as hint and will be sent together with some additional information to all views. The hint is sent to the document by calling CDocument::UpdateAllViews().
 
void CGXGridCore::UpdateStyleRange(const CGXRange& range,
const CGXStyle* pStyle, BOOL bCreateHint)
{
// redraw all cells in range
Redraw(range);
 
// Create Hint
if (bCreateHint)
{
CGXGridHint hint(gxHintUpdateStyleRange, m_nViewID);
hint.range = rgBoundary;
hint.pStyle = pStyle;
 
UpdateAllViews(m_pGridWnd, 0, &hint);
}
}
CDocument::UpdateAllViews() sends the hint to all associated views by calling CView::OnUpdate(). Each view analyzes the hint ID and calls the corresponding update-method telling the method not to create a hint. The update method decides depending on the specific context of the view how to update the view. The integer-id will be used to call the appropriate update method in the view. Note that the bCreateHint parameter will be passed as FALSE to avoid an infinite recursion.
 
void CGXGridCore::OnUpdate(CView* /*pSender*/, LPARAM /*lHint*/, CObject* pHint)
{
CGXGridHint &info = *((CGXGridHint*) pHint);
 
switch (info.m_id)
{
case gxHintUpdateStyleRange:
UpdateStyleRange(
info.range,
info.pStyle,
FALSE
);
break;
 
...
};
}
Figure 121 – Update mechanism in the grid component