Nested event loops in modal dialogs and their effect on event handling

Hi,

I need some help understanding nested event loops in modal dialogs and there effect on event handling.

Here’s the scenario. I have a main window with a “Start” button whose event handler launches a dialog, for example:

onStart(self, e):
    dlg = MyDialog(self)
    print "going modal"
    dlg.ShowModal()
    dlg.Destroy()
    print "do work"
    doSomeWork()

MyDialog has an “OK” button to close the dialog. Now here’s where I’m not clear (and got in trouble today). If the user is fast enough, they can press “OK” to close the dialog and press “Start” to launch another dialog before doSomeWork() gets called. Say they do this 3 times, then the following is printed:

going modal
going modal
going modal
do work
do work
do work

I was expecting the onStart() handler to completely finish before handling the next “Start” button event, i.e.

going modal
do work
going modal
do work
going modal
do work

Why is the next “Start” button event handler being called before the previous finishes?

Thanks.

Attached is some code to reproduce the effect.

Run the application. Press the Start button. A dialog is displayed. Press the Stop button to close the dialog, but quickly press the Start button on the main frame while the dialog is closing. Notice how the dialog box reappears before ShowModal() call of the first dialog returns. Repeat. Finally, close the last dialog box. The following is printed:

going modal
going modal
going modal
do work
do work
do work

I was expecting:

going modal
do work
going modal
do work
going modal
do work

not the recursive calls above. Is this the normal, expected behavior? If so, why?

(Windows XP, wxPython 2.8.12.1, Pyhton 2.7.2)

showmodaltest.py (1.17 KB)

Const wrote:

Attached is some code to reproduce the effect.

Run the application. Press the Start button. A dialog is displayed.
Press the Stop button to close the dialog, but quickly press the Start
button on the main frame while the dialog is closing. Notice how the
dialog box reappears *before* ShowModal() call of the first dialog
returns. Repeat. Finally, close the last dialog box. The following is
printed:
...
not the recursive calls above. Is this the normal, expected behavior?
If so, why?

It's not impossible (obviously). A modal dialog runs a separate message
loop, but that message loop processes and dispatches messages for all
windows in your application, just like your main message loop. When you
close a dialog, there is actually a sequence of messages that get fired
-- it's not just one message. So, what's probably happening here is
that the Close handler calls EndDialog, which closes the window,
re-enables the parent, and then sends a message telling the modal
message loop to exit. In your case, you are getting your additional
left-click message (on the "Start" button) slipped in between the parent
re-enable and the exit of the modal message loop, so your button press
is being handled by the modal loop.

I admit I am surprised by this. Typically, the parent window is
disabled before the message loop starts and enabled after it exits. You
can actually do this yourself, by calling self.Enable(0) and
self.Enable(1) on either side of the ShowModal call.

···

--
Tim Roberts, timr@probo.com
Providenza & Boekelheide, Inc.

Because you're in a pseudo-concurrential environment, so events
can be triggered, and thus processed, at any moment.

···

On Thu, 5 Jan 2012 09:58:55 -0800 (PST) Const <caleb.constantine@gmail.com> wrote:

Finally, close the last dialog box. The following is printed:

    going modal
    going modal
    going modal
    do work
    do work
    do work

I was expecting:

    going modal
    do work
    going modal
    do work
    going modal
    do work

not the recursive calls above. Is this the normal, expected behavior? If
so, why?

--
Printers do it without wrinkling the sheets.

Here (Linux, wx 2.8.12.1-6 & python 2.7.2+) it is working as
expected:
$ python TEST.py
going modal
do work
going modal
do work
going modal
do work

JY

···

On Thu, 5 Jan 2012 10:14:23 -0800 Tim Roberts <timr@probo.com> wrote:

I admit I am surprised by this. Typically, the parent window is
disabled before the message loop starts and enabled after it exits. You
can actually do this yourself, by calling self.Enable(0) and
self.Enable(1) on either side of the ShowModal call.

--
Somewhere, just out of sight, the unicorns are gathering.

