Advertisement:

Skystone Software

http://www.SkystoneSoftware.com

.NET Programming

Advanced Cross-thread Communication Model
Published: 10/7/2005

Overview
Adding threaded processes to your application can be an excellent way to free up your user interface while resource-intensive tasks are executed in the background. Once you set up the threaded tasks, however, you may find that notifying your user of the progress of this task is less than straight-forward.

This is because most user interface devices (controls) cannot be accessed from a thread other than the one in which they were created. This mostly has to do with the way that certain messages are sent to the control, and whether they will be able to be traced back to the calling code block. One of the most basic rules in .NET WinForms development is to avoid calling user interface components in one thread directly from code in another.

To work around this limitation, we can marshal the call to the UI components onto the thread in which they were created by using the Invoke method on one of the objects in that thread.

NOTE: Before proceeding, please be sure that you understand the concepts of using delegates to implement callbacks . C# programmers will be quite familiar with the concept, but it is one that is less common in the VB world. Using delegates to call back code in the UI thread is the first step in this process.

Designing our Application

Consider a WinForms application that consists of a single form. On this form is a button which the user can use to launch a process that is intended to run in the background so that they can otherwise interact with the program in a normal fashion. Strictly for the purposes of this example, we will write a simple function that loops through a set number of items, pretending to do some work on them by invoking Thread.Sleep() within the loop. We will put this code into a class that handles its own threading:

Public Class ProcessClass 
	Private m_clsThread As System.Threading.Thread 

	Public Sub Start() 
		m_clsThread = New System.Threading.Thread(AddressOf DoProcess) 
		m_clsThread.Name = "My Background Thread" 
		m_clsThread.IsBackground = True 
		m_clsThread.Start() 
	End Sub

	Private Sub DoProcess() 
		For i As Integer = 1 To 100 
			m_clsThread.Sleep(100) 
		Next 
	End Sub 
End Class 

This class is very simple: when "Start()" is called, the class creates a thread pointing to the "DoProcess" method. Any class wishing to implement this threaded functionality would simply create an instance of this type and call "Start()".

Please note that this code is simplified for purposes of example. In a production application, you should trap for ThreadAbortException within the threaded code, and you would want to put some code into the "Start" method that would ensure that the thread wasn't already running when it is called.

Adding Progress Indicators

As an experienced developer, however, you already know that your user will want to be continually updated as to the progress of this background process. The first step towards doing this is to set up a method for our ProcessClass to send periodic messages back to the calling code. This can be accomplished via either Events or Delegates. We will use Delegates in this example because they provide us with a method for encapsulating all of our threading code within a single class.

For our example, we will want to pass both a status message and the percent that our operation has been completed. Therefore, we can define our delegate like this:

Public Delegate Sub NotifyProgress(ByVal Message As String, ByVal PercentComplete As Integer) 

Now, if we expose an instance of this delegate on our class (perhaps as a parameter to the constructor), the calling application can pass in an instance of the delegate for the ProcessClass to use when update notifications are required. Our class might now look like this:

Public Class ProcessClass 

	Private m_clsNotifyDelegate As NotifyProgress 
	Private m_clsThread As System.Threading.Thread 
	
	Public Delegate Sub NotifyProgress(ByVal Message As String, ByVal PercentComplete As Integer) 

	Public Sub New(ByVal NotifyDelegate As NotifyProgress) 
		m_clsNotifyDelegate = NotifyDelegate 
	End Sub 

	Public Sub Start() 
		m_clsThread = New System.Threading.Thread(AddressOf DoProcess) 
		m_clsThread.Name = "My Background Thread" 
		m_clsThread.IsBackground = True 
		m_clsThread.Start() 
	End Sub 

	Private Sub DoProcess() 
		For i As Integer = 1 To 100 
			NotifyUI("Processing", i) 
			m_clsThread.Sleep(100) 
		Next 
		NotifyUI("Processing", 100) 
	End Sub 

	Private Sub NotifyUI(ByVal Message As String, ByVal Value As Integer) 
		If Not m_clsNotifyDelegate Is Nothing Then 
			m_clsNotifyDelegate(Message, Value) 
		End If 
	End Sub 	

End Class 

We have now implemented our callback to the main thread, but unfortunately, this code will error if the routine represented by our NotifyProgress delegate attempts to directly update certain user interface components (particularly a ProgressBar or TreeView). One more step is required in order to ensure that the code called by this delegate is free to update the UI in the main thread.

ISynchronizeInvoke Interface

The class System.Windows.Forms.Control implements the System.ComponentModel.ISynchronizeInvoke interface, which is designed to provide a method for executing a delegate on a specific thread. The only member of this interface we need to worry about for purposes of example is the Invoke() method, which accepts a delegate and its arguments as parameters, executing the delegate on the thread owning the object implementing the interface. That's a lot to take in all at once, but all we need to know is that the form that represents the user interface for our application also implements this interface, and therefore we can use its Invoke method to transfer execution of our NotifyProgress delegate to the main thread.

In the previous step, we required that any code creating an instance of the ProcessClass pass an instance of the NotifyProgress delegate into the constructor. We will now add a further requirement, requesting that an object implementing ISynchronizeInvoke also be passed in. Our code now looks like this:

Public Class ProcessClass 

	Private m_clsNotifyDelegate As NotifyProgress 
	Private m_clsThread As System.Threading.Thread 
	Private m_clsSynchronizingObject As System.ComponentModel.ISynchronizeInvoke 

	Public Delegate Sub NotifyProgress(ByVal Message As String, ByVal PercentComplete As Integer) 

	Public Sub New(ByVal SynchronizingObject As System.ComponentModel.ISynchronizeInvoke, ByVal NotifyDelegate As NotifyProgress) 
		m_clsSynchronizingObject = SynchronizingObject 
		m_clsNotifyDelegate = NotifyDelegate 
	End Sub 

	Public Sub Start() 
		m_clsThread = New System.Threading.Thread(AddressOf DoProcess) 
		m_clsThread.Name = "My Background Thread" 
		m_clsThread.IsBackground = True 
		m_clsThread.Start() 
	End Sub 

	Private Sub DoProcess() 
		For i As Integer = 1 To 100 
			NotifyUI("Processing", i) 
			m_clsThread.Sleep(100) 
		Next 
		NotifyUI("Processing", 100) 
	End Sub 

	Private Sub NotifyUI(ByVal Message As String, ByVal Value As Integer) 
		'this method will fail because we're not telling the delegate which thread to run in... 
		'm_clsNotifyDelegate(Message, Value) 
		'build argument list... 
		Dim args(1) As Object 
		args(0) = Message 
		args(1) = Value 
		'call the delegate, specifying the context in which to run... 
		m_clsSynchronizingObject.Invoke(m_clsNotifyDelegate, args) 
	End Sub 

End Class 

As you can see, the object passed into our constructor (presumably the main form, but it could be any component from the main thread that implements the ISynchronizeInvoke interface) is now being used to marshal the call to our progress delegate onto the main thread. The routine that is represented by the NotifyProgress delegate can now directly modify any user interface component in the main thread with impunity, not having to worry about whether it has been called from the right thread.

Using the ProcessClass

The code in our main form will look like this:

Private m_clsProcess As ProcessClass 

	Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click 
		'clear treeview... 
		tvMain.Nodes.Clear() 
		'create a new process class if we haven't already... 
		If m_clsProcess Is Nothing Then 
			m_clsProcess = New ProcessClass(Me, New ProcessClass.NotifyProgress(AddressOf DelegateProgress)) 
		End If 
		'kick off the process, execution returns immediately to the next line... 
		m_clsProcess.Start() 
	End Sub 

	'this routine was declared to handle notify messages sent via delegate... 
	Private Sub DelegateProgress(ByVal Message As String, ByVal PercentComplete As Integer) 
		'display progress in a label and progress bar - this won't error across threads... 
		lblProgress.Text = String.Concat(Message, " [DELEGATE]") 
		pbMain.Value = PercentComplete 
		'display progress in a treeview, this will raise an error if you don't properly marshal the call across threads... 
		tvMain.Nodes.Add(String.Concat(Message, " - ", PercentComplete)).EnsureVisible() 
	End Sub 
Alternate Design Options

While possibly more confusing to the uninitiated, I have chosen to encapsulate all of my threading code in this example (including the ISynchonizeInvoke calls) into a single class. I find this to be the cleanest option, enabling our class to be re-used by multiple user interface code blocks without having to implement ISynchronizeInvoke calls in each. An alternate method of implementing this functionality, however, might be to avoid the use of delegates in our ProcessClass, instead raising events could be trapped by the code creating it. While simpler, perhaps, it forces the code in the event handler to properly marshal each user interface call (using the Invoke() method on the form it is contained in), which can lead to duplication of code.

NOTE: This blog entry has been turned into a FAQ at VBCity.com.



Written by Scott Waletzko for Skystone Software.
Copyright 2005-2007 by Echosoft Design Studios, LLC, All Rights Reserved.