Tuesday, June 10, 2008

Updating the Main UI from a different thread

As you probably know, the availability of threading in .Net can lead to all kinds of little surprises, together of course with the power to write much more responsive applications.
For instance if you have a WinForms app that spawns a worker thread, and then that worker thread needs to update any UI elements on the main form, it will not work, since you can only update UI elements from the same thread they are created on.
I specifically ran into this when using a custom NUnit TestRunner (see previous post). It turns out, the TestRunner runs the test fixtures on a different thread, so you can't just do a frmMain.txtMessage.Text = "New Text from Test Fixture". Would have been too easy.

In addition, VB.Net also has the concept of default form instances, so when you do the above "frmMain.txtMessage" from a different thread than the main UI thread, VB.Net actually instantiates a new form, and then discards it when you are done. Thus there will be no exception raised for the above code, and putting in a brekpoint in the work thread shows the text properly updated. But in the UI the textbox will be empty.
It is much better explained in this post.

But the gist of it is this:

1. You have to use My.Application.OpenForms to get the correct UI thread instance of your form that you want to update

2. you have to use Invoke/BeginInvoke together with delegates to properly update the controls on the Main UI thread from a different thread.

3. The Control base class has a neat little method called InvokeRequired that will return True if the method is called from a different thread. This way you can write a generic function that works both on the UI thread and from other threads. Something like this:



Public Class frmMain

   'some magic voodo that needs to be done here to allow updating of the text box from a different thread
   Private Delegate Sub PostMessageDelegate(ByVal strNewMessage As String)

   'this is part 2 of the magic voodo: this is a public method that will be called on this form from the
   'outside, from different threads, so the text box can be updated
   Public Sub PostMessage(ByVal strMessage As String)
     If txtMessages.InvokeRequired Then
       'this method is called form a different thread, so we need to use BeginInvoke to update the UI
       txtMessages.BeginInvoke(New PostMessageDelegate(AddressOf PostMessage), New Object() {strMessage})
     Else
       txtMessages.Text &= Environment.NewLine & strMessage
     End If
   End Sub
...

End Class

No comments: