Simple Cross-thread Communication
Published: 3/18/2006
Virtually any application written today will have some functionality that is time-consuming, and invariably as a developer you will want to move this resource-intensive processing into a background thread so that your user can continue to interact with the main user interface of your application while it works. .Net makes this a quite simple with the System.Threading namespace, but the trick is knowing how to communicate between the UI process that the user is interacting with and the background process as it runs.
Because of the way that .Net handles UI components, you cannot directly update most controls from a thread other than that which the control was created on. For example, you cannot set the "Text" property of a TextBox control from your background thread without receiving the following error: "Cross-thread operation not valid: Control 'TextBox1' accessed from a thread other than the thread it was created on."
.Net also facilitates working around this limitation by providing a method whereby code can invoke a method call to a specific thread. Forms and many Controls implement the ISynchronizeInvoke interface, which contains a method called "Invoke".
The Invoke method takes two parameters (the second is optional). The first parameter is of type Delegate, which represents the code that is to be invoked to the UI thread. The second (optional) parameter is an array of objects which represent the parameters that are to be sent to the routine as it is invoked. Following is an example of a call from a threaded worker routine to the user interface thread:
VB:
Private Sub btnDoWork_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDoWork.Click
Me.m_clsBackgroundThread = New System.Threading.Thread(AddressOf DoWork)
Me.m_clsBackgroundThread.Name = "Sample Background Thread"
Me.m_clsBackgroundThread.IsBackground = True
Me.m_clsBackgroundThread.Start()
End Sub
Private Sub DoWork()
For i As Integer = 1 To 100
' sleep to simulate work...
System.Threading.Thread.Sleep(500)
' notify user...
Dim sMessage As String = String.Format("Processed {0} item{1}.", i, IIf(i = 1, "", "s"))
Dim args As Object() ] { sMessage }
Me.Invoke(New UpdateProgressDelegate(AddressOf UpdateProgress), args)
Next
End Sub
Private Delegate Sub UpdateProgressDelegate(ByVal Message As String)
Private Sub UpdateProgress(ByVal Message As String)
Me.txtProgress.Text = String.Concat(Message, System.Environment.NewLine, Me.txtProgress.Text)
Me.txtProgress.SelectionStart = 0
Me.txtProgress.ScrollToCaret()
End Sub
private void btnDoWork_Click(object sender, System.EventArgs e)
{
this.m_clsBackgroundThread = new System.Threading.Thread(new System.Threading.ThreadStart(DoWork));
this.m_clsBackgroundThread.Name = "Sample Background Thread";
this.m_clsBackgroundThread.IsBackground = true;
this.m_clsBackgroundThread.Start();
}
private void DoWork()
{
for (int i = 1; i <= 100; i++)
{
// sleep to simulate work...
System.Threading.Thread.Sleep(500);
// notify user...
string sMessage = string.Format("Processed {0} item{1}.", i, ((i == 1) ? "" : "s"));
object ] { sMessage }[] args = null;
this.Invoke(new UpdateProgressDelegate(UpdateProgress), args);
}
}
private delegate void UpdateProgressDelegate(string Message);
private void UpdateProgress(string Message)
{
this.txtProgress.Text = string.Concat(Message, System.Environment.NewLine, this.txtProgress.Text);
this.txtProgress.SelectionStart = 0;
this.txtProgress.ScrollToCaret();
}
(NOTE: The above code was written in VS2005)
In this case, Invoke is called in the form on which all of this code resides (the threading code and the user interface components). Most controls also implement ISynchonizeInvoke, so the textbox itself could have been used to invoke the call instead.
This code can be further consolidated by making the UpdateProgress routine itself "thread aware", and requiring that it invoke itself if needed. This can be done by checking the value of ISynchronizeInvoke.InvokeRequired property, which returns True if the calling code is coming from a thread other than the thread on which the control implementing the interface was created. Following is an example of how the above example can be modified to make the UpdateProgress method thread aware:
VB:
Private Sub btnDoWork_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDoWork.Click
Me.m_clsBackgroundThread = New System.Threading.Thread(AddressOf DoWork)
Me.m_clsBackgroundThread.Name = "Sample Background Thread"
Me.m_clsBackgroundThread.IsBackground = True
Me.m_clsBackgroundThread.Start()
End Sub
Private Sub DoWork()
For i As Integer = 1 To 100
' sleep to simulate work...
System.Threading.Thread.Sleep(500)
' notify user...
Me.UpdateProgress(String.Format("Processed {0} item{1}.", i, IIf(i = 1, "", "s")))
Next
End Sub
Private Delegate Sub UpdateProgressDelegate(ByVal Message As String)
Private Sub UpdateProgress(ByVal Message As String)
If Me.InvokeRequired Then
Dim args As Object() = {Message}
Me.Invoke(New UpdateProgressDelegate(AddressOf UpdateProgress), args)
Else
Me.txtProgress.Text = String.Concat(Message, System.Environment.NewLine, Me.txtProgress.Text)
Me.txtProgress.SelectionStart = 0
Me.txtProgress.ScrollToCaret()
End If
End Sub
private void btnDoWork_Click(object sender, System.EventArgs e)
{
this.m_clsBackgroundThread = new System.Threading.Thread(new System.Threading.ThreadStart(DoWork));
this.m_clsBackgroundThread.Name = "Sample Background Thread";
this.m_clsBackgroundThread.IsBackground = true;
this.m_clsBackgroundThread.Start();
}
private void DoWork()
{
for (int i = 1; i <= 100; i++)
{
// sleep to simulate work...
System.Threading.Thread.Sleep(500);
// notify user...
this.UpdateProgress(string.Format("Processed {0} item{1}.", i, ((i == 1) ? "" : "s")));
}
}
private delegate void UpdateProgressDelegate(string Message);
private void UpdateProgress(string Message)
{
if (this.InvokeRequired)
{
object[] args = {Message};
this.Invoke(new UpdateProgressDelegate(UpdateProgress), args);
}
else
{
this.txtProgress.Text = string.Concat(Message, System.Environment.NewLine, this.txtProgress.Text);
this.txtProgress.SelectionStart = 0;
this.txtProgress.ScrollToCaret();
}
}
(NOTE: The above code was written in VS 2005)
In this example, the UpdateProgress routine can be called from any thread (even the main thread) without concern, and it will invoke a call to itself to the proper thread if required.
Written by Scott Waletzko for Skystone Software.
Copyright 2005-2007 by Echosoft Design Studios, LLC, All Rights Reserved.