Monday, August 27, 2007

What happens during MessageBox.Show()?

Last Friday my pair-programming partner said that System.Windows.Forms.Timer has reentrance problem. By reentrance I mean a situation where during processing of one tick generated by a timer, another tick is triggered and now both are processed simultaneously (which was bad in our case). For this to happen the processing of some tick needs to take longer than the interval set on the timer. After I expressed my doubts about winforms timer having this issue, he skilfully put together an example, sth like the following.

 1 #pragma indent
 3 using System.Windows.Forms
 4 using Nemerle.IO
 6 def form = Form()
 7 def timer = Timer()
 8 timer.Interval = 5000
 9 timer.Tick += (_,_) => print(MessageBox.Show("bum!"))
10 form.Shown += (_,_) => timer.Start()
11 Application.Run(form)

The code is written in the Nemerle programming language, but that is not important. In line 9 a function is defined and attached to handle the tick event. As a consequence, on each tick the following code is executed. print(MessageBox.Show("bum!")) I used the print statement just to make it obvious that this function doesn't return until user closes the msgbox.

Anyway, I was certain that it will prove there is no reentrance problem. However contrary to my expectations, those unbearable message boxes started to pop up, stacking on one another.

Now a little bit about the winforms timer. The handlers attached to the tick event are executed on the GUI thread. Each time the interval elapses, the timer adds an "execute handlers attached to me" event to the event queue. This should guarantee no reentrance since events on the queue are processed one by one.

Processing of an event triggered by the timer can't complete until the messagebox closes. So, it seems that with each new messagebox we have new event being processed. How can that be?

Well, there is a static method on Application class, called DoEvents. You can call it from inside of a method handling some event. Tha MessageBox.Show must be doing sth similar. I wasn't able to reflect into that, because the displaying of the messagebox is eventually delagated to a non managed code. It looks sth like the following but I have deleted some lines.

 1 Application.BeginModalMessageLoop();
 2 try
 3 {
 4     result = Win32ToDialogResult(SafeNativeMethods.MessageBox(new HandleRef(owner, handle), text, caption, type));
 5 }
 6 finally
 7 {
 8     Application.EndModalMessageLoop();
 9 }
10 return result;

As far as I can tell BeginModalMessageLoop() should be rather named PrepareForModalMessageLoop(), because what it does is mainly to disable (gray out) all the forms in the application, EndModalMessageLoop(), on the other hand, enables them back.

I guess that the MessageBox.Show handles events from the queue until there is a message that causes the msgbox to close, upon which the Show method finally returns the appropriate DialogResult value.

CLR Debugger confirms or at least doesn't contradicts my assumption. I attach a fragment of a stack trace from execution of the example. out.exe!Main._N__N_l64198062 identifies the function handling the tick event. Notice that the second call is in the control flow of the first.

out.exe!Main._N__N_l64198062(object _N_u1626 = {Interval = 5000}, System.EventArgs _N_u1627 = {System.EventArgs}) Line 12 + 0xc bytes Unknown
System.Windows.Forms.dll!System.Windows.Forms.Timer.OnTick(System.EventArgs e) + 0x17 bytes
System.Windows.Forms.dll!System.Windows.Forms.Timer.TimerNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0x36 bytes
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg = 275, System.IntPtr wparam, System.IntPtr lparam) + 0x75 bytes
[Native to Managed Transition]
[Managed to Native Transition]

System.Windows.Forms.dll!System.Windows.Forms.MessageBox.ShowCore(System.Windows.Forms.IWin32Window owner = null, string text, string caption, System.Windows.Forms.MessageBoxButtons buttons, System.Windows.Forms.MessageBoxIcon icon, System.Windows.Forms.MessageBoxDefaultButton defaultButton, System.Windows.Forms.MessageBoxOptions options, bool showHelp) + 0x1f8 bytes
System.Windows.Forms.dll!System.Windows.Forms.MessageBox.Show(string text) + 0x26 bytes
out.exe!Main._N__N_l64198062(object _N_u1626 = {Interval = 5000}, System.EventArgs _N_u1627 = {System.EventArgs}) Line 12 + 0xc bytes Unknown

line used to compile the example
ncc -r timer.n

No comments: