Basic Instincts

Updating the UI from a Secondary Thread

Ted Pattison

Code download available at:BasicInstincts0405.exe(130 KB)

Contents

Asynchronous Execution Using Delegates
Marshaling to the UI Thread
Control.InvokeRequired

In my January 2004 column I discussed the use of delegates to execute a method asynchronously. I showed you that it's relatively easy to call BeginInvoke on a delegate object from the code behind a Windows® Form in order to dispatch an asynchronous method call. You also saw how to set up a callback method that fires automatically when the asynchronous method call completes its execution. However, I stopped short of talking about how you should update the UI to tell the user that the work is complete.

Updating user interface elements from a secondary thread in Windows Forms is tricky because a secondary thread is not allowed to read or write property values directly from a form or any of its child controls. This restriction exists because form objects and control objects in Windows Forms are not thread-safe. The only thread that's allowed to directly access a property value of a form or one of its controls is the primary UI thread. You must learn how to update the user interface without breaking this rule.

The technique you're going to learn here involves writing code to switch control to the primary UI thread from the secondary thread where it's executing—a process known as marshaling. This will allow you to update the UI safely and reliably. Once you switch control from a secondary thread to the primary UI thread, you are free to change any property values of a form and its child controls.

Asynchronous Execution Using Delegates

I am going to start by examining the code behind the Windows Form named Form1 (see Figure 1). This code demonstrates how to execute a method asynchronously. The technique involves binding a target method (GetCustomerList) to a delegate object and then calling BeginInvoke on that delegate object to start the method's asynchronous execution.

Figure 1 Asynchronous Method Execution

Class Form1 : Inherits Form '*** create delegate object to execute method asynchronously Private TargetHandler As GetCustomerListHandler _ = AddressOf DataAccessCode.GetCustomerList '*** create delegate object to service callback from CLR Private CallbackHandler As AsyncCallback _ = AddressOf MyCallbackMethod Sub cmdExecuteTask_Click(sender As Object, e As EventArgs) _ Handles cmdExecuteTask.Click '*** execute method asynchronously with callback TargetHandler.BeginInvoke("CA", CallbackHandler, Nothing) End Sub '*** CLR calls this method when asynchronous method completes '*** note this method runs on worker thread (not the UI thread) Sub MyCallbackMethod(ByVal ar As IAsyncResult) Dim retval As String() retval = TargetHandler.EndInvoke(ar) '*** add code here to trigger updating of user interface End Sub End Class Class DataAccessCode Shared Function GetCustomerList(ByVal State As String) As String() '*** call across network to DBMS or Web services to retrieve data '*** imagine; this method takes several seconds to complete End Function End Class

The technique used here also shows how to bind a callback method (specifically, MyCallbackMethod) to a second delegate object. A reference to this delegate object is then passed as the second parameter in the call to BeginInvoke:

TargetHandler.BeginInvoke("CA", CallbackHandler, Nothing)

A key benefit to using a callback method is that it eliminates the need for polling to determine when the asynchronous method has completed. When the secondary thread has finished executing the GetCustomerList method, it will then execute MyCallbackMethod before returning to the pool of worker threads maintained by the common language runtime (CLR).

It's critical for you to remember that a callback method such as MyCallbackMethod executes on a secondary thread, not on the primary UI thread. That means you should never attempt to update the UI directly from a callback method such as this. Instead, you must write code to switch the flow of execution from the secondary thread running the callback method back to the primary UI thread. I'll take you step-by-step through a design to accomplish exactly that.

Marshaling to the UI Thread

I'll begin by creating a new custom method named UpdateUI that will be called when the callback method needs to update the user interface. The calling signature of the UpdateUI method has been designed for this application-specific scenario:

'*** to be called when update to user interface required Sub UpdateUI(StatusMessage As String, Customers As String()) '*** this method will switch control over to primary UI thread End Sub

The UpdateUI method accepts a string parameter named StatusMessage, which is used to pass a status message that will be displayed to the user. A second parameter, named Customers, is based on a string array. The Customers parameter is used to pass the string array of customer names that has been returned from the asynchronous call to the GetCustomerList method. If you would like to use a similar design in your application, there is no need to have the exact same parameter list. You are free to create a method named UpdateUI with a different calling signature that accepts any list of parameters that makes sense for your application.

