none
"Control control name accessed from a thread other than the thread it was created on."

    Question

  • HELP!  I recently revised my code to start using "System.Timers.Timer" (more reliably accurate) rather than "System.Windows.Forms.Timer", however, now I'm having trouble updating the textboxes with the current data in the various forms which are open.  I am getting the "InvalidOperationException: Control <control name> accessed from a thread other than the thread it was created on."

    My VB program opens onto a 'startup form' which, upon "Loading" starts the "driver" form, which communicates to another device and gathers the data I want to: 1) Display on various forms' textboxes, 2) fill up arrays to save to disk.

    I have tried putting in the 'loading' sub of "driver":  "Control.CheckForIllegalCrossThreadCalls = False", but this only stops the debugger from showing the InvalidOperationException, and the values in the "driver" screen are updated, BUT ONLY on this form, none of the other forms (which used to work when I was using the System.Windows.Forms,Timer).

    I have also tried what was suggested in "Help": 'Making Thread-Safe Calls to Windows Forms Controls', checking the control's "InvokeRequired", but I couldn't get the "SetTextCallback" to instantiate or work.

    So, the question is, what's the simplest way to correct this (without using "BackgroundWorker")?

    Here's a sample of my code so far:

    Imports System.ComponentModel
    Imports System.Windows.Forms
    Imports System.Math
    Imports System.IO
    Imports System.Timers
    Imports System.Globalization
    
    Public Class driver
        Inherits System.Windows.Forms.Form
    
        Public aTimer As System.Timers.Timer
        Public aTmrInterval As Double = 500  'this is used in the Tmr_A sub to set the aTimer's interval in milliseconds
    
        Private Sub driver_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load _
    		Tmr_A()     'this is to start the aTimer running
        End Sub
    
        Private Sub Tmr_A()
    	Dim aTimer = New System.Timers.Timer
    	aTimer = New System.Timers.Timer(aTmrInterval)  
    	aTimer.Enabled = True   'allows a timed event to be created & used
    	AddHandler aTimer.Elapsed, AddressOf OnTimedEvent   'see below for this sub, which performs its actions when aTimer triggers
    	aTimer.AutoReset = True     'this makes the timer continue timing for the next cycle after the elapsed time has occured
    	GC.KeepAlive(aTimer)        'exempts this timer from garbage collection
        End Sub
    	
        Private Sub OnTimedEvent(source As Object, e As ElapsedEventArgs)
    	goGetData()	'this sub collects the data into arrays that we want to run calculations & do disk saves
    	doUpdateDisplays()	'this sub updates the various forms' textboxes with the current data we just got
    	'NOTE: this form, "driver" is started when the opening screen is "loaded", the other forms are opened via a "Menu" Screen's
    	'buttons.
        End Sub
    	
        Private Sub doUpdateDisplays()
    	tb00.Text = PLCReadReg(0).ToString	'ERROR happens right here: "InvalidOperationException. "Control control name accessed from a thread other than the thread it was created on." 
            tb01.Text = PLCReadReg(1).ToString
            tb02.Text = PLCReadReg(2).ToString
            tb03.Text = PLCReadReg(3).ToString
        End Sub
    	
    End Class
    
    


    Sometimes I get tired just smiling...

    Wednesday, October 30, 2013 6:24 PM

Answers

  • You can run the timer on its own thread and its elapsed event on the UI thread:

    Imports System.ComponentModel
    Imports System.Windows.Forms
    Imports System.Math
    Imports System.IO
    Imports System.Globalization
    Public Class driver
      
    Inherits System.Windows.Forms.Form
      
    Public WithEvents aTimer As New Timers.Timer(500)
      
    Dim TBs(3) As TextBox
      
    Protected Overrides Sub OnLoad(e As EventArgs)
        
    MyBase.OnLoad(e)
        
    For I As Integer = 0 To TBs.Length - 1
          TBs(I) = 
    New TextBox
          TBs(I).Left = 20
          TBs(I).Top = 30 * I
          TBs(I).Parent = 
    Me
        
    Next
        aTimer.SynchronizingObject = 
    Me
        aTimer.AutoReset = 
    True
        aTimer.Start()
      
    End Sub
      
    Private Sub OnTimedEvent(source As Object, e As Timers.ElapsedEventArgsHandles aTimer.Elapsed
        
    Static I As Integer
        
    For J As Integer = 0 To TBs.Length - 1
          I += J
          TBs(J).Text = 
    CStr(I)
        
    Next
      
    End Sub
    End Class


    • Marked as answer by dwyee Thursday, October 31, 2013 6:47 PM
    Wednesday, October 30, 2013 7:36 PM

All replies

  • It looks like there may be a number of issues here...

    You should probably get rid of

    Dim aTimer = New System.Timers.Timer
    

    since you already declared the aTimer variable at the class level.  You should also get rid of the GC.KeepAlive.

    You'll need to invoke a delegate to update the textbox text since the timer is running on a background thread.  You said you don't want to use a background worker but it would probably be easier for you than using the timer.

    I notice you set the interval to 500ms... there should be no reason why you cannot use a regular Timer component for an interval of that length.  It would actually be preferred since the Timer component is tuned for running on a Form.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Wednesday, October 30, 2013 6:39 PM
    Moderator
  • Hi dwyee,

    this happens cause the System.Timers.Timer doesn't call its callback on the UI-Thread, but the WindowsForms-Timer does. :-)

    You need to access the InvokeRequired-Property. As all TextBoxes are on the same Form I guess, you could do it like this:

        Private Sub doUpdateDisplays()
            If (Me.InvokeRequired) Then
                tb00.Invoke(Sub() tb00.Text = PLCReadReg(0).ToString)
                tb01.Invoke(Sub() tb01.Text = PLCReadReg(1).ToString)
                tb02.Invoke(Sub() tb02.Text = PLCReadReg(2).ToString)
                tb03.Invoke(Sub() tb03.Text = PLCReadReg(3).ToString)
            Else
                tb00.Text = PLCReadReg(0).ToString
                tb01.Text = PLCReadReg(1).ToString
                tb02.Text = PLCReadReg(2).ToString
                tb03.Text = PLCReadReg(3).ToString
            End If
    
        End Sub


    Thomas Claudius Huber

    "If you can´t make your app run faster, make it at least look & feel extremly fast"

    twitter: @thomasclaudiush
    homepage: www.thomasclaudiushuber.com
    author of: ultimate Windows Store Apps handbook | ultimate WPF handbook | ultimate Silverlight handbook

    Wednesday, October 30, 2013 6:41 PM
  • You only need to check if InvokeRequired if there is a possibility of the code executing from the main thread.  In this case the code will only execute from a secondary thread so the check isn't explicitly required; invoking the delegate is all that matters.

    It would also be far better to invoke a single method call and pass the four parameters rather than making four cross-thread calls.

    Fixing the timer declaration is really more important since it is more of a hidden issue.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Wednesday, October 30, 2013 6:53 PM
    Moderator
  • Mr. Huber:

    Thanks for your quick reply!  I've tried your suggestion...it's working on the tb00 textboxes in the "driver" form, but not on any of the other forms (I modified 2 lines to update 2 textboxes so far)...here's what works and doesn't work:

    Private Sub doUpdateDisplays()
        If (Me.InvokeRequired) Then
            tb00.Invoke(Sub() tb00.Text = PLCReadReg(0).ToString)
            tb01.Invoke(Sub() tb01.Text = PLCReadReg(1).ToString)
            tb02.Invoke(Sub() tb02.Text = PLCReadReg(2).ToString)
            tb03.Invoke(Sub() tb03.Text = PLCReadReg(3).ToString)
            tb04.Invoke(Sub() tb04.Text = PLCReadReg(4).ToString)
        Else
            tb00.Text = PLCReadReg(0).ToString
            tb01.Text = PLCReadReg(1).ToString
            tb02.Text = PLCReadReg(2).ToString
            tb03.Text = PLCReadReg(3).ToString
            tbo4.Text = PLCReadReg(4).ToString
            End If
    
    '***the above IS working, but the following is NOT!
        If Grower_Control_Monitoring.InvokeRequired Then  'this "If" never allows the following to execute!
            Grower_Control_Monitoring.TBpHValue.Invoke(Sub() Grower_Control_Monitoring.TBpHValue.Text = FormatNumber(pHValue.ToString, 2, , , ))
            Grower_Control_Monitoring.tbConductivityValue.Invoke(Sub() Grower_Control_Monitoring.tbConductivityValue.Text = FormatNumber(ConductivityValue.ToString, 0, , , ))
        End If
    
    End Sub
    The other form is called "Grower_Control_Monitoring", but I'm unable to update its textboxes text, since the "InvokeRequired" is "False".  What do I do now?


    Sometimes I get tired just smiling...

    Wednesday, October 30, 2013 7:25 PM
  • OK, Mr. Kimble.  I'll try getting rid of the "Dim aTimer..." line.  Will let you know what happens.

    I'm needing more specific info on exactly how to 'invoke a single method call and pass the four parameters rather than making four cross-thread calls.'  This sounds good, but I have no idea how to accomplish it.


    Sometimes I get tired just smiling...

    Wednesday, October 30, 2013 7:28 PM
  • Hmmm...getting rid of the 'Dim aTimer...' line had no effect.  What would you suggest I do now?

    Sometimes I get tired just smiling...

    Wednesday, October 30, 2013 7:33 PM
  • You can run the timer on its own thread and its elapsed event on the UI thread:

    Imports System.ComponentModel
    Imports System.Windows.Forms
    Imports System.Math
    Imports System.IO
    Imports System.Globalization
    Public Class driver
      
    Inherits System.Windows.Forms.Form
      
    Public WithEvents aTimer As New Timers.Timer(500)
      
    Dim TBs(3) As TextBox
      
    Protected Overrides Sub OnLoad(e As EventArgs)
        
    MyBase.OnLoad(e)
        
    For I As Integer = 0 To TBs.Length - 1
          TBs(I) = 
    New TextBox
          TBs(I).Left = 20
          TBs(I).Top = 30 * I
          TBs(I).Parent = 
    Me
        
    Next
        aTimer.SynchronizingObject = 
    Me
        aTimer.AutoReset = 
    True
        aTimer.Start()
      
    End Sub
      
    Private Sub OnTimedEvent(source As Object, e As Timers.ElapsedEventArgsHandles aTimer.Elapsed
        
    Static I As Integer
        
    For J As Integer = 0 To TBs.Length - 1
          I += J
          TBs(J).Text = 
    CStr(I)
        
    Next
      
    End Sub
    End Class


    • Marked as answer by dwyee Thursday, October 31, 2013 6:47 PM
    Wednesday, October 30, 2013 7:36 PM
  • Mr. Kimble:

    as an aside...I really really need this timer to be as periodic and repeatable as possible...the System.Windows.Timers are +/- 15ms, and are outside the periodicity that is required.


    Sometimes I get tired just smiling...

    Wednesday, October 30, 2013 8:17 PM
  • Mr. Wein:

    Thanks for your reply.  Let me see if it would help me.  I'll give it a try.

    Tell me, do I need to set up ALL of my textboxes inside that "Protected Overrides Sub"? Oh...what about the other forms' textboxes that I need to update?


    Sometimes I get tired just smiling...

    Wednesday, October 30, 2013 8:24 PM
  • "do I need to set up ALL of my textboxes inside that "Protected Overrides Sub"? Oh...what about the other forms' textboxes that I need to update?"

    I posted code that demonstratres the use of the SynchronizationContext.  I defined my controls programmatically so I could use most of your code.

    The Tiimers.Timer will trigger an elapsed event every interval.  If the SynchronizationContext is set, these triggers will queue up in the UI's message pump and run when the UI is free.  It's up to you to ensure that the UI is free to run for each trigger.

    Wednesday, October 30, 2013 8:39 PM
  • Mr. Wein:

    Sorry for sounding like a *dummy*...there's so much I need to learn!

    I'm going to give it a try, and I'll let you know how it went.  It's coming up on quitting time for me today, so it'll be tomorrow morning when I'll be able to test it.

    thanks for all your help!

    DY


    Sometimes I get tired just smiling...

    Wednesday, October 30, 2013 8:56 PM
  • Mr. Wein:

    Yes, it worked!!  Thank you for your help!!

    Here's a sample of the actual code:

    Imports System.ComponentModel
    Imports System.Windows.Forms
    Imports System.Math
    Imports System.IO
    Imports System.Timers
    Imports System.Globalization
    
    Public Class driver
        Inherits System.Windows.Forms.Form
        Dim RS232 As New RS232
        Private OpenPortOK As Integer
        Public WithEvents aTimer As New System.Timers.Timer(500)
        
        Protected Overrides Sub OnLoad(e As System.EventArgs)    'this is substituting for the original
        ' "Private Sub driver_Load..."
            MyBase.OnLoad(e)
            aTimer.SynchronizingObject = Me
            aTimer.AutoReset = True
            aTimer.Start()
    
            doOpenPort(OpenPortOK)   'this is the actual "driver"
            Timer1.Start() 'start PC Heartbeat timer
            If OpenPortOK Then
                Timer2.Start()
                Timer4.Start()
            End If
            Timer5.Start()  'temporary 1000ms replacement for Timer3-PLC heartbeat timer
        End Sub
        
        Private Sub OnTimedEvent(source As Object, e As Timers.ElapsedEventArgs) Handles aTimer.Elapsed
    		doReadPLC()
    		doUpdateDisplays()
    	End Sub
        
    End Class

    There's lots of other code controlled/called by the "OnTimedEvent" not shown, but it works!  Oh, the "Invoke" are no longer needed either!

    Thanks Again!


    Sometimes I get tired just smiling...

    Thursday, October 31, 2013 6:58 PM