Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / MFC
Article

Notifying the Document

Rate me:
Please Sign up or sign in to vote.
4.36/5 (11 votes)
12 Jul 2006CPOL6 min read 63.1K   1.2K   35   11
An article on delivering objects to the document in a document/view architecture, using the WM_NOTIFY message.

Notifications

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

License

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


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Dan Bloomquist1-Jan-17 14:51
Dan Bloomquist1-Jan-17 14:51 
GeneralMy vote of 5 Pin
ninepin15-Jan-13 20:52
ninepin15-Jan-13 20:52 
GeneralRe: My vote of 5 Pin
Michael Bergman17-Jan-13 4:54
Michael Bergman17-Jan-13 4:54 
QuestionWhy not send message to main frame instead? Pin
Damir Valiulin8-Jul-06 16:47
Damir Valiulin8-Jul-06 16:47 
Why not send message to main frame of the GUI thread? Simpl pass its HWND handle to the worker thread when you start it and send all messages to it. Then when you recieve a message in the main frame you can easily determine the active view and/or active document and pass the necessary information to it.
AnswerRe: Why not send message to main frame instead? Pin
Michael Bergman9-Jul-06 7:52
Michael Bergman9-Jul-06 7:52 
GeneralRe: Why not send message to main frame instead? Pin
Neville Franks9-Jul-06 11:14
Neville Franks9-Jul-06 11:14 
GeneralRe: Why not send message to main frame instead? Pin
Michael Bergman9-Jul-06 18:39
Michael Bergman9-Jul-06 18:39 
GeneralRe: Why not send message to main frame instead? Pin
Damir Valiulin10-Jul-06 11:16
Damir Valiulin10-Jul-06 11:16 
GeneralRe: Why not send message to main frame instead? Pin
Michael Bergman10-Jul-06 13:13
Michael Bergman10-Jul-06 13:13 
GeneralRe: Why not send message to main frame instead? Pin
Damir Valiulin10-Jul-06 18:18
Damir Valiulin10-Jul-06 18:18 
GeneralGood Article Pin
Neville Franks7-Jul-06 13:54
Neville Franks7-Jul-06 13:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.