'Modal MFC dialog not shown due to idle checks in CWnd::RunModalLoop

Below I've put the source to CWnd::RunModal, which is the message loop run when you call CDialog::DoModal - it takes over as a nested message loop until the dialog is ended.

Note that with a couple of special case exception ShowWindow is only called when the message queue is idle.

This is causing a dialog not to appear for many seconds in some cases in our application when DoModal is called. If I debug into the code and put breakpoints, I see the phase 1 loop is not reached until this time. However if I create the same dialog modelessly (call Create then ShowWindow it appears instantly) - but this would be an awkward change to make just to fix a bug without understanding it well.

Is there a way to avoid this problem? Perhaps I can call ShowWindow explicitly at some point for instance or post a message to trigger the idle behaviour? I read "Old New Thing - Modality" which was very informative but didn't answer this question and I can only find it rarely mentioned on the web, without successful resolution.

wincore.cpp: CWnd::RunModalLoop

int CWnd::RunModalLoop(DWORD dwFlags)
{
    ASSERT(::IsWindow(m_hWnd)); // window must be created
    ASSERT(!(m_nFlags & WF_MODALLOOP)); // window must not already be in modal state

    // for tracking the idle time state
    BOOL bIdle = TRUE;
    LONG lIdleCount = 0;
    BOOL bShowIdle = (dwFlags & MLF_SHOWONIDLE) && !(GetStyle() & WS_VISIBLE);
    HWND hWndParent = ::GetParent(m_hWnd);
    m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);
    MSG *pMsg = AfxGetCurrentMessage();

    // acquire and dispatch messages until the modal state is done
    for (;;)
    {
        ASSERT(ContinueModal());

        // phase1: check to see if we can do idle work
        while (bIdle &&
            !::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
        {
            ASSERT(ContinueModal());

            // show the dialog when the message queue goes idle
            if (bShowIdle)
            {
                ShowWindow(SW_SHOWNORMAL);
                UpdateWindow();
                bShowIdle = FALSE;
            }

            // call OnIdle while in bIdle state
            if (!(dwFlags & MLF_NOIDLEMSG) && hWndParent != NULL && lIdleCount == 0)
            {
                // send WM_ENTERIDLE to the parent
                ::SendMessage(hWndParent, WM_ENTERIDLE, MSGF_DIALOGBOX, (LPARAM)m_hWnd);
            }
            if ((dwFlags & MLF_NOKICKIDLE) ||
                !SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
            {
                // stop idle processing next time
                bIdle = FALSE;
            }
        }

        // phase2: pump messages while available
        do
        {
            ASSERT(ContinueModal());

            // pump message, but quit on WM_QUIT
            if (!AfxPumpMessage())
            {
                AfxPostQuitMessage(0);
                return -1;
            }

            // show the window when certain special messages rec'd
            if (bShowIdle &&
                (pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
            {
                ShowWindow(SW_SHOWNORMAL);
                UpdateWindow();
                bShowIdle = FALSE;
            }

            if (!ContinueModal())
                goto ExitModal;

            // reset "no idle" state after pumping "normal" message
            if (AfxIsIdleMessage(pMsg))
            {
                bIdle = TRUE;
                lIdleCount = 0;
            }

        } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
    }

ExitModal:
    m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
    return m_nModalResult;
}


Solution 1:[1]

So to answer my own question, the solution I found was to explicitly call the following two methods:

ShowWindow(SW_SHOWNORMAL);
UpdateWindow();

CWnd::RunModalLoop is supposed to call these, but only when it detects the message queue is empty/idle. If that doesn't happen then the dialog exists and blocks input to other windows, but isn't visible.

From tracking messages I found WM_ACTIVATE was the last message being sent before things got stuck, so I added an OnActivate() handler to my Dialog class.

Solution 2:[2]

I found a very similar problem in my application. It only happened when the application was under a heavy load for drawing (the user can open many views, the views show real time data, so they are constantly refreshing, and each view must be drawn independently, and the process of drawing takes a lot of time). So, if under that scenario, the user tries to open any modal dialog (let's say, the "About" dialog, or if the app needs to show any modal dialog like a MessageBox), it "freezes" and the dialog only shows after pressing the ALT key. Analyzing RunModalLoop() I got to the same conclusion as you did, that is, the first loop (titled "phase1" in the code comments), is never called, since it needs the message queue to be empty, and it never is; the app then falls in the second loop, phase2, from which it never exits, because the call to PeekMessage() at the end of phase2 never returns zero (the message queue is very busy for so many views constantly updating). Since this code is in wincore.cpp, my solution was to find the closest function that could be overloaded, and lucky me found ContinueModal() which is virtual. Since I already had a class derived from CDialog which I always use as a replacement, I only had to define in that class a BOOL variable m_bShown, and an overload of ContinueModal():

BOOL CMyDialog::ContinueModal()
{
    // Begin extra code by Me
    if (m_nFlags & WF_CONTINUEMODAL)
    {
        if (!m_bShown && !(GetStyle() & WS_VISIBLE))
        {
            ShowWindow(SW_SHOWNORMAL);
            UpdateWindow();
            m_bShown = TRUE;
        }
    }
    // End extra code
    return m_nFlags & WF_CONTINUEMODAL;
}

This causes ContinueModal() to actually show the window if it has never been shown before. I set m_bShown to FALSE both in its declaration and in OnInitDialog(), so it arrives to RunModalLoop() in FALSE, and set it to TRUE immediately after showing the window to disable the little additional code snippet. This solved the problem for me and supposedly should solve it for anybody. The only doubts remaining are, 1) is RunModalLoop() called from somewhere else within MFC that would conflict with this modification? and 2) Why did Microsoft program RunModalLoop() this weird way leaving this glaring hole there that can cause an app to freeze without any apparent reason?

Solution 3:[3]

I recently had the same problem. I solved by sending the undocumented message 0x118 before calling DoModal(), which is handled in the phase2.

... PostMessage(0x118, 0, 0); return CDialog::DoModal();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mr. Boy
Solution 2
Solution 3 Eduardo Alonso