Application.DoEvents is a method provided in the framework that can be used to ensure that your application remains responsive to user events during extended processing tasks. But if it is used incorrectly it can create errors that are extrememly difficult to debug. This problem has caused many people to recommend against the use of DoEvents, but the alarm is unjustified. A few small changes to your code will ensure that DoEvents can be used safely.
Why is it needed?
Windows generally looks after the sequence of events in your code in a manner that is logical and effective. But sometimes you need to change the default behaviour.
For instance, if you have a loop that executes for an extended period, you might want to create a Cancel button that allows the user to cancel the loop before it completes. So you could create a button that sets a flag, and then insert a test for the value of that flag in your loop – if it’s set, terminate the loop. But this doesn’t actually work! Windows will not process the cancel button click event until the loop ends, defeating the purpose of the button.
This is the problem that DoEvents solves. By inserting the statement in your loop, you force Windows to process any outstanding events that are queued up. If that includes the button click then the click event code is executed, the exit flag is set, and when the loop continues it sees that exit flag and terminates. Problem solved!
What’s the problem?
That process seems effective when it is looked at in isolation, but the point that must be considered is that, while DoEvents allows the user to click the Cancel button and have that click recognised before the loop terminates, it also allows the user to click any other button (or select a menu item, or whatever). Does that matter? Well, it depends on the application, and in some cases it can matter a great deal.
If the user clicks the button that started the loop, then the loop will restart. In some cases, repeating the process won’t matter, but in other cases this second processing will create incorrect results. But when the second invocation of the loop finishes, the first invocation (the one that got interrupted) continues from where it left off. Again, this won’t matter (other than the wasted processing time) for same cases. But in other cases it might completely destroy the calculation.
And if you consider why DoEvents was used in the first place – the loop takes time to complete and the user might want change their mind – then you can see the possibility that the user might also think that the machine has stopped, and instead of cancelling, might restart the procedure, creating the problem described above.
It’s obvious how that problem might be easily missed in testing, or, if it accidentally occurs, how debugging it would be very difficult due to the convoluted execution sequence.
What’s the fix?
It depends on the application. If the only issue is accidentally restarting the loop, then add code in the looping process that prevents this happening. This is known as ‘re-entrancy protection’ – code that protects against re-entering the procedure.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click Static InProcess As Boolean = False If InProcess Then Exit Sub 'If the loop is already running, exit InProcess = True 'Prevent this process from re-starting Do ' Something that will take some time Application.DoEvents 'Allow GUI actions to process If ExitFlag = True Then 'Did user request abort? Exit Do ' Yes - early exit End If Loop InProcess = False 'Allow this process to re-start End Sub
That fix could even be modified to display a message such as “Processing – please wait” if the user tries to re-start the process. It is often all the protection that is needed in simple cases. That is, other things that the user could do, such as clicking other buttons on the form, typing in text boxes, or even closing the form, will not cause a problem.
In more complex cases it may not be enough to protect the routine against re-starting – you might need to prevent other operations from occurring.
One way to do this is to disable controls on the form before the loop starts, and only enable them again after the loop ends. This has the benefit of giving the user a visual cue that user input (other than the Cancel button) is not currently allowed.
But working out what operations need to be protected, and what controls need to be disabled, can become complex when there are a lot of processes and controls. In that case it might be useful to re-design the form to simplify things so that protection against re-entrancy can be implemented more easily. For instance, if the process is started from a dialog, then only the one ‘start’ button on the dialog needs to be protected – the fact that the dialog must complete before any code in the main form can continue, means that everything on that main form is automatically protected.
Many advocates of ‘good’ OO design maintain that DoEvents should not be used. While it is true that improved design can often eliminate the need for enabling the GUI during a long process, it is not correct to claim that DoEvents demonstrates poor design. What is correct is that a poor implementation of DoEvents can create nasty errors, but that is a feature of many compoennts in .Net other than DoEvents.
It is also not correct, as is frequently claimed, that the solution is to put the time-consuming process into its own thread. Doing so will mean that the GUI remains responsive while the process runs, which deals with the original problem of enabling a Cancel button. But the issue of re-entrancy is just as much a problem with a separate thread as it is with DoEvents. If the GUI is active, the process can be accidentally restarted, and restarting it in a new thread has the same ptotential problem as restarting a procedure in the GUI thread. Depending on the process, it might not matter (apart from the extra delay) or it might completely corrupt the calculation. And debugging the cause of the problem is no easier (in fact, might be a lot harder) if threads are involved. If threads are used in order to keep the GUI actve while the process executes, the problem of re-entrancy still needs to be dealt with.
Use DoEvents in cases where the GUI needs to be kept alive during a long process, but be aware of the problem that could be caused if the user interacts with your form while that process is running. Look at every action the user could take, and work out whether that will interfere with the process. If it can, then it must be prevented from hapening while the process is running. If the procedures to do that become too complex, consider re-designing the form.
Use threading where you want to keep the GUI alive during the process and you want the user to be able to do things with your form. If you need to prevent the user from re-starting the threaded process (and you usually will) then implement the same sort of re-entrancy protection you would have used with DoEvents, except that you will need to be very selective in how it is applied, in order to ensure those things the user can do safely are, in fact, accessible.