This works (so does wx.WindowDisabler), except it can put the application’s main frame behind another application window after ShowModal() returns. I suppose I could call Frame.Raise() after ShowModal(), although this causes some serious flickering. To reproduce: Start the test app. Bring another application window in front of the test app. Bring the test app to the front. Click Start to show the dialog. Click Stop to close the dialog. Observe the main frame is now behind the other application window.

···

On Thursday, 5 January 2012 14:44:23 UTC-3:30, Tim Roberts wrote:

I admit I am surprised by this. Typically, the parent window is
disabled before the message loop starts and enabled after it exits. You
can actually do this yourself, by calling self.Enable(0) and
self.Enable(1) on either side of the ShowModal call.

If this is normal behavior, then it seems unsafe to me. Shouldn’t the current event handler (onShowDialog() below) finish before the event loop processes another user triggered event? If this is not the case, how can I ever reliably do work after calling ShowModal()? For example:

onShowDialog(self, event):
    dlg = MyDialog(self)
    res = dlg.ShowModal()
    doSomeWork()

Potentially any user triggered event (e.g. button presses) might occur and change the state of the application in unexpected ways before doSomeWork() ever gets called. In the example in previous posts, I simply get recursive behavior because the user clicks on the same button hence calling the same event handler, but the user can potentially click any button. Then, that button’s event handler will be completed before doSomeWork() is even called!. This overlapping of event handling can have disastrous results.

So, the only way to reliably do some work after calling ShowModal() is as follows:

onShowDialog(self, event):
    dlg = MyDialog(self)
    with wx.WindowDisabler():
        res = dlg.ShowModal()
        doSomeWork()

This works, but can send the main application frame behind another window; see previous post.

I’ll let Robin and the others answer about if that’s normal or not. An alternative would be to use PubSub though. When the dialog closes, you publish a message and the listener will fire and run whatever you need.

···

Mike Driscoll

Blog: http://blog.pythonlibrary.org

As I said, I'm NOT a wx guru - I only told you it seems "normal"
from a concurrencial point of view, where events can be triggered any
time - Though, things are very different in concurrent languages.

Tim told you that this behavior surprises him as there is a locking
logic to precisely avoid that (and it is working as expected under
Linux).
So, this could be linked to w$ internals or to a bug or to something
else; as Mike said, wait for wx gurus' answer:)

JY

···

On Fri, 6 Jan 2012 06:34:15 -0800 (PST) Const <caleb.constantine@gmail.com> wrote:

If this is normal behavior, then it seems unsafe to me.

--
See store for details.

The behavior is a bit surprising, but Tim's explanation is valid and is very likely what is happening. Given that explanation it seems likely that the problem can be avoided simply by giving the dialog's event loop a chance to process pending events before it is terminated, and, indeed, adding a wx.Yield to your sample before the Close does restore the expected behavior and all is well with the world again.

···

On 1/6/12 6:34 AM, Const wrote:

If this is normal behavior, then it seems unsafe to me. Shouldn't the
current event handler (`onShowDialog()` below) finish before the event
loop processes another user triggered event? If this is not the case,
how can I ever reliably do work after calling `ShowModal()`? For example:

onShowDialog(self, event):
dlg = MyDialog(self)
res = dlg.ShowModal()
doSomeWork()

Potentially any user triggered event (e.g. button presses) might occur
and change the state of the application in unexpected ways before
`doSomeWork()` ever gets called.

--
Robin Dunn
Software Craftsman

Const wrote:

If this is normal behavior, then it seems unsafe to me. Shouldn't the
current event handler (`onShowDialog()` below) finish before the event
loop processes another user triggered event?

The situation is not that cut-and-dried. Remember that, to the
operating system, the modal dialog is just another window in your
application. Clicking a button on that window is exactly the same as
clicking a button in your main window. Both of them are just a cascade
of events that get sent to your thread's message queue.