Next, I will rewrite the implementation of MyCallbackMethod to call the UpdateUI method. This allows the application to begin the process of updating the user interface at the completion of the asynchronous call to GetCustomerList. Examine the method implementation in Figure 2.

Figure 2 Calling UpdateUI

'*** this method fires at completion of asynchronous call Sub MyCallbackMethod(ByVal ar As IAsyncResult) Try '*** retrieve return value from async call Dim retval As String() retval = TargetHandler.EndInvoke(ar) '*** begin process to update UI UpdateUI("Task complete", retval) Catch ex As Exception Dim msg As String msg = "Error: " & ex.Message UpdateUI(msg, Nothing) End Try End Sub

As you can see, the call to UpdateUI is made just after the call to EndInvoke from within the same Try statement. Making the call to EndInvoke within a Try statement is important because it is the only reliable way to determine whether the asynchronous call completed successfully. Immediately after a successful call to EndInvoke, MyCallBackMethod calls the UpdateUI method to start the process of updating the user interface.

Now let's take a look at an implementation for the UpdateUI method. In the scenario I have just explained, MyCallbackMethod calls UpdateUI in a synchronous fashion. That means that both of these methods will be running on a secondary thread instead of on the primary UI thread. Therefore, the UpdateUI method cannot access the form or any of its controls directly. Instead, this method must be written to switch the flow of execution from a secondary thread to the primary UI thread.

When you want to switch the flow of execution from a secondary thread to the primary UI thread, you should call one of two different public methods supplied by Windows Forms controls. These two methods, Invoke and BeginInvoke, are defined by the Control class inside the System.Windows.Forms namespace. You can call Invoke or BeginInvoke on any type of Control object, including a Form object. The code in my example will call the BeginInvoke method on the Form object.

Things can be a little confusing at first when you start calling the Invoke method or the BeginInvoke method on a Form object or a Control object. That's because delegate objects also expose a pair of methods named—you guessed it—Invoke and BeginInvoke. However, the Invoke and BeginInvoke methods supplied by the Control class are very different from the Invoke and BeginInvoke methods supplied by a delegate object.

It is the act of calling Invoke or BeginInvoke on a Form object or a Control object that switches the flow of execution from a secondary thread to the primary UI thread. The difference between the two methods is that a call to Invoke is a blocking call while a call to BeginInvoke is not. In most cases it is more efficient to call BeginInvoke because the secondary thread can continue to execute without having to wait for the primary UI thread to complete its work updating the user interface. In the example in this month's column I use BeginInvoke for this reason.

Now it's time to write the implementation of the UpdateUI method using a call to BeginInvoke. Take a look at the code in Figure 3. As you can see, I have written UpdateUI to call the Form object's BeginInvoke method. At a high level, the secondary thread calls BeginInvoke in order to trigger the primary UI thread to run a method named UpdateUI_Impl. Note that the call to BeginInvoke on the Form object requires two parameters:

Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl) Dim args() As Object = {StatusMessage, Customers} Me.BeginInvoke(handler, args)

Figure 3 Calling BeginInvoke

Sub UpdateUI(StatusMessage As String, Customers As String()) '*** switch control over to primary UI thread Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl) Dim args() As Object = {StatusMessage, Customers} '*** call BeginInvoke method of Form object Me.BeginInvoke(handler, args) End Sub Delegate Sub UpdateUIHandler(StatusMessage As String, _ Customers As String()) Sub UpdateUI_Impl(StatusMessage As String, Customers As String()) '*** update user interface controls from primary UI thread Me.sbMain.Panels(0).Text = StatusMessage Me.lstCustomers.DataSource = Customers End Sub

The first parameter passed in the call to BeginInvoke is a delegate object. This parameter allows the secondary thread to pass a reference telling the primary UI thread which method to execute. Note that the delegate object in this example points to the method named UpdateUI_Impl. The second parameter passed in the call to BeginInvoke is an Object array. This is a very flexible parameter because it allows the secondary thread to pass an arbitrary number of parameters that can each be of any type. In my example, the Object array passed to BeginInvoke contains the following two elements: a string containing the status message and a string array holding the names of customers.

