Create a wait delay alternative to Application.DoEvents

2.3k views Asked by At

For years I create delays in my software using, for example:

Wait(10000)

Sub Wait(milliseconds)
    <here I get the current time>
    Do
         <here I loop until the current time passed in seconds and then exit this loop>
         Application.DoEvents()
    Loop
End Sub

The problem is, this uses a lot of CPU. I tried Thread.Sleep(1000), but this FREEZES my application while it's performing!
I tried using a Timer, but I STILL need a loop that doesn't freeze yet acts like Application.DoEvents(). It seems impossible.

My goal is to do this:

label1.text = "ok about to start"
Wait(5000)        
' the following line CAN NOT run until after 5 seconds. 
label1.text = "DONE"
1

There are 1 answers

7
Jimi On

How to execute code after a delay.

There are different methods to execute code after a delay or execute it asynchronously, after a delay or not. Here, I'm considering a Timer and simple implementations of the Async/Await pattern.

A loop that calls Application.DoEvent() should be avoided in any case.


► Using a Timer to delay the execution of a single instruction or the code in one or more methods:

You cannot await for a Timer, but you can use a method that creates a Timer and executes an Action when the Timer raises its event, signaling that the Interval specified has elapsed.

The method that follows accept as arguments a delay value and an Action delegate.
The Delay is used to set the Timers' Interval, the Action represent the code that will be executed when the Timer Ticks (I'm using a System.Windows.Forms.Timer here, since you seem to refer to a WinForms application).

Private waitTimer As New System.Windows.Forms.Timer()
Private TimerTickHandler As EventHandler

Private Sub Wait(delay As Integer, action As Action)
    waitTimer.Interval = delay
    TimerTickHandler = New EventHandler(
        Sub()
            action.Invoke()
            waitTimer.Stop()
            RemoveHandler waitTimer.Tick, TimerTickHandler
        End Sub)
    AddHandler waitTimer.Tick, TimerTickHandler
    waitTimer.Start()
End Sub

We can call this method when we need to execute code after a delay.

The Action can be a simple instruction: in this case, the Text of label1 will be set to "Done" after 5 seconds, while the UI Thread continues its operations:

label1.text = "About to Start..."
Wait(5000, Sub() Me.label1.Text = "Done")

The Action can also be a method:

Private Sub SetControlText(control As Control, controlText As String)
    control.Text = controlText
End Sub

' Elsewhere
Wait(5000, Sub() SetControlText(Me.label1, "Done"))

Of course the SetControlText() method can execute more complex code and, optionally, set a Timer itself, calling Wait().

► Using the Async/Await pattern.

An async method provides a convenient way to do potentially long-running work without blocking the caller's thread. The caller of an async method can resume its work without waiting for the async method to finish.

In simple terms, adding the Async modifier to a method, allows to use the Await operator to wait for an asynchronous procedure to terminate before the code that follows is executed, while the current Thread is free to continue its processing.


▬ Note that the Async modifier is always applied to a Function() that returns a Task or a Task(Of something) (something can be any value/reference a method can return).
It's only applied to a Sub() when the Sub (void) method represents an Event Handler. This is very important to remember and apply without exceptions (unless you're quite aware of the implications). ▬

Read the Docs about this (in the previous link) and these:

Async and Await
Don't Block on Async Code


A simple Async method can be used to delay the execution of an action:

Private Async Function Wait(delay As Integer, action As Action) As Task
    Await Task.Delay(delay)
    action?.Invoke()
End Function

This is similar to the Timer functions and acts in a similar way. The difference is that you can Await this method to both execute the code it runs asynchronously and to wait for its completion to run other code after the method returns. The calling Thread (the UI Thread, here), will continue its operations while the Wait() method is awaited.

Assume that, in a Button.Click handler, we want to execute an Action (a method that doesn't return a value) or a Function (a method that returns a value) and the code that follows should execute only after this Action or Function returns:

Private Async Sub button1_Click(sender As Object, e As EventArgs) Handles button1.Click
    Await Wait(5000, New Action(Sub() Me.label1.Text = "Done"))
    Await Wait(5000, Nothing)
    ' (...)
    ' ... More code here. It will execute after the last Await completes
End Sub

Here, we instruct to wait for 5 seconds, then set the Text of a Label to a value, wait other 5 seconds, doing nothing, then execute the code that follows

If we don't need to perform an Action, we can simply use Task.Delay():

Private Async Sub button1_Click(sender As Object, e As EventArgs) Handles button1.Click
    label1.text = "About to Start..."
    Await Task.Delay(5000)
    label1.Text = "Done"
    ' (...)
    ' ... More code here. It will execute after the last Await completes
End Sub

We can also use Async/Await to wait for the completion of a Task run in a ThreadPool Thread, calling Task.Run() to execute code in a Lambda expression:
(just an example, we shouldn't use a ThreadPool Thread for such a simple task)

Private Async Sub button1_Click(sender As Object, e As EventArgs) Handles button1.Click
    label1.text = "About to Start..."
    Await Task.Run(Async Function()
                       Await Task.Delay(5000)
                       BeginInvoke(New Action(Sub() Me.label1.Text = "Done"))
                   End Function)
    ' (...)
    ' ... More code here. It will execute after the last Await completes
End Sub

See also the Task.ContinueWith() method:

Creates a continuation that executes asynchronously when the target Task completes.