Results 1 to 39 of 39

Thread: Accessing Controls from Worker Threads

  1. #1

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Accessing Controls from Worker Threads

    C# version here.

    While it is legal to access a control from threads other than the one it was created on, it is simply not a good idea. From VS 2005 onwards it is not allowed while debugging by default. Any call that accesses a control's Handle on a thread that does not own that handle will either fail or behave other than as expected. The way to safely access controls from worker threads is via delegation.

    First you test the InvokeRequired property of the control, which will tell you whether or not you can safely access the control. InvokeRequired is one of the few members of the Control class that is thread-safe, so you can access it anywhere. If the property is True then an invocation is required to access the control because the current method is executing on a thread other than the one that owns the control's Handle.

    The invocation is performed by calling the control's Invoke or BeginInvoke method. You create a delegate, which is an object that contains a reference to a method. It is good practice to make that a reference to the current method. You then pass that delegate to the Invoke or BeginInvoke method. That will essentially call the referenced method again, this time on the thread that owns the control's Handle.

    So, the first step is to identify the control member you want to access. For a first example, let's use the ResetText method of a TextBox. That's nice and simple because there are no parameters and no return value. When starting out you can build up your solution in stages. The first stage is to write a simple method that accesses your control member as desired:
    vb.net Code:
    1. Private Sub ResetTextBoxText()
    2.     Me.TextBox1.ResetText()
    3. End Sub
    Nice and easy. Ordinarily you could simply call that method and it would do what you wanted: reset the TextBox's Text property.

    Now, the next stage is to test the control's InvokeRequired property and place the existing method body in the Else block:
    vb.net Code:
    1. Private Sub ResetTextBoxText()
    2.     If Me.TextBox1.InvokeRequired Then
    3.  
    4.     Else
    5.         Me.TextBox1.ResetText()
    6.     End If
    7. End Sub
    That now says that if an invocation is NOT required we can access the control member directly.

    The next stage is to create the delegate and use it to invoke the current method on the appropriate thread. As I said, our method has no parameters and no return value so it's nice and easy. In that case you can create an instance of the existing MethodInvoker delegate:
    vb.net Code:
    1. Private Sub ResetTextBoxText()
    2.     If Me.TextBox1.InvokeRequired Then
    3.         Me.TextBox1.Invoke(New MethodInvoker(AddressOf ResetTextBoxText))
    4.     Else
    5.         Me.TextBox1.ResetText()
    6.     End If
    7. End Sub
    If an invocation is required, this creates a MethodInvoker delegate and passes it a reference to the current method. That delegate is then passed to the control's Invoke method, which carries it across the thread boundary to the thread that owns the control's Handle and invokes it. To invoke a delegate means to execute the method it has a reference to.

    So now you can simply call that method from any thread you like and rest assured that if you're not on the correct thread that the method itself will handle it for you. To see this in action, try creating a new project and add a Button and a TextBox to the form along with a BackgroundWorker. Now add the following code:
    Code:
    Private Sub Form1_Load(ByVal sender As System.Object, _
                           ByVal e As System.EventArgs) Handles MyBase.Load
        Threading.Thread.CurrentThread.Name = "UI Thread"
    End Sub
    
    Private Sub Button1_Click(ByVal sender As System.Object, _
                              ByVal e As System.EventArgs) Handles Button1.Click
        Me.ResetTextBoxText()
    
        Me.BackgroundWorker1.RunWorkerAsync()
    End Sub
    
    Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _
                                         ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        Threading.Thread.CurrentThread.Name = "Worker Thread"
    
        Me.ResetTextBoxText()
    End Sub
    
    Private Sub ResetTextBoxText()
        Debug.WriteLine(Threading.Thread.CurrentThread.Name, "ResetTextBoxText")
    
        If Me.TextBox1.InvokeRequired Then
            Me.TextBox1.Invoke(New MethodInvoker(AddressOf ResetTextBoxText))
        Else
            Me.TextBox1.ResetText()
        End If
    End Sub
    Now run the program, click the Button and watch the Output window. You'll see that the ResetTextBoxText method is executed three times. The first time is in the UI thread when it's called from the Button's Click event handler. The second time is in the worker thread when it's called from the DoWork event handler. The third time is in the UI thread again, when it's invoked via the delegate.

    EDIT: I should also point out the difference between Invoke and BeginInvoke. Invoke is a synchronous method. That means that when you call it the background thread will block until the method invoked on the UI thread completes. BeginInvoke is asynchronous, so it will return immediately and the background thread will continue on its way while the method invoked on the UI thread executes.

  2. #2

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Passing Parameters via Delegation

    To invoke a method via a delegate, the delegate must have the same signature as the method. The example in the first post invokes a method that has no parameters and no return value. In that case you can use the existing MethodInvoker delegate. If you need to specify one or more parameters or get a return value though, you will have to declare your own delegate type.

    Let's say that you want to set the Text property of a TextBox from a worker thread. From the first post we know the first step is to declare a method that does what we want:
    vb.net Code:
    1. Private Sub SetTextBoxText(ByVal text As String)
    2.     Me.TextBox1.Text = text
    3. End Sub
    Now, that method has a parameter so we cannot use the MethodInvoker delegate. We must declare our own delegate type with the same signature. The signature is basically the parameter list and the return type. To create your delegate declaration you should copy the method declaration:
    vb.net Code:
    1. Private Sub SetTextBoxText(ByVal text As String)
    then add the 'Delegate' key word:
    vb.net Code:
    1. Private Delegate Sub SetTextBoxText(ByVal text As String)
    and finally change the name:
    vb.net Code:
    1. Private Delegate Sub SetTextBoxTextInvoker(ByVal text As String)
    Now that we have declared our new delegate type we can create instances of it just as we did with the MethodInvoker.

    The next step from above was to add a test for the InvokeRequired property of our control:
    vb.net Code:
    1. Private Sub SetTextBoxText(ByVal text As String)
    2.     If Me.TextBox1.InvokeRequired Then
    3.  
    4.     Else
    5.         Me.TextBox1.Text = text
    6.     End If
    7. End Sub
    Finally we call the control's Invoke method and pass our delegate:
    vb.net Code:
    1. Private Sub SetTextBoxText(ByVal text As String)
    2.     If Me.TextBox1.InvokeRequired Then
    3.         Me.TextBox1.Invoke(New SetTextBoxTextInvoker(AddressOf SetTextBoxText), _
    4.                            text)
    5.     Else
    6.         Me.TextBox1.Text = text
    7.     End If
    8. End Sub
    Notice that, while last time the only parameter passed to the Invoke method was the delegate, this time we pass the 'text' value as well. Any parameters passed to Invoke after the delegate will then be passed again to the method that gets invoked by the delegate. Our delegate will be invoking the SetTextBoxText method, which requires a 'text' parameter. That code gets the 'text' value passed in to the first call and propagates it to the second call via the Invoke method and the delegate.

    The above example passes a single parameter but the very same mechanism can be used to pass multiple parameters. Here's a similar example that can be used to set the Text property of any control:
    vb.net Code:
    1. Private Delegate Sub SetControlTextInvoker(ByVal ctl As Control, ByVal text As String)
    2.  
    3. Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
    4.     If ctl.InvokeRequired Then
    5.         ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), _
    6.                    ctl, _
    7.                    text)
    8.     Else
    9.         ctl.Text = text
    10.     End If
    11. End Sub
    Pass in the control and the text and the method handles the rest. It tests the InvokeRequired property and calls the Invoke method of the appropriate control, then sets the Text property of that same control.


    EDIT: A note for those using .NET 1.x. The Control.Invoke method signature changed in .NET 2.0 to accept any number of individual parameters. Previous versions required any parameters to be passed within an array. That means that while this is fine in .NET 2.0 and above:
    vb.net Code:
    1. ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), _
    2.            ctl, _
    3.            text)
    you would have to do this in .NET 1.x:
    vb.net Code:
    1. ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), _
    2.            New Object() {ctl, _
    3.                          text})

  3. #3

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Getting Return Values

    Getting a return value is no more difficult. One thing I haven't mentioned is that the Invoke method is a function. If the method invoked by the delegate returns a value then that is propagated by the Invoke method. Let's use an example of getting the Text property of a TextBox. Following the steps laid out already we first create a method to do the job:
    vb.net Code:
    1. Private Function GetTextBoxText() As String
    2.     Return Me.TextBox1.Text
    3. End Function
    Next we declare a matching delegate:
    vb.net Code:
    1. Private Delegate Function GetTextBoxTextInvoker() As String
    2.  
    3. Private Function GetTextBoxText() As String
    4.     Return Me.TextBox1.Text
    5. End Function
    After that we test the InvokeRequired property:
    vb.net Code:
    1. Private Delegate Function GetTextBoxTextInvoker() As String
    2.  
    3. Private Function GetTextBoxText() As String
    4.     If Me.TextBox1.InvokeRequired Then
    5.  
    6.     Else
    7.         Return Me.TextBox1.Text
    8.     End If
    9. End Function
    Finally we call the Invoke method and pass an instance of our delegate:
    vb.net Code:
    1. Private Delegate Function GetTextBoxTextInvoker() As String
    2.  
    3. Private Function GetTextBoxText() As String
    4.     If Me.TextBox1.InvokeRequired Then
    5.         Return CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
    6.     Else
    7.         Return Me.TextBox1.Text
    8.     End If
    9. End Function
    Let's clean that up a little so we only have one Return statement, which is widely considered to be best practice:
    vb.net Code:
    1. Private Delegate Function GetTextBoxTextInvoker() As String
    2.  
    3. Private Function GetTextBoxText() As String
    4.     Dim text As String
    5.  
    6.     If Me.TextBox1.InvokeRequired Then
    7.         text = CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
    8.     Else
    9.         text = Me.TextBox1.Text
    10.     End If
    11.  
    12.     Return text
    13. End Function
    Notice that in this case we are actually returning the result of the Invoke method, which is the same value as was returned by the method that was invoked. This is how you get a value back onto your worker thread from the UI thread.

    Notice also that the return value from the invoke method must be cast as the appropriate type. Invoke can be used to invoke any method at all, so it could return any value at all. That means that its actual return type is Object. You must therefore cast each returned object as its actual type.

    Now to show that this method can be generalised to any control too, as well as combine the passing of parameters and returning a value, here's an extended example based on what we've already seen:
    vb.net Code:
    1. Private Delegate Function GetControlTextInvoker(ByVal ctl As Control) As String
    2.  
    3. Private Function GetControlText(ByVal ctl As Control) As String
    4.     Dim text As String
    5.  
    6.     If ctl.InvokeRequired Then
    7.         text = CStr(ctl.Invoke(New GetControlTextInvoker(AddressOf GetControlText), _
    8.                                ctl))
    9.     Else
    10.         text = ctl.Text
    11.     End If
    12.  
    13.     Return text
    14. End Function

  4. #4
    PowerPoster JuggaloBrotha's Avatar
    Join Date
    Sep 2005
    Location
    Lansing, MI; USA
    Posts
    4,286

    Re: Accessing Controls from Worker Threads

    This is a far better explanation than anything else I've seen

    Thanks JM
    Currently using VS 2015 Enterprise on Win10 Enterprise x64.

    CodeBank: All Threads • Colors ComboBox • Fading & Gradient Form • MoveItemListBox/MoveItemListView • MultilineListBox • MenuButton • ToolStripCheckBox • Start with Windows

  5. #5
    Fanatic Member
    Join Date
    Sep 2005
    Location
    Toledo, OH
    Posts
    785

    Re: Accessing Controls from Worker Threads

    JM is, so far, the most detailed individual who offers help to those in need of something... so he's not only giving you the answer to your problem, but is in fact giving you the explanation of why it happened and what not..

    thank you JM

    note: all you other individuals who help, you all do a great job, keep it up.

  6. #6
    Addicted Member
    Join Date
    Mar 2008
    Posts
    150

    Re: Accessing Controls from Worker Threads

    Are there any tricks in getting the Debug output?

    I've got the output window open, tried enabling the other options than just 'Program Output' (which seems to be all i really need anyway) and 'Exception Messages'. I also tried changing the code from Debug.WriteLine to Debug.Write etc but didn't make any difference. *Sigh!*

    Any tips?

  7. #7
    Pro Grammar chris128's Avatar
    Join Date
    Jun 2007
    Location
    England
    Posts
    7,604

    Re: Accessing Controls from Worker Threads

    I've always wondered how you were supposed to do this 'properly' ... now I know Thanks jmc

    EDIT: to the person who posted above me - has that actually got anything at all to do with this thread? You would almost certainly be better off posting in the VB.NET forum.
    My free .NET Windows API library (Version 2.2 Released 12/06/2011)

    Blog: cjwdev.wordpress.com
    Web: www.cjwdev.co.uk


  8. #8
    Addicted Member
    Join Date
    Mar 2008
    Posts
    150

    Re: Accessing Controls from Worker Threads

    If you know that an invoke will always be required in accessing your control from a worker thread, suggesting that the function you're calling is only used by your worker thread, is it a 'safe option' to call the delegate directly without going via the 'real' function?

    I'm not sure if that makes sense, so using the following code as an example:

    vb.net Code:
    1. Private Delegate Function GetTextBoxTextInvoker() As String
    2.  
    3.     Private Function GetTextBoxText() As String
    4.         If Me.TextBox1.InvokeRequired Then
    5.             Return CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
    6.         Else
    7.             Return Me.TextBox1.Text
    8.         End If
    9.     End Function

    Rather than calling GetTextBoxText() from your worker thread, and it invoking itself via the delegate function GetTextBoxTextInvoker()...can you safely call GetTextBoxTextInvoker() from your worker thread instead and 'cut out the middle man'?

    To me it saves a few lines of code being executed for no apparent reason, and I say 'no apparent reason' because I'm not sure if there's another reason to do so...and this is the basis of my question.

    I'm happy to be wrong, and I've tested this, and it works, but I'm just wondering if there's a 'gotcha' in here somewhere.

  9. #9

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by scootabug
    If you know that an invoke will always be required in accessing your control from a worker thread, suggesting that the function you're calling is only used by your worker thread, is it a 'safe option' to call the delegate directly without going via the 'real' function?

    I'm not sure if that makes sense, so using the following code as an example:

    vb.net Code:
    1. Private Delegate Function GetTextBoxTextInvoker() As String
    2.  
    3.     Private Function GetTextBoxText() As String
    4.         If Me.TextBox1.InvokeRequired Then
    5.             Return CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
    6.         Else
    7.             Return Me.TextBox1.Text
    8.         End If
    9.     End Function

    Rather than calling GetTextBoxText() from your worker thread, and it invoking itself via the delegate function GetTextBoxTextInvoker()...can you safely call GetTextBoxTextInvoker() from your worker thread instead and 'cut out the middle man'?

    To me it saves a few lines of code being executed for no apparent reason, and I say 'no apparent reason' because I'm not sure if there's another reason to do so...and this is the basis of my question.

    I'm happy to be wrong, and I've tested this, and it works, but I'm just wondering if there's a 'gotcha' in here somewhere.
    Yes, you certainly can do that. For instance, if you wanted to append text to a TextBox then you could forgo the "middleman" and just create a delegate to invoke the AppendText method of the TextBox directly. If you are accessing a property rather than a method, or you're accessing multiple members, then you're still going to need to write a method that you can invoke on the UI thread though. I prefer to stick to the pattern I've demonstrated for clarity and consistency. If you always do this the same way and all the threading-specific code is always in the one method then it's always clear exactly what you're doing.

  10. #10
    Addicted Member
    Join Date
    Mar 2008
    Posts
    150

    Re: Accessing Controls from Worker Threads

    Great, thanks!

  11. #11
    Fanatic Member
    Join Date
    Feb 2006
    Posts
    607

    Re: Accessing Controls from Worker Threads

    scoota, I dont seem to understand what your talking about. Mind to give an example where you cut out the 'middle' man?

    Learning purposes only! Thanks

  12. #12

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by masfenix
    scoota, I dont seem to understand what your talking about. Mind to give an example where you cut out the 'middle' man?

    Learning purposes only! Thanks
    If you want to append text to a TextBox then, by my pattern, you'd do this:
    vb.net Code:
    1. Private Delegate Sub AppendTextToTextBoxInvoker(ByVal text As String)
    2.  
    3. Private Sub AppendTextToTextBox(ByVal text As String)
    4.     If Me.TextBox1.InvokeRequired Then
    5.         Me.TextBox1.Invoke(New AppendTextToTextBoxInvoker(AddressOf AppendTextToTextBox), text)
    6.     Else
    7.         Me.TextBox1.AppendText(text)
    8.     End If
    9. End Sub
    and then somewhere in your background thread you'd do this:
    vb.net Code:
    1. Me.AppendTextToTextBox(someString)
    By cutting out the middle man we mean removing the need for the AppendTextToTextBox method. Instead of that last line calling AppendTextToTextBox and it invoking itself, you can just invoke the TextBox's AppendText method directly:
    vb.net Code:
    1. Me.TextBox1.Invoke(New AppendTextToTextBoxInvoker(AddressOf Me.TextBox1.AppendText), someString)
    I like the pattern I've described because, hopefully, it makes it clear to those who might not understand the process exactly what's happening. As you get more comfortable with multi-threading then it becomes easier to take "shortcuts". As is the case in so many situations, it's best to lay down and learn the rules first, then learn where and when it's OK to break the rules later.

  13. #13
    Fanatic Member
    Join Date
    Feb 2006
    Posts
    607

    Re: Accessing Controls from Worker Threads

    Thankyou! I cant even imagine doing this on something like C/C++. Hehe you'd have to code like 50 lines just to acomplish a single task.

  14. #14
    Junior Member
    Join Date
    Oct 2008
    Posts
    26

    Re: Accessing Controls from Worker Threads

    This doesn't work for LIstboxes or Comboxes because it doesn't pass the selected index. How can I accomplish that?

  15. #15

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by sebex
    This doesn't work for LIstboxes or Comboxes because it doesn't pass the selected index. How can I accomplish that?
    Um, yes it does. The SelectedIndex is just a number and I've shown you how to pass data in both directions using this technique. Post #2 shows you how to pass data from the background thread to the UI thread. Post #3 shows you how to return data from the UI thread to the background thread. Think about it. Post #3 uses a method named GetTextBoxText to get the Text of a TextBox. Does that really sound all that different to getting the SelectedIndex of a ListBox or ComboBox? You use the exact same pattern and just change the detail. A logical name for the method would be GetListControlSelectedIndex. That should make it obvious what needs changing.

    Note that ListControl is the base class of both the ListBox and the ComboBox from which they both inherit the SelectedIndex property, so you can create a single method that will work for both, or you can write specific methods if you prefer.

  16. #16

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Here's the last example from post #1 redone for WPF:
    vb.net Code:
    1. Private WithEvents worker As New BackgroundWorker
    2.  
    3. Private Sub Window1_Loaded(ByVal sender As Object, _
    4.                            ByVal e As RoutedEventArgs) Handles MyBase.Loaded
    5.     Thread.CurrentThread.Name = "UI Thread"
    6. End Sub
    7.  
    8. Private Sub Button1_Click(ByVal sender As Object, _
    9.                           ByVal e As RoutedEventArgs) Handles Button1.Click
    10.     Me.ResetTextBoxText()
    11.  
    12.     Me.worker.RunWorkerAsync()
    13. End Sub
    14.  
    15. Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _
    16.                                      ByVal e As DoWorkEventArgs) Handles worker.DoWork
    17.     Thread.CurrentThread.Name = "Worker Thread"
    18.  
    19.     Me.ResetTextBoxText()
    20. End Sub
    21.  
    22. Private Sub ResetTextBoxText()
    23.     Debug.WriteLine(Thread.CurrentThread.Name, "ResetTextBoxText")
    24.  
    25.     If Me.TextBox1.Dispatcher.CheckAccess() Then
    26.         Me.TextBox1.Clear()
    27.     Else
    28.         Me.TextBox1.Dispatcher.Invoke(New Action(AddressOf ResetTextBoxText))
    29.     End If
    30. End Sub

  17. #17

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Here's the last example from post #1 reworked to use a SynchronizationContext:
    vb.net Code:
    1. Private context As Threading.SynchronizationContext = Threading.SynchronizationContext.Current
    2.  
    3. Private Sub Form1_Load(ByVal sender As System.Object, _
    4.                        ByVal e As System.EventArgs) Handles MyBase.Load
    5.     Threading.Thread.CurrentThread.Name = "UI Thread"
    6. End Sub
    7.  
    8. Private Sub Button1_Click(ByVal sender As System.Object, _
    9.                           ByVal e As System.EventArgs) Handles Button1.Click
    10.     Me.context.Send(AddressOf Me.ResetTextBoxText, Nothing)
    11.  
    12.     Me.BackgroundWorker1.RunWorkerAsync()
    13. End Sub
    14.  
    15. Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _
    16.                                      ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    17.     Threading.Thread.CurrentThread.Name = "Worker Thread"
    18.  
    19.     Me.context.Send(AddressOf Me.ResetTextBoxText, Nothing)
    20. End Sub
    21.  
    22. Private Sub ResetTextBoxText(ByVal userState As Object)
    23.     Debug.WriteLine(Threading.Thread.CurrentThread.Name, "ResetTextBoxText")
    24.  
    25.     Me.TextBox1.ResetText()
    26. End Sub
    Notice that, in this case, there's no need to check which thread you're on. If in doubt, you simply tell the SynchronizationContext to Send the method call to the appropriate thread, no matter what thread you're currently on. Note that Send is to Invoke as Post is to BeginInvoke, i.e. one is synchronous and one is asynchronous.

  18. #18
    Wait... what? weirddemon's Avatar
    Join Date
    Jan 2009
    Location
    USA
    Posts
    3,828

    Re: Accessing Controls from Worker Threads

    JMC, in regards to post #17, if you use that example as is, it isn't multi-threaded. Not from what I can tell. When it tries to reset the TextBox, I quickly try to press a different blank button and I can't because the UI thread is busy.

    I launched a process and called waitforexit in the ResetTextBoxTest method to be definitive, and the UI was still not responsive.

    Am I missing something?
    CodeBank contributions: Process Manager, Temp File Cleaner

    Quote Originally Posted by SJWhiteley
    "game trainer" is the same as calling the act of robbing a bank "wealth redistribution"....

  19. #19

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by weirddemon View Post
    JMC, in regards to post #17, if you use that example as is, it isn't multi-threaded. Not from what I can tell. When it tries to reset the TextBox, I quickly try to press a different blank button and I can't because the UI thread is busy.

    I launched a process and called waitforexit in the ResetTextBoxTest method to be definitive, and the UI was still not responsive.

    Am I missing something?
    ResetTextBoxText is executed on the UI thread. It must be, because it's accessing a control directly. The whole purpose of this thread is how to deal with the fact that you can't access controls directly on anything but the UI thread.

    In that example, it's the DoWork event handler of the BackgroundWorker that is executed on the background thread. That's the whole point of the BackgroundWorker: you call RunWorkerAsync and the DoWork event is raised on a background thread, where you do the work. It's the Send method of the SynchronizationContext that marshals the method call to the UI thread, instead of calling Invoke.

    The BackgroundWorker also exists so that you don't have to worry about doing things like using a SynchronizationContext or ISynchronizeInvoke, but I just used it for simplicity in this example. Normally, if you need to update the UI when using a BackgroundWorker, you would use the ReportProgress method and ProgressChanged event and/or the RunWorkerCompleted event. I have another CodeBank thread that deals with that subject though.

  20. #20
    Lively Member
    Join Date
    Jan 2007
    Posts
    79

    Re: Accessing Controls from Worker Threads

    Thank You Jim.

    This/These examples are really great and explained nicely.
    But what bothers me that VB doesn't have a delegate to do a lot controls in one go.
    If you have/collect a lot of data from an bgworker, you have to split it all up to do each and every control in single steps.
    Forces you to rethink everything.

    Kind Regards, Starf0x

  21. #21

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by Starf0x View Post
    Thank You Jim.

    This/These examples are really great and explained nicely.
    But what bothers me that VB doesn't have a delegate to do a lot controls in one go.
    If you have/collect a lot of data from an bgworker, you have to split it all up to do each and every control in single steps.
    Forces you to rethink everything.

    Kind Regards, Starf0x
    You are looking at this in completely the wrong way. Delegates have nothing specific to do with controls. A delegate is simply a reference to a method. That's it, that's all. In these examples, I'm using the Invoke method of a control to marshal a delegate onto the UI thread. Once you are on the UI thread, you you can do anything you want. If you want to update a thousand controls then update a thousand controls. The delegate has nothing whatsoever to do with that.
    vb.net Code:
    1. Private Sub EnableEveryControl()
    2.     If Me.InvokeRequired Then
    3.         Me.Invoke(New MethodInvoker(AddressOf EnableEveryControl))
    4.     Else
    5.         For Each ctl In Me.Controls
    6.             ctl.Enabled = True
    7.         Next
    8.     End If
    9. End Sub
    As you can see, this code uses the very same MethodInvoker delegate as I used in previous examples, yet the code is able to affect every control on the form, whether that is one or one million. The delegate has NOTHING to do with the controls. It is merely a means to invoke a method on the UI thread. What you do in that method is COMPLETELY up to you, as it always is.

  22. #22
    Frenzied Member stateofidleness's Avatar
    Join Date
    Jan 2009
    Posts
    1,780

    Re: Accessing Controls from Worker Threads

    Wow JMC, I know i've posted a lot of "threading" questions recently, and this thread actually makes is so much clearer. Thanks for taking the time to write it up with such clarity!

  23. #23
    Fanatic Member
    Join Date
    Oct 2007
    Posts
    544

    Re: Accessing Controls from Worker Threads

    Also because the invoke happens on Main UI thread anyway, we don't need to do synclock right?>

  24. #24

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by teguh123 View Post
    Also because the invoke happens on Main UI thread anyway, we don't need to do synclock right?>
    When using my pattern, whatever you execute inside the Else block is executed on the UI thread. As such, only one instance of that code can be executed at a time. That means that you don't need to synchronise that specific block of code to protect it from itself, but you may still have to protect against undesirable interactions with other blocks of code that may be executed on other threads.

  25. #25
    PowerPoster
    Join Date
    Apr 2007
    Location
    The Netherlands
    Posts
    5,070

    Re: Accessing Controls from Worker Threads

    Just wanted to mention I made an extension method that you can use to get and set properties from any control via this same method (except you don't have to type the delegate and invocation each time).
    It can be found here:
    http://www.vbforums.com/showthread.php?t=646484

  26. #26
    Junior Member rkinci's Avatar
    Join Date
    Jan 2012
    Location
    Maine
    Posts
    19

    Re: Accessing Controls from Worker Threads

    Very helpful. Thanks.

  27. #27
    Hyperactive Member
    Join Date
    Jan 2007
    Posts
    448

    Re: Accessing Controls from Worker Threads

    This is the best and simplest explanation I have seen for threading. There were a couple of close "thirds" out there..Not quite "Seconds".
    Thanks jmc!

  28. #28
    Registered User
    Join Date
    Jun 2014
    Posts
    1

    Re: Accessing Controls from Worker Threads

    Thank you very much! It Helps me alot!

  29. #29
    Hyperactive Member
    Join Date
    Nov 2014
    Posts
    428

    Re: Accessing Controls from Worker Threads

    If I want to update multiple controls from the single thread using more than one control updating commands and then if not all the commands require invocation then how will I write the commands? For Example :

    Code:
    Private Sub ResetTextBoxText()
    If Me.TextBox1.InvokeRequired Then
    Me.TextBox1.Invoke(New MethodInvoker(AddressOf ResetTextBoxText))
    Else
    Me.TextBox1.ResetText() ' Suppose this command need invocation.
    Me.TextBox1.ResetText() ' Suppose this command doesn't need invocation.
    Me.TextBox1.ResetText() ' Suppose this command need invocation.
    Me.TextBox1.ResetText() ' Suppose this command doesn't need invocation.
    Me.TextBox1.ResetText() ' Suppose this command need invocation.
    End If
    End Sub
    If I write code like that then will it work properly? Or all the codes which don't need invocation will also be invoked? What type of code we can use for the above circumstances?

  30. #30
    Sinecure devotee
    Join Date
    Aug 2013
    Location
    Southern Tier NY
    Posts
    6,582

    Re: Accessing Controls from Worker Threads

    If you have further questions, you should probably start another thread and reference this one.
    You don't seem to understand what the code is doing.
    The invokeRequired is testing one of the controls, and it doesn't really matter which one you use to do the test since they were all created by the GUI thread (unless you've done something specific to bypass the normal control creation).

    So, since the control needs invocation, you are invoking this Sub (ResetTextBoxText) through a delegate, which will re-run this sub on the GUI thread shortly.
    You then exit the sub.
    The next time the sub is called will probably be from the Invocation of the delegate for this sub earlier.

    This time, InvokeRequired will not be true (you are running on the GUI thread), so you do the Else case, and you can access any of the controls on the form because you are running the code in the GUI thread.
    Since you are on the GUI thread, none of the controls will need invocation.

  31. #31

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by mnxford View Post
    If I want to update multiple controls from the single thread using more than one control updating commands and then if not all the commands require invocation then how will I write the commands? For Example :

    Code:
    Private Sub ResetTextBoxText()
    If Me.TextBox1.InvokeRequired Then
    Me.TextBox1.Invoke(New MethodInvoker(AddressOf ResetTextBoxText))
    Else
    Me.TextBox1.ResetText() ' Suppose this command need invocation.
    Me.TextBox1.ResetText() ' Suppose this command doesn't need invocation.
    Me.TextBox1.ResetText() ' Suppose this command need invocation.
    Me.TextBox1.ResetText() ' Suppose this command doesn't need invocation.
    Me.TextBox1.ResetText() ' Suppose this command need invocation.
    End If
    End Sub
    If I write code like that then will it work properly? Or all the codes which don't need invocation will also be invoked? What type of code we can use for the above circumstances?
    The whole point is that the only actions you take in that method would be those that must be done on the UI thread. If there's something else to do that doesn't need doing on the UI thread then don't put it in that method in the first place.

  32. #32
    New Member
    Join Date
    Nov 2015
    Posts
    2

    Re: Getting Return Values

    Hi John et al,

    Firstly, a big thanks to you John for taking the time to create this piece and enlighten us.

    I wonder if you could help me further.

    I've written an application which does some fairly heavy back-end SQL work. The SQL queries can take anything from a second up to minutes depending on what's be requested by the user.

    Whilst a query is running I'd like to provide the user with some feedback so they know the application is busy.

    So, what I've done so far is as follows.

    1. On the main form the user selects the criteria they require which will be collated to build the SQL query. There are hundreds of selectable items so it can get quite complicated but I've got all the item collection and SQL building working. That isn't the problem.

    2. When they user has selected what they need, they click a button on the main form which opens a new form. The new form (call it the 'count' form) will display an number depicting the number of records that the criteria matches. There are a couple of options on the 'count' form which allow some refinement to be made to the query which will rerun the query is changed by the user.

    The issue I have is that when the query is run I can of course display a message in say a status bar saying it's 'processing...' but I wanted to display an animation like that of a web page when it's doing something. So I've got my animation gif in my resources and this is what I've coded so far (please note I've used code from various places so I make no claims this is my own work).

    Code:
    Public Class Export
        Dim bw As BackgroundWorker = New BackgroundWorker
        Public Delegate Sub PictureVisibilityDelegate(ByVal visibility As Boolean)
        Dim ChangePictureVisibility As PictureVisibilityDelegate
    
           Private Sub Export_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            AddHandler bw.DoWork, AddressOf bw_DoWork
            AddHandler bw.RunWorkerCompleted, AddressOf bw_RunWorkerCompleted
            ChangePictureVisibility = AddressOf ChangeVisibility
            dtPostcodes.Clear()
            If Not bw.IsBusy = True Then
                If Me.IsHandleCreated Then
                    bw.RunWorkerAsync()
                End If
            End If
        End Sub
    
        Public Sub ChangeVisibility(ByVal visibility As Boolean)
            PictureBox1.Visible = visibility
        End Sub
    
        Private Sub bw_DoWork(sender As Object, e As DoWorkEventArgs)
            Me.Invoke(ChangePictureVisibility, True)
            getCount()
        End Sub
    
        Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
            Me.Invoke(ChangePictureVisibility, False)
        End Sub
    
        Private Sub getCount()
            Dim selectsql As String = String.Empty
            selectsql = buildSQL(True)
            Dim dbcontext As String = String.Empty
            dbcontext = SQLClass.setDBContext(My.Settings.SQLServer, My.Settings.SQLDatabase)
            Using conn As New SqlConnection(dbcontext)
                Using cmd As New SqlCommand()
                    With cmd
                        .Connection = conn
                        .CommandText = selectsql.ToString
                        .CommandType = CommandType.Text
                        .CommandTimeout = 300
                    End With
                    Try
                        conn.Open()
                        count = Convert.ToInt32(cmd.ExecuteScalar())
                    Catch ex As SqlException
                        Debug.Print(ex.Message)
                    End Try
                End Using
            End Using
            lblRowCount.Text = String.Format("{0:N0}", Val(count))
            txtRecToExport.Text = count.ToString
        End Sub
    
        ' ...
        ' ...
        ' ...
    
    End Class
    So in a nutshell, I want the animated image to be displayed whilst the SQL query is running and then not be visible once the query is complete.

    Any feedback would be much appreciated.

  33. #33

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Getting Return Values

    Quote Originally Posted by layer7 View Post
    Hi John et al,

    Firstly, a big thanks to you John for taking the time to create this piece and enlighten us.

    I wonder if you could help me further.

    I've written an application which does some fairly heavy back-end SQL work. The SQL queries can take anything from a second up to minutes depending on what's be requested by the user.

    Whilst a query is running I'd like to provide the user with some feedback so they know the application is busy.

    So, what I've done so far is as follows.

    1. On the main form the user selects the criteria they require which will be collated to build the SQL query. There are hundreds of selectable items so it can get quite complicated but I've got all the item collection and SQL building working. That isn't the problem.

    2. When they user has selected what they need, they click a button on the main form which opens a new form. The new form (call it the 'count' form) will display an number depicting the number of records that the criteria matches. There are a couple of options on the 'count' form which allow some refinement to be made to the query which will rerun the query is changed by the user.

    The issue I have is that when the query is run I can of course display a message in say a status bar saying it's 'processing...' but I wanted to display an animation like that of a web page when it's doing something. So I've got my animation gif in my resources and this is what I've coded so far (please note I've used code from various places so I make no claims this is my own work).

    Code:
    Public Class Export
        Dim bw As BackgroundWorker = New BackgroundWorker
        Public Delegate Sub PictureVisibilityDelegate(ByVal visibility As Boolean)
        Dim ChangePictureVisibility As PictureVisibilityDelegate
    
           Private Sub Export_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            AddHandler bw.DoWork, AddressOf bw_DoWork
            AddHandler bw.RunWorkerCompleted, AddressOf bw_RunWorkerCompleted
            ChangePictureVisibility = AddressOf ChangeVisibility
            dtPostcodes.Clear()
            If Not bw.IsBusy = True Then
                If Me.IsHandleCreated Then
                    bw.RunWorkerAsync()
                End If
            End If
        End Sub
    
        Public Sub ChangeVisibility(ByVal visibility As Boolean)
            PictureBox1.Visible = visibility
        End Sub
    
        Private Sub bw_DoWork(sender As Object, e As DoWorkEventArgs)
            Me.Invoke(ChangePictureVisibility, True)
            getCount()
        End Sub
    
        Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
            Me.Invoke(ChangePictureVisibility, False)
        End Sub
    
        Private Sub getCount()
            Dim selectsql As String = String.Empty
            selectsql = buildSQL(True)
            Dim dbcontext As String = String.Empty
            dbcontext = SQLClass.setDBContext(My.Settings.SQLServer, My.Settings.SQLDatabase)
            Using conn As New SqlConnection(dbcontext)
                Using cmd As New SqlCommand()
                    With cmd
                        .Connection = conn
                        .CommandText = selectsql.ToString
                        .CommandType = CommandType.Text
                        .CommandTimeout = 300
                    End With
                    Try
                        conn.Open()
                        count = Convert.ToInt32(cmd.ExecuteScalar())
                    Catch ex As SqlException
                        Debug.Print(ex.Message)
                    End Try
                End Using
            End Using
            lblRowCount.Text = String.Format("{0:N0}", Val(count))
            txtRecToExport.Text = count.ToString
        End Sub
    
        ' ...
        ' ...
        ' ...
    
    End Class
    So in a nutshell, I want the animated image to be displayed whilst the SQL query is running and then not be visible once the query is complete.

    Any feedback would be much appreciated.
    There are some real issues there. This thread is all about the Invoke method and yet your code calls Invoke in two places that it doesn't to and then doesn't call it when it does need to That said, the whole point of using a BackgroundWorker is to avoid calling Invoke at all.

    Firstly, why do something to the UI as the very first thing in the DoWork event handler? Why not just do that immediately before calling RunWorkerAsync? You're already on the UI thread there so no need to call Invoke.

    Secondly, there's no need to call Invoke in the RunWorkerCompleted event handler because that is executed on the UI thread. That's the point: that's where you update the UI when the background operation is done.

    Last and definitely most is the fact that you call `getCount` from the DoWork event handler, which means that it's on a secondary thread. What's the very last thing you do in that method? You access two controls directly! That's exactly what this whole thread is about NOT doing. You should be either calling Invoke in order to update those two controls or else actually use your BackgroundWorker. You're updating those controls when all the background work is done. If only the BackgroundWorker provided a mechanism for that. Hang on, it does! That's exactly what the RunWorkerCompleted event is for.

  34. #34
    New Member
    Join Date
    Nov 2015
    Posts
    2

    Re: Getting Return Values

    Apologies my code was a little out of date.

    It might be easier to point you in the direction of Solution 3 from here http://www.codeproject.com/Questions...lepluscodeplus.

    The frozen UI issue is why I attempted to adopt this solution.

    Here is the updated code but it's still not playing ball.

    Code:
     
    Public Class Export
        Dim bw As BackgroundWorker = New BackgroundWorker
        Public Delegate Sub PictureVisibilityDelegate(ByVal visibility As Boolean)
        Dim ChangePictureVisibility As PictureVisibilityDelegate
    
        Private Sub Export_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            AddHandler bw.DoWork, AddressOf bw_DoWork
            AddHandler bw.RunWorkerCompleted, AddressOf bw_RunWorkerCompleted
            ChangePictureVisibility = AddressOf ChangeVisibility
        End Sub
    
        Private Sub Export_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
            If Not bw.IsBusy = True Then
                If Me.IsHandleCreated Then
                    bw.RunWorkerAsync()
                End If
            End If
        End Sub
    
        Public Sub ChangeVisibility(ByVal visibility As Boolean)
            PictureBox1.Visible = visibility
        End Sub
    
        Private Sub bw_DoWork(sender As Object, e As DoWorkEventArgs)
            Me.Invoke(ChangePictureVisibility, True)
            getCount()
        End Sub
    
        Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
            Me.Invoke(ChangePictureVisibility, False)
        End Sub
    
        Private Sub getCount()
            Dim selectsql As String = String.Empty
            selectsql = buildSQL(True)
            Dim dbcontext As String = String.Empty
            dbcontext = SQLClass.setDBContext(My.Settings.SQLMIdeaLServer, My.Settings.SQLMIdeaLDatabase)
            Using conn As New SqlConnection(dbcontext)
                Using cmd As New SqlCommand()
                    With cmd
                        .Connection = conn
                        .CommandText = selectsql.ToString
                        .CommandType = CommandType.Text
                        .CommandTimeout = 300
                    End With
                    Try
                        conn.Open()
                        count = Convert.ToInt32(cmd.ExecuteScalar())
                    Catch ex As SqlException
                        Debug.Print(ex.Message)
                    End Try
                End Using
            End Using
            lblRowCount.Text = String.Format("{0:N0}", Val(count))
            txtRecToExport.Text = count.ToString
        End Sub
    
     '...
    
    End Class
    Last edited by layer7; Nov 30th, 2015 at 11:20 AM.

  35. #35

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Getting Return Values

    Quote Originally Posted by layer7 View Post
    Apologies my code was a little out of date.

    It might be easier to point you in the direction of Solution 3 from here http://www.codeproject.com/Questions...lepluscodeplus.

    The frozen UI issue is why I attempted to adopt this solution.

    Here is the updated code but it's still not playing ball.

    Code:
     
    Public Class Export
        Dim bw As BackgroundWorker = New BackgroundWorker
        Public Delegate Sub PictureVisibilityDelegate(ByVal visibility As Boolean)
        Dim ChangePictureVisibility As PictureVisibilityDelegate
    
        Private Sub Export_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            AddHandler bw.DoWork, AddressOf bw_DoWork
            AddHandler bw.RunWorkerCompleted, AddressOf bw_RunWorkerCompleted
            ChangePictureVisibility = AddressOf ChangeVisibility
        End Sub
    
        Private Sub Export_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
            If Not bw.IsBusy = True Then
                If Me.IsHandleCreated Then
                    bw.RunWorkerAsync()
                End If
            End If
        End Sub
    
        Public Sub ChangeVisibility(ByVal visibility As Boolean)
            PictureBox1.Visible = visibility
        End Sub
    
        Private Sub bw_DoWork(sender As Object, e As DoWorkEventArgs)
            Me.Invoke(ChangePictureVisibility, True)
            getCount()
        End Sub
    
        Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
            Me.Invoke(ChangePictureVisibility, False)
        End Sub
    
        Private Sub getCount()
            Dim selectsql As String = String.Empty
            selectsql = buildSQL(True)
            Dim dbcontext As String = String.Empty
            dbcontext = SQLClass.setDBContext(My.Settings.SQLMIdeaLServer, My.Settings.SQLMIdeaLDatabase)
            Using conn As New SqlConnection(dbcontext)
                Using cmd As New SqlCommand()
                    With cmd
                        .Connection = conn
                        .CommandText = selectsql.ToString
                        .CommandType = CommandType.Text
                        .CommandTimeout = 300
                    End With
                    Try
                        conn.Open()
                        count = Convert.ToInt32(cmd.ExecuteScalar())
                    Catch ex As SqlException
                        Debug.Print(ex.Message)
                    End Try
                End Using
            End Using
            lblRowCount.Text = String.Format("{0:N0}", Val(count))
            txtRecToExport.Text = count.ToString
        End Sub
    
     '...
    
    End Class
    I pointed out three issues and nothing has been done about them in this new code. If you're not going to act on my advice then you're wasting my time. Raed my previous post and make the changes suggested. Once you've done that, if you're still having issues, post back and we can talk again. Until you make use of the information I have already provided, I have nothing more to add.

  36. #36
    Hyperactive Member
    Join Date
    May 2012
    Posts
    304

    Re: Accessing Controls from Worker Threads

    jmc,

    As always thank you for time, this post has solved something I've been trying accomplish for a while now.

    My problem now is, I'm trying to do it using a Task rather than BackroundWorker.

    vb Code:
    1. Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    2.         Threading.Thread.CurrentThread.Name = "UI Thread"
    3.     End Sub
    4.  
    5.     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    6.         Me.SetControlText(Me.TextBox1, "test")
    7.  
    8.         Dim t As New Task(Sub() SetControlText(Me.TextBox1, "background"))
    9.         t.Start()
    10.  
    11.         'Me.BackgroundWorker1.RunWorkerAsync()
    12.     End Sub
    13.  
    14.     Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    15.         Threading.Thread.CurrentThread.Name = "Worker Thread"
    16.         Threading.Thread.Sleep(5000)
    17.         Me.SetControlText(Me.TextBox1, "background")
    18.     End Sub
    19.  
    20.     Private Delegate Sub SetControlTextInvoker(ByVal ctl As Control, ByVal text As String)
    21.  
    22.     Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
    23.         If ctl.InvokeRequired Then
    24.             ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), ctl, text)
    25.         Else
    26.             ctl.Text = text
    27.         End If
    28.     End Sub

    This works but the Task does not run Asynchronously.

    So as a test I tried the Button1_Click as an Async Sub:

    vb Code:
    1. Private Async Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    2.         Me.SetControlText(Me.TextBox1, "test")
    3.  
    4.         Await Task.Run(Sub()
    5.                            'Task.Delay(3000)
    6.                            SetControlText(Me.TextBox1, "background")
    7.                        End Sub)
    8.  
    9.         'Me.BackgroundWorker1.RunWorkerAsync()
    10.     End Sub

    This again works, but I'm struggling to simulate Threading/compute time to see if it's fully working or if the UI locks up.

    I then tried the Private Sub SetControlText() as an Async sub, with a Task within the Else section. This gave me a Cross-Thread marshal error. Do I have to add another Invoke, or Invoke in the new Task?

    I've looked most of the day online for examples and explanation, I'm really struggling and maybe need to take a break, but thought I would ask for advice. Am I close or trying to do it completely wrong?

    I've found some examples using

    vb Code:
    1. Imports System.Runtime.Remoting.Messaging
    2. IAsyncResult
    3. AsyncDelegate
    4. AsyncCallback

    Is this something I need to be exploring, or can I easily tweak the code I have?

    I apologise I've only scratched the surface on Threading and Tasks so any help or advice would be greatly appreciated.

    Thank you in advance.
    Last edited by squatman; Mar 6th, 2021 at 06:30 PM.

  37. #37

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,274

    Re: Accessing Controls from Worker Threads

    Quote Originally Posted by squatman View Post
    jmc,

    As always thank you for time, this post has solved something I've been trying accomplish for a while now.

    My problem now is, I'm trying to do it using a Task rather than BackroundWorker.

    vb Code:
    1. Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    2.         Threading.Thread.CurrentThread.Name = "UI Thread"
    3.     End Sub
    4.  
    5.     Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    6.         Me.SetControlText(Me.TextBox1, "test")
    7.  
    8.         Dim t As New Task(Sub() SetControlText(Me.TextBox1, "background"))
    9.         t.Start()
    10.  
    11.         'Me.BackgroundWorker1.RunWorkerAsync()
    12.     End Sub
    13.  
    14.     Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    15.         Threading.Thread.CurrentThread.Name = "Worker Thread"
    16.         Threading.Thread.Sleep(5000)
    17.         Me.SetControlText(Me.TextBox1, "background")
    18.     End Sub
    19.  
    20.     Private Delegate Sub SetControlTextInvoker(ByVal ctl As Control, ByVal text As String)
    21.  
    22.     Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
    23.         If ctl.InvokeRequired Then
    24.             ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), ctl, text)
    25.         Else
    26.             ctl.Text = text
    27.         End If
    28.     End Sub

    This works but the Task does not run Asynchronously.

    So as a test I tried the Button1_Click as an Async Sub:

    vb Code:
    1. Private Async Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    2.         Me.SetControlText(Me.TextBox1, "test")
    3.  
    4.         Await Task.Run(Sub()
    5.                            'Task.Delay(3000)
    6.                            SetControlText(Me.TextBox1, "background")
    7.                        End Sub)
    8.  
    9.         'Me.BackgroundWorker1.RunWorkerAsync()
    10.     End Sub

    This again works, but I'm struggling to simulate Threading/compute time to see if it's fully working or if the UI locks up.

    I then tried the Private Sub SetControlText() as an Async sub, with a Task within the Else section. This gave me a Cross-Thread marshal error. Do I have to add another Invoke, or Invoke in the new Task?

    I've looked most of the day online for examples and explanation, I'm really struggling and maybe need to take a break, but thought I would ask for advice. Am I close or trying to do it completely wrong?

    I've found some examples using

    vb Code:
    1. Imports System.Runtime.Remoting.Messaging
    2. IAsyncResult
    3. AsyncDelegate
    4. AsyncCallback

    Is this something I need to be exploring, or can I easily tweak the code I have?

    I apologise I've only scratched the surface on Threading and Tasks so any help or advice would be greatly appreciated.

    Thank you in advance.
    The whole point of that SetControlText method is that you can just call it from anywhere and it will just work. If it is called on the UI thread then it will go straight to the Else block and set the control's Text directly and if it is called a secondary thread then it will go to the If block, marshal a second call to the UI thread and go to the Else block. There is no need to do anything to that method. If you do then all you can achieve is to break it.

    That said, one change that should be made those days is that it should just use a standard Action delegate rather than a delegate that you define yourself. In this case, an Action(Of Control, String) is what you want:
    vb.net Code:
    1. Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
    2.     If ctl.InvokeRequired Then
    3.         ctl.Invoke(New Action(Of Control, String)(AddressOf SetControlText), ctl, text)
    4.     Else
    5.         ctl.Text = text
    6.     End If
    7. End Sub
    Apart from that, you're trying to solve a problem that doesn't exist. Like I said, that method will work no matter where it's called so you can just call it and be confident that it will work. If you have tested it by calling it from an explicit background thread then it will definitely work from within a task.

  38. #38
    Hyperactive Member
    Join Date
    May 2012
    Posts
    304

    Re: Accessing Controls from Worker Threads

    jmc,

    Thank you for the reply.

    You're right your code obviously will work from anywhere UI or secondary thread. I probably didn't explain very well, I was trying invoke a control within a method that I will always want to run on a secondary thread, and not wanting to create a backgroundworker for each type of method I want to do this for.

    I've played around and managed to get it to do what I want, I just hope it's an acceptable practice:

    vb Code:
    1. Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    2.         Me.TextBox1.Invoke(New Action(Of Control, String)(AddressOf SetControlText), Me.TextBox1, "task")
    3.     End Sub
    4.  
    5.     Private Async Sub SetControlText(ByVal ctl As Control, ByVal text As String)
    6.         Await Task.Delay(3000)
    7.         ctl.Text = text
    8.     End Sub

    Thank you once again, and for the tip on New Action rather than delegate, much neater.

  39. #39
    New Member
    Join Date
    Oct 2021
    Posts
    9

    Re: Passing Parameters via Delegation

    Hi Jim
    After finding this article it has helped me greatly with an understanding of working with threads. Please can you advise how I would add rows to a DataGridView ?

    Thanks
    CM

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width