Here's where things get interesting. When the secondary thread calls BeginInvoke, Windows Forms automatically calls the UpdateUI_Impl method on the primary UI thread. When Windows Forms calls the UpdateUI_Impl method, it passes the two parameters that are passed in the Object array. Therefore, the implementation of UpdateUI_Impl has access to the status message and the string array of customer names. Since this method is executing on the primary UI thread, it can use these parameter values to directly update the property values of any controls on the form:

Sub UpdateUI_Impl(StatusMessage As String, Customers As String()) '*** update user interface controls from primary UI thread Me.sbMain.Panels(0).Text = StatusMessage Me.lstCustomers.DataSource = Customers End Sub

Control.InvokeRequired

At this point I have a design that updates the user interface in a safe and reliable fashion. However, there is one more technique I would like to discuss here that would make this design a little better. It would be really convenient if the UpdateUI method was written so that it can be called from any method on the form. However, the current design isn't particularly efficient if you call the UpdateUI method from code that is already running on the primary UI thread. That's because UpdateUI will create a delegate object and call BeginInvoke unnecessarily.

Let's make a modification to the UpdateUI method to check whether it's currently executing on the primary UI thread or on a secondary thread. You can perform this check by querying a public property of the Control class named InvokeRequired. This coding technique gives the UpdateUI method an opportunity to call UpdateUI_Impl synchronously on the same thread whenever it is appropriate to do so:

Sub UpdateUI(ByVal StatusMessage As String, ByVal Customers As String()) If Me.InvokeRequired Then '*** switch over to primary UI thread Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl) Dim args() As Object = {StatusMessage, Customers} Me.BeginInvoke(handler, args) Else '*** direct call - already running on primary UI thread UpdateUI_Impl(StatusMessage, Customers) End If End Sub

You have now seen all the steps required to execute a method asynchronously on a secondary thread and to update the user interface in a safe and reliable manner. The complete process is shown in Figure 4. If you want to download a sample application written in Visual Basic® .NET along with all the source code for this month's column, you can find it at the link at the top of this article.

Figure 4 Switching Control Between Threads

Public Class Form1 : Inherits Form '*** create delegate object to execute method asynchronously Private TargetHandler As GetCustomerListHandler _ = AddressOf DataAccessCode.GetCustomerList '*** create delegate object to service callback from CLR Private CallbackHandler As AsyncCallback _ = AddressOf MyCallbackMethod Sub cmdExecuteTask_Click(sender As Object, e As EventArgs) _ Handles cmdExecuteTask.Click '*** execute method asynchronously with callback UpdateUI("Starting task...", Nothing) TargetHandler.BeginInvoke("CA", CallbackHandler, Nothing) End Sub '*** callback method runs on worker thread and not the UI thread Sub MyCallbackMethod(ByVal ar As IAsyncResult) Try Dim retval As String() retval = TargetHandler.EndInvoke(ar) UpdateUI("Task complete", retval) Catch ex As Exception Dim msg As String msg = "Error: " & ex.Message UpdateUI(msg, Nothing) End Try End Sub '*** can be called from any method on form to update UI Sub UpdateUI(StatusMessage As String, Customers As String()) '*** check to see if thread switch is required If Me.InvokeRequired Then Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl) Dim args() As Object = {StatusMessage, Customers} Me.BeginInvoke(handler, args) Else UpdateUI_Impl(StatusMessage, Customers) End If End Sub '*** delegate used to switch control over to primary UI thread Delegate Sub UpdateUIHandler(StatusMessage As String, _ Customers As String()) '*** this method always runs on primary UI thread Sub UpdateUI_Impl(StatusMessage As String, Customers As String()) Me.sbMain.Panels(0).Text = StatusMessage Me.lstCustomers.DataSource = Customers End Sub End Class

Send your questions and comments for Ted to  instinct@microsoft.com.

Ted Pattison is a cofounder of Barracuda .NET, an education company that assists companies building collaborative applications using Microsoft technologies. Ted is the author of several books including Building Applications and Components with Visual Basic .NET (Addison-Wesley, 2003).