Here is an issue that showed up for one of our customers in the Microsoft forums for Visual Basic .NET. It is NOT the typical ”cross-threaded UI update” issue that you might think. No, this one, IMO, is slightly harder to catch since no exception is thrown to let you know something has gone wrong.
One of the many new features of Visual Basic .NET are default form instances. As the feature lists explain, a default form instance prevents you from having to “new up” an instance of a form before acting on it. So, instead of using:
Dim frm As New frmMain
frm.Show()
I can simply use:
frmMain.Show()
This is handy, particularly for the folks coming from VB6 who are used to forms that behave in this manner. However, default instances present a very interesting problem.
Take, for instance, a project that I create with a form (named frmMain) and a module (named BackgroundMethods.vb) . For the form, I have simply added a multi-line text box (named txtOutput) and a button control (named button1).
Here is the code I have in my project:
[frmMain]
Imports System.Threading
Public Class frmMain
Private Sub Button1_Click( ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
Dim t As Thread = New Thread(AddressOf GetData)
t.Start()
txtOutput.Text &= "Updates complete"
' Break after the call above to read the value
' in txtOutput.Text
End Sub
End Class
[BackgroundMethods.vb]
Imports System.Threading
Module BackgroundMethods
Public Sub GetData()
WaitForData("Message 1")
WaitForData("Message 2")
End Sub
Public Sub WaitForData(ByVal strMessage As String)
' Presumably this method would actually be
' waiting for data from a network connection,<
' serial port, or other source
Thread.Sleep(2000)
My.Forms.frmMain.txtOutput.Text &= (vbCrLf & Now().ToShortTimeString() & _
vbTab & strMessage)
' Break after the call above to read the value
' in My.Forms.frmMain.txtOutput.Text
End Sub
End Module
So the simple example is that I have a button which, when clicked, will execute the “GetData” method asynchronously in a module. That method is going to call the WaitForData method that updates the UI with two different messages (“message1” and “message2”). WaitForData is supposed to simulate a long-running process, so I threw in the typical “thread.sleep” call to make this illusion.
If you run this code, you will notice that no exceptions are thrown, but the UI for your form is also not updated. Why is this? You would have at least expected a cross-thread exception, right?
In any other managed language, this likely wouldn’t happen — namely because the “My” application is specific to VB, as well as default form instances! In C#, if I try to update the UI from another thread, I would get an exception stating: “Cross-thread operation not valid: Control ‘txtSerialIn’ accessed from a thread other than the thread it was created on.” which could be solved by using the “Invoke” method.
The issue with Visual Basic, in this instance, is that the default form instances are thread-specific. So, when I try to access the form using My.Forms.frmMain from within the worker thread, a NEW default instance is created under the covers. My calls to update the text box are then executed on the NEW instance of the form which resides in the same thread as the call to update it — hence it doesn’t throw a cross-thread exception. In the mind of the VB program, the request to update the textbox occurred without an error. When the worker thread dies, the second instance of the form (which was never displayed) is now a candidate for garbage collection. Meanwhile, back on the original thread and original form the textbox is left blank. You can validate this by running the program (I have attached sample code to this blog post) and setting breakpoints on the textbox update line in the WaitForData method, and in the last line of the button1_click event handler of the code. You will notice that after the second call to WaitForData (before the debugger exits the Sub) that My.Forms.frmMain.txtOutput.Text has been properly set to the value you expected. However, remember that this is on a second background instance of the form, not the original one you expected. Once the debugger hits the last line in the button1_click event handler, read the value of txtOutput.Text and realize THAT instance of the textbox was never updated.
So what is the solution?
First off, you still have to use the “Invoke solution” that is often times bandied about in threading discussions. To do this in VB.NET (and particularly in our solution), do the following:
1. Add the following code to the top of your BackgroundMethods.vb file:
Delegate Sub UpdateTextHandler(ByVal strMessage As String
This allows you to create a delegate that can be invoked on the UI.
2. Add the following method to your frmMain file:
Public Sub UpdateTextMethod(ByVal strMessage As String)
txtOutput.Text &= (vbCrLf & Now().ToShortTimeString() & vbTab & strMessage)
End Sub
This creates the method that will actually be executed via your delegate from the worker thread.
3. Change your WaitForData method as follows:
Public Sub WaitForData(ByVal strMessage As String)
' Presumably this method would actually be
' waiting for data from a network connection
' serial port, or other source
Thread.Sleep(2000)
Dim f As frmMain = My.Application.OpenForms("frmMain")
f.Invoke(New UpdateTextHandler(AddressOf f.UpdateTextMethod), _
New Object() {strMessage})
End Sub
This method now uses the form’s “Invoke” method (actually defined in Control) to execute UpdateTextMethod on the original form via the UpdateTextHandler delegate.
So what is happening is that your new thread is getting an instance to the existing frmMain instance by going through the OpenForms call. Once I have that instance, I can invoke a delegate that points to the “UpdateTextMethod” of the existing form ( passing in the message in the object array ). By invoking, I am able to get back on the UI’s thread and that call can execute any updates to the UI that it wishes.
Keep this in mind the next time you are not receiving errors and your UI isn’t getting updated how you would expect — particularly if you code communicates with the network, a serial device, or other device which communicates asynchronously.