Welcome to MSDN Blogs Sign in | Join | Help

Threading issue with VB.NET Default Instances

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.


		
Published Tuesday, April 11, 2006 5:40 PM by Tobin Titus
Filed under: ,

Attachment(s): DefaultInstanceProblem.zip

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# re: Threading issue with VB.NET Default Instances

This is totally lame - instead I'd just use

frmMain = ExtendedConvert(frmMain) Mwuahahahahaha
Thursday, April 20, 2006 3:34 PM by Bill

# re: Threading issue with VB.NET Default Instances

Bill, as always, your smallest contributions typically make me laugh the most.  Thanks dude! :)
Thursday, April 20, 2006 3:41 PM by Tobin Titus

# re: Threading issue with VB.NET Default Instances

Thank you, very good explanation.  I have been trying to figure this out for a while now.  What really threw me off was that the .Text was updating in the debuggin watch window.  So i spent my time trying .Refresh() etc...

-Thanks, A
Monday, May 15, 2006 10:22 AM by Aaron

# re: Threading issue with VB.NET Default Instances

Bless you, my son!  My use of invoke was causing my application to hang if the desktop was locked and unlocked, and this solved that problem.
Monday, June 19, 2006 4:47 PM by Tony Hinkle

# re: Threading issue with VB.NET Default Instances

No problem at all Tony. I'm glad it worked for you.
Monday, June 19, 2006 5:34 PM by Tobin Titus

# re: Threading issue with VB.NET Default Instances

I couldn't figure out why my textbox wasn't updating. Everything makes so much sense now!
Thank you a lot!
Tuesday, June 27, 2006 3:02 AM by marky

# re: Threading issue with VB.NET Default Instances

Glad you enjoyed it Marky. Good luck.
Tuesday, July 11, 2006 5:03 PM by Tobin Titus

# re: Threading issue with VB.NET Default Instances

Great post, i have solved my problem. Thanks a lot.

Friday, March 09, 2007 12:58 PM by Minh Thanh

# re: Threading issue with VB.NET Default Instances

Thanx for the post. I've also solved my problem. Greetz from Holland

Wednesday, June 20, 2007 5:15 AM by Duckie

# re: Threading issue with VB.NET Default Instances

I'm having a problem.  I have console app that creats a form to display its output unless you pass it a command line parameter telling it to run quietly.  The program runs fine until it tries to update the output form from an async callback.

The module that contains Sub Main() also contains:

   Delegate Sub OutputPrintCallback(ByVal OutputText As String)

   Public Sub OutputPrint(ByVal OutputText As String)

       If OutputForm IsNot Nothing Then OutputForm.OutputTextBox.Invoke(New OutputPrintCallback(AddressOf OutputForm.OutputPrint), New Object() {OutputText})

       If LogStream IsNot Nothing Then LogStream.Write(vbCrLf & Now.ToString("hh:mm:ss - ") & OutputText)

       Debug.Write(vbCrLf & Now.ToString("hh:mm:ss - ") & OutputText)

       Application.DoEvents()

   End Sub

and on the Log Form:

Public Class LogForm

   Public Sub OutputPrint(ByVal OutputText As String)

       OutputTextBox.Text &= vbCrLf & Now.ToString("hh:mm:ss - ") & OutputText

       OutputTextBox.SelectionStart = OutputTextBox.Text.Length

       OutputTextBox.ScrollToCaret()

   End Sub

End Class

This works well until an async thread tries to OutputPrint. Then everything just locks up.  

Everything is started from Sub Main, the form is just used for output.

OutputForm is an instance of LogForm created of the program's not running in quiet mode.

Any suggestions?

Should the Delegate be in the Form or the Module?

Friday, July 13, 2007 7:23 PM by Patrick

# re: Threading issue with VB.NET Default Instances

Thanks goodness someone understands this stuff.

You have saved yet another .NET weakling from a slow lingering death, racking their brains to find out why , oh WHY isn't this control updating?

Friday, August 03, 2007 6:04 PM by John

# re: Threading issue with VB.NET Default Instances

Very neat! I had the same wacky problem too. Since I only have one form I used .OpenForms(0) instead in my app.

Tuesday, August 21, 2007 6:43 AM by passbyer

# re: Threading issue with VB.NET Default Instances

Excellent.. this solved my problem with updating my form from my ManagementEventWatcher!

Thanks!

Tuesday, September 04, 2007 3:11 PM by Bas van den Berg

# re: Threading issue with VB.NET Default Instances

Thank you so much!!!

I had a similar problem but mine was obfuscated by 3 levels of events and a module.  After definitely diagnosing the problem as a separate instance of my form, I went searching for answers and eventually found your article, and realized I was calling the default instance of the form!  Again, thanks for your clear explanation of a complicated problem.

-Ross

Thursday, May 15, 2008 4:40 PM by C. Ross Eskridge

# re: Threading issue with VB.NET Default Instances

A forum friend used this page to explain a similar Problem for a text box not updating on a networking threaded solution. Basically, after delegating and invoking the text box - everything works like a charm now!

Thanks!

Sunday, October 05, 2008 7:07 PM by Alexander

# re: Threading issue with VB.NET Default Instances

Thank you Tobin! It is working perfectly!

Regards form Slovakia.

Thursday, November 20, 2008 1:30 PM by IDzOR

# re: Threading issue with VB.NET Default Instances

Thanks for your explanation. I was trying to figure that out for about 2 hours :)

Greets from Latvia :)

Wednesday, February 11, 2009 11:19 AM by Tomy

Leave a Comment

(required) 
required 
(required) 

  
Enter Code Here: Required
 
Page view tracker