Introduction
Multi-threaded applications are heavily used these days. We create multiple threads for many different purposes. A thread may be working as a TCP/IP listener, another may be querying a database periodically, yet another may be trying to connect and download some stuff from the Internet. In a typical Windows environment, we can not let the UI thread deal with all these. Because, the UI thread is so expensive that it needs to attend user actions such as responding to mouse and keyboard events rather than spend time in doing the above mentioned tasks. If we let the UI thread to handle such tasks, there is also a great chance to let the user think that the application has got 'stuck'. To avoid this, many developers use timer controls or System.Timer
objects to process something in the background. But, using multiple threads is the most preferred, or rather the professional way of handling such scenarios. Although it sounds good, there is a problem. We can not directly update any UI element from a different thread.
Problem
Let's take a very simple scenario. I want to run two threads which simply increment a variable from 0 to 9. Every time the variable is incremented, I want to update a rich text box in my form with the current value (as shown above). If we are to do this without another thread, it's simple.
The diagram above depicts the erroneous logic that fails to update the UI element from within another thread.
Solution
As briefed above in the introduction, there are several ways to handle this. The most easiest way would be to use a timer control or a System.Timer
object which are special threads that can directly update the UI element. But, we can not always apply this technique for all scenarios. For example, I can not have a timer to 'listen' to a TCP/IP port, and should implement a thread to listen continuously. Now, how do I update the UI from this thread then?
It can be done in many different ways. There are several articles in this forum that explain many different techniques to update the UI from a thread. But, I've found an easier way to do this, using the Invoke
method of the container Form
or Control
, a Delegate
object, and a method that resembles the Delegate
.
While browsing the definitions in the Object browser, the definition of the Invoke
method struck my eyes, and helped me in achieving my task without much complexities.
Public Function Invoke(ByVal method As System.Delegate) As Object
Member of: System.Windows.Forms.Control
.
Summary: Executes the specified delegate on the thread that owns the control's underlying window handle.
Parameters: method
- A delegate that contains a method to be called in the control's thread context.
Return values: The return value from the delegate being invoked, or null
if the delegate has no return value.
The two delegates defined in the CallBackThread
class:
Public Delegate Sub CallBackDelegate(ByVal status As String)
Public Delegate Sub ThreadFunctionDelegate()
The code section that calls back the UI:
Public Sub UpdateUI(ByVal msg As String)
If m_BaseControl IsNot Nothing AndAlso _
m_CallBackFunction IsNot Nothing Then
m_BaseControl.Invoke(m_CallBackFun/ction, New Object() {msg})
End If
End Sub
The thread function and the way to call back the UI:
Private Sub ThreadMethod1()
For i As Integer = 1 To 10
objThread1.UpdateUI("1st Thread Ticking " & i)
Threading.Thread.Sleep(500)
Next
End Sub
Using the Code
This project is a simple example which has a reusable class that can activate a thread and send the status as strings to the UI. It can be modified to cater any other scenario by merely changing the thread function and the callback function implementations alone. At the same time, any advanced functionality can be achieved by changing the Delegate
s and the UpdateUI
method of the class.
- Send a simple status text to the UI: No need to change the
CallBackThread
class. Change the thread function (e.g., Private Sub ThreadMethod1()
) and the callback function (e.g., Private Sub CallBackMethod1(ByVal status As String)
) to cater your needs. - Send multiple parameters back to the UI: Update the following things.
- Change the
CallBackDelegate
in the CallBackThread
class (e.g., Public Delegate Sub CallBackDelegate(ByVal msg As String, progress as Integer)
) - Change the
UpdateUI
function in the CallBackThread
class
Public Sub UpdateUI(ByVal msg As String, progress as Integer)
If m_BaseControl IsNot Nothing AndAlso _
m_CallBackFunction IsNot Nothing Then
m_BaseControl.Invoke(m_CallBackFunction, New Object() {msg, progress})
End If
End Sub
- Change the callback method implementation in the form/control to match the callback delegate change (e.g.,
Private Sub CallBackMethod1(ByVal msg As String, progrss as Integer)
)
- Call a parameterized thread function: This needs changing the following areas of code.
- The constructor (
Public Sub New(...)
) of the CallBackThread
class. - The function invoker (
Private Sub ThreadFunction(...)
) in the CallBackThread
class. - The thread function implementation (
Private Sub ThreadMethod1(...)
) in the form/control.
Please refer to the MSDN for how to call thread functions with a parameter.