2

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"
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • What's wrong with the Timer? Which Timer did you try? Can you use Tasks? Meaning, have an `async` Function you can await and call `Task.Delay()`? You can pass a delay value and an `Action()` or `Func()` to the method, async or not async. Btw, if you can use async stuff, `Wait(5000) ` can be set locally with just `Await Task.Delay(5000)`. – Jimi Mar 09 '20 at 04:07
  • thanks for responding. never used tasks or async and I looked online and only saw C language codes so didn't understand. I did try timer but it doesn't loop or wait until it's done until it runs the code after I call the timer. The label1.text = "DONE" is only supposed to run AFTER 5 seconds has passed. Can I Create a loop using tasks or async thingy without freezing the app yet giving it an application.doevents alternative so I can do other stuff meanwhile? –  Mar 09 '20 at 08:35
  • What version of Visual Studio are you using? You need at least VS 2012 to have async/await available. – Jimi Mar 09 '20 at 14:02
  • I'm using VS 2019. Just don't know how to use aync and await. So these with 2 I can actually create a loop until timer is done etc? –  Mar 09 '20 at 20:48
  • The main point is **not** to use loops at all. `Await` allows to wait for an async procedure to terminate before the code that follows is executed, while the current Thread is free to continue its processing. You cannot do that with a Timer, but you can use a Timer to execute an Action. I'll post a couple of examples. In the meanwhile, take a look at [Asynchronous programming with Async and Await](https://learn.microsoft.com/en-us/dotnet/visual-basic/programming-guide/concepts/async/). – Jimi Mar 09 '20 at 21:37

1 Answers1

3

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.

Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Jimi, first of all you're awesome. I mean Detailed your response, absolutely amazing! Thank you so much. I did try the: DelayWithTimer(5000, Sub() Me.Label1.Text = "Done") and noticed it works just like a timer. It does not have a loop. So technically I can acomplish the same thing with a timer. Here's the probem with that. I don't want to jump to different subs every time I need to create a pause. If I have a SUB, that is supposd to do a lot of tasks, for example web scraping with webbrowser, it needs to wait several times during until something has been done or loaded. –  Mar 11 '20 at 01:32
  • so I MUST have some type of loop. I really don't want to have the async or timer start another sub I just want it to resume with the code (But without using application.do events loop which uses so much CPU), but at the same time act like applicaiton.doevents has been fired.I'm sorry if I'm not explaining well. if a 1 single sub is used to open a url in a browser, sign in, do bunch of stuff and that's it, the timer/async above will not work. 1. Load url 2. WAIT until fully loaded 3. Enter login details. sign in 4. WAIT until signed in.. –  Mar 11 '20 at 01:33
  • with async or timer, it means I Have to jump from one sub to another EVERY TIME I have to create a pause. doe sthat make sense? really don't want to be doing that. Is there there no way this command can run and it wont run the command after until the 5 seoncds passed ? DelayWithTimer(5000, Sub() Me.Label1.Text = "Done") –  Mar 11 '20 at 01:33
  • To log on to a a web Page using a WebBrowser control, you use its `DocumentCompleted` event, not a loop. You don't use loops to wait for something to happen, you use events or Tasks. For example: [Auto Website Login: SetAttribute not working](https://stackoverflow.com/a/54030367/7444103) and [How to get an HtmlElement value inside Frames/IFrames?](https://stackoverflow.com/a/53218064/7444103). You need to learn how to use these tools. The async `Wait()` method I posted (or `await Task.Delay()`) can be used to wait in-place for a given time when needed (not to wait for a page to load, though). – Jimi Mar 11 '20 at 01:40
  • Just one last Q, any ideas how I Can solve the question below? So sorry to bug you with this –  Mar 11 '20 at 05:29
  • @Andrea ~ Don't forget to mark this as the answer, if it is. SO lives and dies by its Q&A system. – InteXX Mar 11 '20 at 21:01
  • I'm sorry a bit new to this stackflow. Is anyone seeing this comment? –  Mar 15 '20 at 00:23