How can I run code in a background thread and still access the UI?

While I’m glad you found a solution, I advise against using Application.DoEvents() because it is bad practice.

Please see this blog post: Keeping your UI Responsive and the Dangers of Application.DoEvents.

Simply put, Application.DoEvents() is a dirty workaround that makes your UI seem responsive because it forces the UI thread to handle all currently available window messages. WM_PAINT is one of those messages which is why your window redraws.

However this has some backsides to it… For instance:

  • If you were to close the form during this “background” process it would most likely throw an error.

  • Another backside is that if the ScanButtonInForm1() method is called by the click of a button you’d be able to click that button again (unless you set Enabled = False) and starting the process once more, which brings us to yet another backside:

  • The more Application.DoEvents()-loops you start the more you occupy the UI thread, which will cause your CPU usage to rise rather quickly. Since every loop is run in the same thread your processor cannot schedule the work over different cores nor threads, so your code will always run on one core, eating as much CPU as possible.

The replacement is, of course, proper multithreading (or the Task Parallel Library, whichever you prefer). Regular multithreading actually isn’t that hard to implement.

The basics

In order to create a new thread you only need to declare an instance of the Thread class and pass a delegate to the method you want the thread to run:

Dim myThread As New Thread(AddressOf <your method here>)

…then you should set its IsBackground property to True if you want it to close automatically when the program closes (otherwise it keeps the program open until the thread finishes).

Then you just call Start() and you have a running background thread!

Dim myThread As New Thread(AddressOf myThreadMethod)
myThread.IsBackground = True
myThread.Start()

Accessing the UI thread

The tricky part about multithreading is to marshal calls to the UI thread. A background thread generally cannot access elements (controls) on the UI thread because that might cause concurrency issues (two threads accessing the same control at the same time). Therefore you must marshal your calls to the UI by scheduling them for execution on the UI thread itself. That way you will no longer have the risk of concurrency because all UI related code is run on the UI thread.

To marhsal calls to the UI thread you use either of the Control.Invoke() or Control.BeginInvoke() methods. BeginInvoke() is the asynchronous version, which means it doesn’t wait for the UI call to complete before it lets the background thread continue with its work.

One should also make sure to check the Control.InvokeRequired property, which tells you if you already are on the UI thread (in which case invoking is extremely unnecessary) or not.

The basic InvokeRequired/Invoke pattern looks like this (mostly for reference, keep reading below for shorter ways):

'This delegate will be used to tell Control.Invoke() which method we want to invoke on the UI thread.
Private Delegate Sub UpdateTextBoxDelegate(ByVal TargetTextBox As TextBox, ByVal Text As String)

Private Sub myThreadMethod() 'The method that our thread runs.
    'Do some background stuff...

    If Me.InvokeRequired = True Then '"Me" being the current form.
        Me.Invoke(New UpdateTextBoxDelegate(AddressOf UpdateTextBox), TextBox1, "Status update!") 'We are in a background thread, therefore we must invoke.
    Else
        UpdateTextBox(TextBox1, "Status update!") 'We are on the UI thread, no invoking required.
    End If

    'Do some more background stuff...
End Sub

'This is the method that Control.Invoke() will execute.
Private Sub UpdateTextBox(ByVal TargetTextBox As TextBox, ByVal Text As String)
    TargetTextBox.Text = Text
End Sub

New UpdateTextBoxDelegate(AddressOf UpdateTextBox) creates a new instance of the UpdateTextBoxDelegate that points to our UpdateTextBox method (the method to invoke on the UI).

However as of Visual Basic 2010 (10.0) and above you can use Lambda expressions which makes invoking much easier:

Private Sub myThreadMethod()
    'Do some background stuff...

    If Me.InvokeRequired = True Then '"Me" being the current form.
        Me.Invoke(Sub() TextBox1.Text = "Status update!") 'We are in a background thread, therefore we must invoke.
    Else
        TextBox1.Text = "Status update!" 'We are on the UI thread, no invoking required.
    End If

    'Do some more background stuff...
End Sub

Now all you have to do is type Sub() and then continue typing code like if you were in a regular method:

If Me.InvokeRequired = True Then
    Me.Invoke(Sub()
                  TextBox1.Text = "Status update!"
                  Me.Text = "Hello world!"
                  Label1.Location = New Point(128, 32)
                  ProgressBar1.Value += 1
              End Sub)
Else
    TextBox1.Text = "Status update!"
    Me.Text = "Hello world!"
    Label1.Location = New Point(128, 32)
    ProgressBar1.Value += 1
End If

And that’s how you marshal calls to the UI thread!

Making it simpler

To make it even more simple to invoke to the UI you can create an Extension method that does the invoking and InvokeRequired check for you.

Place this in a separate code file:

Imports System.Runtime.CompilerServices

Public Module Extensions
    ''' <summary>
    ''' Invokes the specified method on the calling control's thread (if necessary, otherwise on the current thread).
    ''' </summary>
    ''' <param name="Control">The control which's thread to invoke the method at.</param>
    ''' <param name="Method">The method to invoke.</param>
    ''' <param name="Parameters">The parameters to pass to the method (optional).</param>
    ''' <remarks></remarks>
    <Extension()> _
    Public Function InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object()) As Object
        If Parameters IsNot Nothing AndAlso _
            Parameters.Length = 0 Then Parameters = Nothing

        If Control.InvokeRequired = True Then
            Return Control.Invoke(Method, Parameters)
        Else
            Return Method.DynamicInvoke(Parameters)
        End If
    End Function
End Module

Now you only need to call this single method when you want to access the UI, no additional If-Then-Else required:

Private Sub myThreadMethod()
    'Do some background stuff...

    Me.InvokeIfRequired(Sub()
                            TextBox1.Text = "Status update!"
                            Me.Text = "Hello world!"
                            Label1.Location = New Point(128, 32)
                        End Sub)

    'Do some more background stuff...
End Sub

Returning objects/data from the UI with InvokeIfRequired()

With my InvokeIfRequired() extension method you can also return objects or data from the UI thread in a simple manner. For instance if you want the width of a label:

Dim LabelWidth As Integer = Me.InvokeIfRequired(Function() Label1.Width)

Example

The following code will increment a counter that tells you for how long the thread has run:

Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
    Dim CounterThread As New Thread(AddressOf CounterThreadMethod)
    CounterThread.IsBackground = True
    CounterThread.Start()

    Button1.Enabled = False 'Make the button unclickable (so that we cannot start yet another thread).
End Sub

Private Sub CounterThreadMethod()
    Dim Time As Integer = 0

    While True
        Thread.Sleep(1000) 'Wait for approximately 1000 ms (1 second).
        Time += 1

        Me.InvokeIfRequired(Sub() Label1.Text = "Thread has been running for: " & Time & " seconds.")
    End While
End Sub

Hope this helps!

Leave a Comment