Now, as I said before, I am surprised by the behavior you're seeing.
When I have written modal window handlers, the process has been:

    Disable parent window
    while( event loop alive )
        GetMessage
        DispatchMessage
    Enable parent window

With that architecture, the situation you're describing cannot happen,
because the parent window is disabled until the event loop exits. There
can't be any new clicks until the parent is re-enabled, and anything
that happens afterward will stay in the queue until we get back to our
main message loop. It APPEARS, from the behavior, that ShowModal is not
doing that. If so, I think you could argue that's a bug in wx. I will
look into the source code and see what I can find..

Ah, it turns out there's already a wxWidgets bug report about this:
    wxTrac has been migrated to GitHub Issues - wxWidgets
The obvious fix apparently has undesirable side effects, so it has not
been fixed yet.

So, the only way to reliably do some work after calling `ShowModal()`
is as follows:

    onShowDialog(self, event):
        dlg = MyDialog(self)
        with wx.WindowDisabler():
            res = dlg.ShowModal()
            doSomeWork()

This works, but can send the main application frame behind another
window; see previous post.

The current ShowModal handler does create a WindowDisabler before
starting the loop. The problem seems to be that it deletes the disabler
during its OnExit handler, which is called in response to an event while
the message loop is still running.

···

--
Tim Roberts, timr@probo.com
Providenza & Boekelheide, Inc.

Thanks for finding that ticket Tim. I'll add some notes to it and a link to this conversation.

···

On 1/6/12 10:58 AM, Tim Roberts wrote:

Const wrote:

If this is normal behavior, then it seems unsafe to me. Shouldn't the
current event handler (`onShowDialog()` below) finish before the event
loop processes another user triggered event?

The situation is not that cut-and-dried. Remember that, to the
operating system, the modal dialog is just another window in your
application. Clicking a button on that window is exactly the same as
clicking a button in your main window. Both of them are just a cascade
of events that get sent to your thread's message queue.

Now, as I said before, I am surprised by the behavior you're seeing.
When I have written modal window handlers, the process has been:

     Disable parent window
     while( event loop alive )
         GetMessage
         DispatchMessage
     Enable parent window

With that architecture, the situation you're describing cannot happen,
because the parent window is disabled until the event loop exits. There
can't be any new clicks until the parent is re-enabled, and anything
that happens afterward will stay in the queue until we get back to our
main message loop. It APPEARS, from the behavior, that ShowModal is not
doing that. If so, I think you could argue that's a bug in wx. I will
look into the source code and see what I can find..

Ah, it turns out there's already a wxWidgets bug report about this:
     wxTrac has been migrated to GitHub Issues - wxWidgets
The obvious fix apparently has undesirable side effects, so it has not
been fixed yet.

So, the only way to reliably do some work after calling `ShowModal()`
is as follows:

     onShowDialog(self, event):
         dlg = MyDialog(self)
         with wx.WindowDisabler():
             res = dlg.ShowModal()
             doSomeWork()

This works, but can send the main application frame behind another
window; see previous post.

The current ShowModal handler does create a WindowDisabler before
starting the loop. The problem seems to be that it deletes the disabler
during its OnExit handler, which is called in response to an event while
the message loop is still running.

--
Robin Dunn
Software Craftsman

This is much better, except I do get a “wxApp::Yield(): wxYield called recursively” exception if I rapidly click on the “Stop” button. Using wx.SafeYield() seems to fix rapidly clicking the mouse button, but pressing the “Enter” key still results in a exception.

Traceback (most recent call last):
File “C:\sandbox\showmodaltest.py”, line 41, in _onStopClick
wx.SafeYield()
File “C:\Programs\Python27\lib\site-packages\wx-2.8-msw-unicode\wx_core.py”, line 7720, in SafeYield
return core.SafeYield(*args, **kwargs)
wx._core.PyAssertionError: C++ assertion “wxAssertFailure” failed at …\src\msw\app.cpp(707) in wxApp::Yield(): wxYield called recursively

If I place wx.SafeYield() after the call to Close() or EndModal(), everything seems to work (in the demo script at least). Weird.