Introduction
I was just reading Will Green’s (Hotgazpacho) blogpost on using the RX extensions with winforms. And there he references Asynchronous Control Updates In C#/.NET/WinForms by Derrick Bailey. And I swear I never read about that trick (or more likely, I forgot. So I write about it here so I will not forget (yeah right).
Since we are still using threads and not the Async-Await keywords which will make these things go away we still have to write cross thread safe code in winforms.
bad code
Here is an example of code that will not work.
Imports System.Threading
Public Class Form1
Private _counter As Integer
Private Sub StartCounting()
Dim timer1 As New Threading.Timer(New TimerCallback(AddressOf UpdateTextBox), Nothing, 1, 5000)
End Sub
Private Sub UpdateTextBox(ByVal obj As Object)
_counter += 1
TextBox1.Text = _counter.ToString()
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
StartCounting()
End Sub
End Class
You will get this exception.
Good ugly code
We can fix the above by adding a bit of magic.
Imports System.Threading
Public Class Form1
Private _counter As Integer
Private Sub StartCounting()
Dim timer1 As New Threading.Timer(New TimerCallback(AddressOf UpdateTextBox), Nothing, 1, 5000)
End Sub
Private Sub UpdateTextBox(ByVal obj As Object)
If Me.InvokeRequired Then
Invoke(Sub(x) UpdateTextBox(obj), obj)
Else
_counter += 1
TextBox1.Text = _counter.ToString()
End If
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
StartCounting()
End Sub
End Class
The problem here is that you will have to repeat that a lot. And you seem to forget how to do that every time you need it. So let’s move on to something a little simpler.
The better code
Imports System.Threading
Imports System.Runtime.CompilerServices
Public Class Form1
Private _counter As Integer
Private Sub StartCounting()
Dim timer1 As New Threading.Timer(New TimerCallback(AddressOf UpdateTextBox), Nothing, 1, 5000)
End Sub
Private Sub UpdateTextBox(ByVal obj As Object)
TextBox1.DoCrossThreadCall(Sub(x)
_counter += 1
x.Text = _counter.ToString()
End Sub)
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
StartCounting()
End Sub
End Class
Public Module ControlExtensions
<Extension()>
Public Sub DoCrossThreadCall(Of TControl As Control)(ByVal control As TControl, ByVal controlAction As Action(Of TControl))
If control.InvokeRequired Then
control.Invoke(controlAction, control)
Else
controlAction(control)
End If
End Sub
End Module
I can now use that extension everywhere I need it and keep the other code cleaner. The extension method is called DoCrossThreadCall just in case you didn’t notice.
Conclusion
This extension method makes your code a whole lot cleaner. But if you have a chance you should use the Async-Await keywords in the next version of .net since that will take care of that problem all by itself.