Notifying the Document






4.36/5 (9 votes)
An article on delivering objects to the document in a document/view architecture, using the WM_NOTIFY message.
Introduction
This article shows how to deliver an object to the document in a document/view architecture using the WM_NOTIFY
message.
Background
When I write MFC applications, I employ the document-view architecture. In general, the document is a passive storage area which allows you to display information on request or update that information from the view. There are cases where a document abstracts a database or a portion of a database, and it may not be practical to update the view immediately; for instance, if the database is on a remote machine and the connection is slow, then trying to gather the information on the main thread and pass it to the view will lock up the user interface. Locking up the user interface does not make for a pleasant user experience. So, I make my requests on worker threads. This allows the user to continue to interact with the application, and it allows the document to post information back to the user about the status of the request.
The problem is that CDocument
is not a window: you cannot post messages from a thread directly to a CDocument
object. CDocument
is based on CCmdTarget
, which is the MFC class that is the basis of message routing. So, how do we get information from the worker thread to the CDocument
object?
About a year ago, I read an article in MSDN on command routing, and in that article was this benign flow chart which showed how WM_COMMAND
was being routed from the main frame to the active view and, finally, to the document. That same chart showed how WM_NOTIFY
took a similar route to the document! I can no longer find that article. In fact, I can't seem to find any trace of any documentation which says that WM_NOTIFY
gets passed to the document, save a single sentence in a Microsoft Systems Journal Q&A column from September 1998. Paul DiLascia wrote the following: "Whenever the frame gets a command (WM_COMMAND
or WM_NOTIFY
message), it routes it to the active view. The view, in turn, routes commands to its document."
About five years ago, Mehdi Mousavi wrote an article for CodeProject, called "Win32 vs. MFC - Part II". While the article does not explicitly deal with WM_NOTIFY
, there is a nice little graphic that shows the routing of WM_COMMAND
and WM_NOTIFY
to the CCmdTarget::OnCmdMsg
method.
Well, if you were expecting to add an ON_NOTIFY
message map entry to your CDocument
-based class, you would be disappointed. I know I was.
Nevertheless, WM_NOTIFY
does get to the document. Really!
The following article describes a strategy for notifying the document from a worker thread using WM_NOTIFY
, because we also want to pass along the relevant additional information from the worker thread. I will show you how the document gets the WM_NOTIFY
message, and how you can access it.
Using the code
I have created a CNotifyObject
class which maintains an extended form of the NMHDR
structure. Normally, I would also abstract an engine class which separates the document from active content but, for brevity, I have made the document directly responsible for the content.
You may already have run into the NMLISTVIEW
structure which extends the NMHDR
structure by adding several variables that hold values used by a CListView
or CListCtrl
window. I extended the NMHDR
structure with the NMHDROBJECT
structure:
typedef struct tagNMHDROBJECT { NMHDR nmHdr ; CNotifyObject * pObject ; } NMHDROBJECT ; typedef NMHDROBJECT * LPNMHDROBJECT ;
CNotifyObject
maintains an NMHDROBJECT
structure (called m_hdrObject
), and sets m_hdrObject.pObject = this
when the object is constructed. The address of m_hdrObject
can be pushed into the LPARAM
parameter during a call to PostMessage
to send the CNotifyObject
to the main thread, where it can be distributed to the views.
The following method runs from a thread inside of CNotifierAppDoc
. Note how we are getting the main frame window from the CWinApp
object and posting the WM_NOTIFY
message to it.
From NotifierAppDoc.cpp:
void CNotifierAppDoc::OnInformationThread( int index ) { TRACE( _T("CNotifierAppDoc::OnInformationThread()\n") ) ; HWND hMain = AfxGetApp()->m_pMainWnd->GetSafeHwnd() ; NMHDR hdr = { hMain, IDD_NotifyAppDoc, 0 } ; CString s ; s.Format( _T("Information Item %d"), index ) ; CInformationObject * pObject = new CInformationObject( index, s, hdr ) ; ::PostMessage( hMain, WM_NOTIFY, 0, (LPARAM)&(pObject->m_hdrObject) ) ; }
The WM_NOTIFY
message makes its way to the CWnd::OnNotify()
method; the method checks to see if it has an owner, and if it doesn't, it is handed off to the OnCmdMsg
method.
From wincore.cpp:
BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult) { ASSERT(pResult != NULL); NMHDR* pNMHDR = (NMHDR*)lParam; HWND hWndCtrl = pNMHDR->hwndFrom; // get the child ID from the window itself UINT_PTR nID = _AfxGetDlgCtrlID(hWndCtrl); int nCode = pNMHDR->code; ASSERT(hWndCtrl != NULL); ASSERT(::IsWindow(hWndCtrl)); if (_afxThreadState->m_hLockoutNotifyWindow == m_hWnd) return TRUE; // locked out - ignore control notification // reflect notification to child window control if (ReflectLastMsg(hWndCtrl, pResult)) return TRUE; // eaten by child AFX_NOTIFY notify; notify.pResult = pResult; notify.pNMHDR = pNMHDR; return OnCmdMsg((UINT)nID, MAKELONG(nCode, WM_NOTIFY), ¬ify, NULL); }
You see that last line? CWnd::OnNotify
combines WM_NOTIFY
with pNMHDR->nCode
into a single value, and passes it to the derived class' OnCmdMsg
method. CFrameWnd
passes it to CView
, and CView
passes it to CDocument
:
From viewcore.cpp:
BOOL CView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { // first pump through pane if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE; // then pump through document if (m_pDocument != NULL) { // special state for saving view before routing to document CPushRoutingView push(this); return m_pDocument->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); } return FALSE; }
And that is why you can't just set up an ON_NOTIFY
handler in the document. Instead, we override the document's OnCmdMsg()
method.
From NotifierAppDoc.cpp:
BOOL CNotifierAppDoc::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { if ( HIWORD(nCode) == WM_NOTIFY ) // verify that this is a WM_NOTIFY message { WORD wCode = LOWORD(nCode) ; AFX_NOTIFY * notify = reinterpret_cast<AFX_NOTIFY*>(pExtra) ; if ( notify->pNMHDR->idFrom == IDD_NotifyAppDoc ) // verify that this is our notification { LPNMHDROBJECT lpnmhdr = (LPNMHDROBJECT)(notify->pNMHDR) ; CNotifyObject * pObject = lpnmhdr->pObject ; UpdateAllViews( NULL, pObject->get_Message(), pObject ) ; delete pObject ; // we can delete the object because // we are done with it and we are notifying // subsequent windows that this message has been handled. *(notify->pResult) = 1 ; return TRUE ; // if this is ours we return TRUE // to let it know it has been handled } } return CDocument::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); }
Okay, you might say, that's a lot of work just to update a view. Why not just get the active view in the thread and post the notification directly to the view?
Good question. You could do that. But getting the notification object to the document allows you to value-add to the notification before sending it out. Second, if you have multiple views, as in the case of a splitter window project or an MDI project, you will want to update all of the views and not just the active view (which is why I use UpdateAllViews()
in the OnCmdMsg
method); otherwise, the active view will have the responsibility for updating the inactive views, and you don't really want to write that code for all of your views, do you? It is much better to let the document update them. Finally, I am creating temporary notification objects at will, and there has to be a point where an object that is no longer useful can be deleted (otherwise, watch out for memory leaks). Having the document control the access of the object through UpdateAllViews
ensures that each view gets its time with the object (and maintains a copy, if necessary) and the object can be deleted at the end.
Points of interest
I have set up a half second delay before executing the OnInformationThread
method, and a two second delay before executing the OnErrorThread
method. These can both be found in the CNotifierAppDoc
class, and you are free to change them and play with the values.
I am a sucker for copy constructors, so my CNotifyObject
s and their subclasses have copy constructors and assignment operators. They are extensible, and allow for deep copying.
From NotifierAppView.cpp:
void CNotifierAppView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { if ( pHint != NULL ) { if ( pHint->IsKindOf( RUNTIME_CLASS( CNotifyObject ) ) != FALSE ) { CNotifyObject * pNotify = (CNotifyObject*)pHint ; switch ( (long)(NotifyObjectType(pNotify)) ) { case NOTIFY_ERROR_OBJECT: { // use a reference to the object CErrorObject * Error = (CErrorObject*)pNotify ; if ( m_List.GetItemCount() >= 20 ) { m_List.DeleteItem(0) ; } int item = m_List.InsertItem( m_List.GetItemCount(), _T("CErrorObject") ) ; m_List.SetItemText( item, 1, Error->get_ErrorMessage() ) ; break ; } case NOTIFY_INFORMATION_OBJECT: { // copy the contents to a local variable CInformationObject Info = *(CInformationObject*)pNotify ; if ( m_List.GetItemCount() >= 10 ) { m_List.DeleteItem(0) ; } int item = m_List.InsertItem( m_List.GetItemCount(), _T("CInformationObject") ) ; m_List.SetItemText( item, 1, Info.get_InformationMessage() ) ; break ; } default: ; } } } }
Notice that the variable Error
is a pointer to a CErrorObject
, while Info
is a CInformationObject
which is a copy of the pNotify
object! This means that you can maintain a copy of the object in your view, if you want. You could extend the objects so that they contain a reference to the document so the document can update the objects directly.
Also, in the same code, there is a class called NotifyObjectType
, which takes a CNotifyObject
derived object and returns a value which can be compared in a switch
statement. No more if/else if/else if/else
coding! On the downside, it does use RUNTIME_CLASS
underneath, which some people find slightly more objectionable than RTTI! You pay your money and you take your choices.
Finally, there is absolutely no reason why you should feel constrained to use CObject
as the basis for CNotifyObject
; pick your own base class, and run with it. I have, on many occasions, overloaded CDocument::UpdateAllViews
to take other type objects as the third parameter (including void*
).
History
This is version 1.0.