DoEvents Oddity

Mike_R

Junior Contributor
Joined
Oct 20, 2003
Messages
316
Location
NYC
I was experimenting with DoEvents and have an oddity... I'm having trouble having it acknowledge the Form_Closing() Event properly.

In it's simplest incarnation, I tried this:
Visual Basic:
Private Sub btnStart_Click(ByVal sender As System.Object, _
                            ByVal e As System.EventArgs) _
                            Handles btnStart.Click
    Do
        Application.DoEvents()
    Loop
End Sub
Now this didn't do so well, LOL. When the btnStart was clicked, the use of 'Application.DoEvents' did allow the Form to exit, but:

(1) I had to click the Red Close <X> twice to get it to acknowledge.
(2) Although the Form did in fact disappear, it was still "running", which I could tell because the Immediate window in the debugger was still open.

The fact that it was still "running" although "gone" is interesting, this could not occur in VB6, but I guess as long as the Code is "alive" (that is, still running), then I guess it is still on the stack and the Object/Form (or at least some part of it) still exists?


My next idea was to do two things:

(1) Use Mod 1000 so that I am not flooding so many DoEvents calls. I didn't expect anything from this, although it should speed up the loop since DoEvents is pretty heavy.

(2) I added a 'Private m_Shutdown As Boolean' to trigger the exit of the Loop.


Visual Basic:
Private m_Shutdown As Boolean

Private Sub btnStart_Click(ByVal sender As System.Object, _
                            ByVal e As System.EventArgs) _
                            Handles btnStart.Click
    Dim count As Long
    Do
        count += 1
        If (count Mod 1000) = 0 Then
            Application.DoEvents()
        End If
    Loop Until m_Shutdown
End Sub

Private Sub Form1_Closing(ByVal sender As Object, _
                          ByVal e As System.ComponentModel.CancelEventArgs) _
                          Handles MyBase.Closing
    MessageBox.Show ("Form1_Closing()")
    m_Shutdown = True
End Sub

Private Sub Form1_Closed(ByVal sender As Object, _
                         ByVal e As System.EventArgs) _
                         Handles MyBase.Closed
    MessageBox.Show ("Form1_Closed()")
End Sub
The results of the above were much better... but not perfect:

(1) It did now close cleanly. That is, when the Form Closed, the Loop was no longer running. Perfect. :)

(2) However, it does still require that the Form's Red Close <X> be clicked twice.


Any ideas on why the <X> needs to be hit twice? (See Attached VB.Net Project.)

Hmmm... ok, wait, if you run the project, hit the <Start> button on the Form and then hit the <X> button, the interesting thing is that on the first click, the <Start> button will depress. This seems to occur no matter where you click on the Form or Titlebar as well.

So is this a sort of "TakeFocusOnClick" sort of situation? There must be a simple way to allow for the first click to actually go where the User wants, no? I'm just too n00b in .Net to quite understand what is happening here.

Thanks in advance for all those interested/help out... :)
 

Attachments

Last edited by a moderator:
When the start button is clicked, the Button Click event is sent to the Application's event que but is never allowed to run because you create an infinite loop in the button click event. The next time you click a button the start button's click event is deqeued and run and the program in sent into an infinite loop again. The process is repeated for each event that is raised, which is why it takes two click of the close button to close the form.
 
Thank you, Diesel, for that excellent explanation, that was really clear.

So how now do I fix it? This must be a very common situation, no? The User clicks a button which kicks off some long process... The process reports to a ProgressControl or the like over time, while calling 'DoEvents' within the loop so that the User can Cancel out of it, if he/she wishes. But here it's not executing cleanly.

I tried the following protection, in an attempt to block the infinite loop when re-entering the 2nd time:
Visual Basic:
Private Sub btnStart_Click(ByVal sender As System.Object, _
					       ByVal e As System.EventArgs) _
					       Handles btnStart.Click
    Static count As Long
    If count = 0 Then
        Do
            count += 1
            If (count Mod 1000) = 0 Then
                Application.DoEvents()
            End If
        Loop Until m_Shutdown
    End If
End Sub
But it did not improve things. Then to just see if it was getting called twice as you stated, I added this:
Visual Basic:
MessageBox.Show("Count = " & count)
to the first line in the routine (after declaring the 'Static Count As Long') and, amazingly, this fixed it! The Sub does not even get called a 2nd time. Hmmm....

So I figured that the issue was that the MessageBox.Show() was forcing the App to yield, much like calling 'DoEvents' or 'Sleep()', so I tried replacing the MessageBox.Show() and got the following:
Visual Basic:
Private Sub btnStart_Click(ByVal sender As System.Object, _
					       ByVal e As System.EventArgs) _
					       Handles btnStart.Click
    Static count As Long

    Application.DoEvents()           ' <-- Added
    System.Threading.Thread.Sleep(0) ' <-- Added

    If count = 0 Then
        Do
            count += 1
            If (count Mod 1000) = 0 Then
                Application.DoEvents()
                System.Threading.Thread.Sleep(0) ' <-- Added
            End If
        Loop Until m_Shutdown
    End If
End Sub
And the above fails, clicking the Red Close <X> the first time still visibly depresses the <Start> button instead. Yet the MessageBox.Show() worked... But there must be another way other than having to fire up a MessageBox, no?

What is the standard approach to handling this situation?
 
Last edited:
Normally, the button would call a function that does the looping. If you want to allow a cancel mid-process, you'll want a form-level variable to handle closing. For example:

C#:
public class Form1 : System.Windows.Form
{
    private bool bRunning = false;
    private bool bPaused = false;
    
    private void ClickEvent()
    {
        bRunning = true;
        do
        {
            if(!bPaused)
            {
                // Your processing code here, including progress bar updates
            }
            Application.DoEvents();
        } while(bRunning);

        // If needed:
        // Application.Exit();
    }

    private void Form1_Closing(...)
    {
        if(bRunning)
        {
            bPaused = true;
            DialogResult result = MessageBox.Show("Are you sure you want to Cancel?");
            if(result == DialogResult.Yes)
            {
                bRunning = false;
            }
            {
                e.Cancel = true;
                bPaused = false;
            }
        }
    }
}

All typed in here, so no syntax checking etc. Should give you the jist.

-ner
 
Nerseus, thank you so much for taking a look at this... but it didn't seem to work. :(


Nerseus said:
Normally, the button would call a function that does the looping.
Yes, my original program has the button calling a Monte Carlo simulation that takes a while... and the Button_Click() routine is still held on the Stack. In VB6 this would not affect other Events from firing (as long as DoEvents, Sleep() or some other Yielding process is called) but here it is giving me trouble...


Nerseus said:
If you want to allow a cancel mid-process, you'll want a form-level variable to handle closing. For example...
Ok, I looked at your code and could not believe that it would work. It's a cleaner example than what I was doing, but if you look at the basic elements they are the same:

  1. They are both preventing recursion into the loop. My code is using 'If count = 0 Then' while yours is using 'if(!bPaused)'.
  2. They both Loop, cutting off with 'Until m_Shutdown' or 'While(bRunning)' respectively.

I would have been surprised if your code somehow did anything different. (And it didn't. :() Here is the exact code I used:
Code:
private bool bRunning = false;
private bool bPaused = false;
private int btnClickCount = 0;

private void btnStart_Click(object sender, System.EventArgs e)
{
    btnClickCount++;
    bRunning =true;
    Do
    {
        if(!bPaused)
        {
            // Processing code here, Progress bar updates...
        }
        Application.DoEvents();
    } while(bRunning);
}

private void Form1_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    if(bRunning)
    {
        bPaused = true;
        DialogResult result = MessageBox.Show("Are you sure you want to Exit?",
            "Exit?",
            MessageBoxButtons.YesNo);
        if(result == DialogResult.Yes)
        {
            bRunning = false;
        }
        Else
        {
            e.Cancel = true;
            bPaused = false;
        }
    }
}

private void Form1_Closed(object sender, System.EventArgs e)
{
    string msgStr = "btnClickCount = " + btnClickCount.ToString("0");
    MessageBox.Show(msgStr);
}
It is exactly the code you provided, although I added the int btnClickCount to see how many times the btnStart_Click() Event was actually getting called.

The result is that btnClickCount returns 1, even though, graphically/visibly the Start button seems to depress a 2nd time when one clicks the Red [X] button to close the Form. And it is only on the second click on the Red [X] that the Form actually closes.

The project is attached, if you want to give it a try for yourself. Just hit the <Start> buttton and then click the Red [X] to exit the Form. You should have to click the [X] twice. And if you look closely, you'll see the <Start> button depress instead of the [X]. Very odd...

Thanks so much for you time so far! But I do hope you can get it... actually, I'm surprised that it is proving tricky at all!

Thanks again. :)
 

Attachments

Last edited by a moderator:
I see what you mean - only it's a bit more than you describe. Meaning, when you click the start button it does run the code immediately. But, it's not waiting for a second click - it's waiting for any input. If you click anywhere on the form - the title bar, the form itself, etc. - it seems to cancel that click.

I didn't test this, but my guess is you could fix this one of two ways (maybe) or definitely one way (hacky). The easiest seems to use the API SendMessage with a LBUTTONUP message (can't remember the constant offhand). This would fake it so it seems like the user clicked and then let go of the mouse.
The other possible solution would take more experimenting but would involve overriding WndProc and intercepting clicks in there (maybe in conjuction with the form's KeyPreview set to true).

The definite solution is to enable a timer in the button's click and then have the timer call the function to do the loop. I call it hacky, but it would work because it uncouples the events from the procedural looping code.

An interesting problem - one I'd look into if I had more time.

-ner
 
Ner,

Thanks so much for taking a look at this...



Nerseus said:
I see what you mean - only it's a bit more than you describe. Meaning, when you click the start button it does run the code immediately. But, it's not waiting for a second click - it's waiting for any input. If you click anywhere on the form - the title bar, the form itself, etc. - it seems to cancel that click.
Yes, I wrote earlier:

"If you run the project, hit the <Start> button on the Form and then hit the <X> button, the interesting thing is that on the first click, the <Start> button will depress. This seems to occur no matter where you click on the Form or Titlebar as well."



Nerseus said:
I didn't test this, but my guess is you could fix this one of two ways (maybe) or definitely one way (hacky)....
Yes the SendMessage/Subclassing ideas are interesting, but sure seem overkill for something like this!

I did think of the Timer idea, and I agree with you that it is best. I don't even think of it as "Hacky" as you say, it's really pretty clean...

But it's also pretty "Dumb" if you think about it! I mean does every click that kicks off a long running process always have to call such a process indirectly? And has no-one else hit this before? Seems impossible, it's too common a situation... Very odd!



An interesting problem - one I'd look into if I had more time.
-ner
Oh, no problem, I thank you very much for the time you took!

Thanks Ner :)
 
Back
Top