Friday, October 30, 2009

Cross-Thread UI Component Calls and Invoke()



Chapter 10 -
Multithreaded Applications
Microsoft Visual Studio 2008 Programming
by Jamie Plenderleith and Steve Bunn 
McGraw-Hill/Osborne © 2009























Cross-Thread UI Component Calls and Invoke()


If you are starting a worker thread from the main UI thread after a button click or some other event, very often the worker thread will need to communicate something back to the UI. So, once you have fired off a new thread to perform some work in the background, there are two separate threads running in the application. This means that, in theory, we could do something like this:





Public Class Form1

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles MyBase.Load
Dim myWorkerThread As New Thread(AddressOf WorkerMethod)
myWorkerThread.Start()
End Sub

Private Sub WorkerMethod()
For Each drive In My.Computer.FileSystem.Drives
Me.AddDrive(drive.Name)
Next
End Sub
End Class



The preceding code will cause an exception in the WorkerMethod() method when it attempts to access the ListBox1 control. But why? Because Windows Forms controls are not thread-safe, which means that a control bound to the UI’s thread cannot be directly accessed from another thread. An attempt to do so will cause the following exception to be thrown:





InvalidOperationException (Cross-thread operation not valid: Control
'ListBox1' accessed from a thread other than the thread it was created on.)



To get around this, we need to figure out whether the control we want to talk to needs to be “invoked” and, if so, invoke it and then do the work from the same thread that the control was created on. This sounds a lot more complicated than it actually is, and once this issue has bitten you a few times, you’ll breeze through it.


To fix the preceding code, we can do something like the following:





Public Class Form1

Private Delegate Sub AddDriveCallback(ByVal Name As String)


Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles MyBase.Load
Dim myWorkerThread As New Thread(AddressOf WorkerMethod)
myWorkerThread.Start()
End Sub

Private Sub WorkerMethod()
For Each drive In My.Computer.FileSystem.Drives
Me.AddDrive(drive.Name)
Next
End Sub

Private Sub AddDrive(ByVal Name As String)
If ListBox1.InvokeRequired Then
Dim d As New AddDriveCallback(AddressOf AddDrive)
Me.Invoke(d, Name)
Else
Me.ListBox1.Items.Add(Name)
End If
End Sub
End Class



The simple part in the preceding code is that instead of adding the drive letter directly in the loop, we are calling a method to do it for us. This is also useful because we’ve encapsulated the functionality inside that method—and thank goodness for that, because the functionality has just gotten a lot more complicated! The issue that we need to overcome is that the code executing in the worker method and the Windows Forms control we want to talk to are on two different threads.


Controls have a method called Invoke() that executes a delegate on the thread that owns the control’s window handle. They also have an InvokeRequired property, which indicates whether the Invoke() method needs to be called to talk to a control. In plainer terms, if you have a single-threaded application, then all of the code to perform work and run the UI exist in the same thread. So, your code can communicate directly with your UI controls without having to worry about invoking them.


However, if you are in a multithreaded application and the UI exists in a separate thread from the worker thread, you need to do some extra work. When you want to execute some code, such as to add an item to a ListBox or change the contents of a TextBox, you need that code to execute on the same thread as the control is executing on. This is where delegates come in. If you’re not familiar with delegates, they’re simply pointers to a method, similar to how a variable is a pointer to a value or object. So, we cause the delegate to run on the UI’s thread, and thus cause the code that puts something into the ListBox to run on the UI’s thread. And because the code that’s adding something to the ListBox is running in the same thread that the ListBox was created in, there are no cross-thread issues to be concerned with.


This is a pattern that you should come to recognize and understand. It will catch you out the first few times you use it, and it’s a little strange at first, but eventually you won’t have any problems with it.


























No comments: