-
Nov 27th, 2007, 12:42 AM
#1
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:
Private Sub ResetTextBoxText() Me.TextBox1.ResetText() 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:
Private Sub ResetTextBoxText() If Me.TextBox1.InvokeRequired Then Else Me.TextBox1.ResetText() End If 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:
Private Sub ResetTextBoxText() If Me.TextBox1.InvokeRequired Then Me.TextBox1.Invoke(New MethodInvoker(AddressOf ResetTextBoxText)) Else Me.TextBox1.ResetText() End If 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.
Last edited by jmcilhinney; Oct 28th, 2008 at 12:11 AM.
-
Nov 27th, 2007, 12:58 AM
#2
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:
Private Sub SetTextBoxText(ByVal text As String)
Me.TextBox1.Text = text
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:
Private Sub SetTextBoxText(ByVal text As String)
then add the 'Delegate' key word:
vb.net Code:
Private Delegate Sub SetTextBoxText(ByVal text As String)
and finally change the name:
vb.net Code:
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:
Private Sub SetTextBoxText(ByVal text As String)
If Me.TextBox1.InvokeRequired Then
Else
Me.TextBox1.Text = text
End If
End Sub
Finally we call the control's Invoke method and pass our delegate:
vb.net Code:
Private Sub SetTextBoxText(ByVal text As String)
If Me.TextBox1.InvokeRequired Then
Me.TextBox1.Invoke(New SetTextBoxTextInvoker(AddressOf SetTextBoxText), _
text)
Else
Me.TextBox1.Text = text
End If
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:
Private Delegate Sub SetControlTextInvoker(ByVal ctl As Control, ByVal text As String)
Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
If ctl.InvokeRequired Then
ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), _
ctl, _
text)
Else
ctl.Text = text
End If
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:
ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), _
ctl, _
text)
you would have to do this in .NET 1.x:
vb.net Code:
ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), _
New Object() {ctl, _
text})
Last edited by jmcilhinney; Dec 2nd, 2007 at 04:56 AM.
-
Nov 27th, 2007, 01:18 AM
#3
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:
Private Function GetTextBoxText() As String
Return Me.TextBox1.Text
End Function
Next we declare a matching delegate:
vb.net Code:
Private Delegate Function GetTextBoxTextInvoker() As String
Private Function GetTextBoxText() As String
Return Me.TextBox1.Text
End Function
After that we test the InvokeRequired property:
vb.net Code:
Private Delegate Function GetTextBoxTextInvoker() As String
Private Function GetTextBoxText() As String
If Me.TextBox1.InvokeRequired Then
Else
Return Me.TextBox1.Text
End If
End Function
Finally we call the Invoke method and pass an instance of our delegate:
vb.net Code:
Private Delegate Function GetTextBoxTextInvoker() As String
Private Function GetTextBoxText() As String
If Me.TextBox1.InvokeRequired Then
Return CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
Else
Return Me.TextBox1.Text
End If
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:
Private Delegate Function GetTextBoxTextInvoker() As String
Private Function GetTextBoxText() As String
Dim text As String
If Me.TextBox1.InvokeRequired Then
text = CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
Else
text = Me.TextBox1.Text
End If
Return text
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:
Private Delegate Function GetControlTextInvoker(ByVal ctl As Control) As String
Private Function GetControlText(ByVal ctl As Control) As String
Dim text As String
If ctl.InvokeRequired Then
text = CStr(ctl.Invoke(New GetControlTextInvoker(AddressOf GetControlText), _
ctl))
Else
text = ctl.Text
End If
Return text
End Function
-
Nov 27th, 2007, 11:35 AM
#4
Re: Accessing Controls from Worker Threads
This is a far better explanation than anything else I've seen
Thanks JM
-
Aug 10th, 2008, 04:55 AM
#5
Fanatic Member
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.
-
Aug 14th, 2008, 04:27 PM
#6
Addicted Member
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?
-
Aug 18th, 2008, 02:53 PM
#7
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.
-
Sep 1st, 2008, 08:48 PM
#8
Addicted Member
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:
Private Delegate Function GetTextBoxTextInvoker() As String
Private Function GetTextBoxText() As String
If Me.TextBox1.InvokeRequired Then
Return CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
Else
Return Me.TextBox1.Text
End If
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.
-
Sep 1st, 2008, 09:12 PM
#9
Re: Accessing Controls from Worker Threads
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:
Private Delegate Function GetTextBoxTextInvoker() As String
Private Function GetTextBoxText() As String
If Me.TextBox1.InvokeRequired Then
Return CStr(Me.TextBox1.Invoke(New GetTextBoxTextInvoker(AddressOf GetTextBoxText)))
Else
Return Me.TextBox1.Text
End If
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.
-
Sep 1st, 2008, 11:19 PM
#10
Addicted Member
Re: Accessing Controls from Worker Threads
-
Sep 3rd, 2008, 07:59 PM
#11
Fanatic Member
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
-
Sep 3rd, 2008, 08:22 PM
#12
Re: Accessing Controls from Worker Threads
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:
Private Delegate Sub AppendTextToTextBoxInvoker(ByVal text As String)
Private Sub AppendTextToTextBox(ByVal text As String)
If Me.TextBox1.InvokeRequired Then
Me.TextBox1.Invoke(New AppendTextToTextBoxInvoker(AddressOf AppendTextToTextBox), text)
Else
Me.TextBox1.AppendText(text)
End If
End Sub
and then somewhere in your background thread you'd do this:
vb.net Code:
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:
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.
-
Sep 3rd, 2008, 08:46 PM
#13
Fanatic Member
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.
-
Nov 24th, 2008, 04:06 AM
#14
Junior Member
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?
-
Nov 24th, 2008, 04:27 AM
#15
Re: Accessing Controls from Worker Threads
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.
-
Apr 21st, 2010, 02:20 AM
#16
Re: Accessing Controls from Worker Threads
Here's the last example from post #1 redone for WPF:
vb.net Code:
Private WithEvents worker As New BackgroundWorker Private Sub Window1_Loaded(ByVal sender As Object, _ ByVal e As RoutedEventArgs) Handles MyBase.Loaded Thread.CurrentThread.Name = "UI Thread" End Sub Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As RoutedEventArgs) Handles Button1.Click Me.ResetTextBoxText() Me.worker.RunWorkerAsync() End Sub Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _ ByVal e As DoWorkEventArgs) Handles worker.DoWork Thread.CurrentThread.Name = "Worker Thread" Me.ResetTextBoxText() End Sub Private Sub ResetTextBoxText() Debug.WriteLine(Thread.CurrentThread.Name, "ResetTextBoxText") If Me.TextBox1.Dispatcher.CheckAccess() Then Me.TextBox1.Clear() Else Me.TextBox1.Dispatcher.Invoke(New Action(AddressOf ResetTextBoxText)) End If End Sub
Last edited by jmcilhinney; May 30th, 2010 at 12:01 AM.
-
Jun 4th, 2010, 12:47 AM
#17
Re: Accessing Controls from Worker Threads
Here's the last example from post #1 reworked to use a SynchronizationContext:
vb.net Code:
Private context As Threading.SynchronizationContext = Threading.SynchronizationContext.Current 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.context.Send(AddressOf Me.ResetTextBoxText, Nothing) 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.context.Send(AddressOf Me.ResetTextBoxText, Nothing) End Sub Private Sub ResetTextBoxText(ByVal userState As Object) Debug.WriteLine(Threading.Thread.CurrentThread.Name, "ResetTextBoxText") Me.TextBox1.ResetText() 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.
-
Nov 21st, 2010, 01:50 PM
#18
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
Originally Posted by SJWhiteley
"game trainer" is the same as calling the act of robbing a bank "wealth redistribution"....
-
Nov 21st, 2010, 06:36 PM
#19
Re: Accessing Controls from Worker Threads
Originally Posted by weirddemon
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.
-
Dec 11th, 2010, 03:21 PM
#20
Lively Member
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
-
Dec 11th, 2010, 08:53 PM
#21
Re: Accessing Controls from Worker Threads
Originally Posted by Starf0x
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:
Private Sub EnableEveryControl()
If Me.InvokeRequired Then
Me.Invoke(New MethodInvoker(AddressOf EnableEveryControl))
Else
For Each ctl In Me.Controls
ctl.Enabled = True
Next
End If
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.
-
Dec 12th, 2010, 09:19 PM
#22
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!
-
Mar 6th, 2011, 09:44 PM
#23
Fanatic Member
Re: Accessing Controls from Worker Threads
Also because the invoke happens on Main UI thread anyway, we don't need to do synclock right?>
-
Mar 6th, 2011, 09:56 PM
#24
Re: Accessing Controls from Worker Threads
Originally Posted by teguh123
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.
-
Apr 6th, 2011, 05:40 PM
#25
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
-
Jan 23rd, 2012, 04:26 PM
#26
Junior Member
Re: Accessing Controls from Worker Threads
-
Jun 23rd, 2013, 11:59 PM
#27
Hyperactive Member
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!
-
Jun 13th, 2014, 01:24 AM
#28
Registered User
-
Oct 14th, 2015, 10:35 AM
#29
Hyperactive Member
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?
-
Oct 14th, 2015, 11:12 AM
#30
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.
-
Oct 14th, 2015, 06:41 PM
#31
Re: Accessing Controls from Worker Threads
Originally Posted by mnxford
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.
-
Nov 30th, 2015, 05:27 AM
#32
New Member
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.
-
Nov 30th, 2015, 08:27 AM
#33
Re: Getting Return Values
Originally Posted by layer7
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.
-
Nov 30th, 2015, 11:13 AM
#34
New Member
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.
-
Nov 30th, 2015, 03:08 PM
#35
Re: Getting Return Values
Originally Posted by layer7
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.
-
Mar 6th, 2021, 05:16 PM
#36
Hyperactive Member
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:
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.SetControlText(Me.TextBox1, "test")
Dim t As New Task(Sub() SetControlText(Me.TextBox1, "background"))
t.Start()
'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"
Threading.Thread.Sleep(5000)
Me.SetControlText(Me.TextBox1, "background")
End Sub
Private Delegate Sub SetControlTextInvoker(ByVal ctl As Control, ByVal text As String)
Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
If ctl.InvokeRequired Then
ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), ctl, text)
Else
ctl.Text = text
End If
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:
Private Async Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Me.SetControlText(Me.TextBox1, "test")
Await Task.Run(Sub()
'Task.Delay(3000)
SetControlText(Me.TextBox1, "background")
End Sub)
'Me.BackgroundWorker1.RunWorkerAsync()
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:
Imports System.Runtime.Remoting.Messaging
IAsyncResult
AsyncDelegate
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.
-
Mar 6th, 2021, 09:01 PM
#37
Re: Accessing Controls from Worker Threads
Originally Posted by squatman
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:
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.SetControlText(Me.TextBox1, "test")
Dim t As New Task(Sub() SetControlText(Me.TextBox1, "background"))
t.Start()
'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"
Threading.Thread.Sleep(5000)
Me.SetControlText(Me.TextBox1, "background")
End Sub
Private Delegate Sub SetControlTextInvoker(ByVal ctl As Control, ByVal text As String)
Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
If ctl.InvokeRequired Then
ctl.Invoke(New SetControlTextInvoker(AddressOf SetControlText), ctl, text)
Else
ctl.Text = text
End If
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:
Private Async Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Me.SetControlText(Me.TextBox1, "test")
Await Task.Run(Sub()
'Task.Delay(3000)
SetControlText(Me.TextBox1, "background")
End Sub)
'Me.BackgroundWorker1.RunWorkerAsync()
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:
Imports System.Runtime.Remoting.Messaging
IAsyncResult
AsyncDelegate
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:
Private Sub SetControlText(ByVal ctl As Control, ByVal text As String)
If ctl.InvokeRequired Then
ctl.Invoke(New Action(Of Control, String)(AddressOf SetControlText), ctl, text)
Else
ctl.Text = text
End If
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.
-
Mar 7th, 2021, 08:08 AM
#38
Hyperactive Member
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:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Me.TextBox1.Invoke(New Action(Of Control, String)(AddressOf SetControlText), Me.TextBox1, "task")
End Sub
Private Async Sub SetControlText(ByVal ctl As Control, ByVal text As String)
Await Task.Delay(3000)
ctl.Text = text
End Sub
Thank you once again, and for the tip on New Action rather than delegate, much neater.
-
Jan 2nd, 2024, 08:12 AM
#39
New Member
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
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|