I have seen this problem before but I haven't seen an answer to the question that applied to my particular case. I have a BackgroundWorker running in my VB form, as well as a progress bar and some labels. I also (if it's important) have a WebBrowser on my form, but it isn't affected by the thread.
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim ints As Integer = Int(InputBox("What number to start at?"))
Dim inte As Integer = Int(InputBox("What number to end at?"))
ToolStripStatusLabel1.Text = "0 / " & inte - ints
ToolStripProgressBar1.Maximum = inte
ToolStripProgressBar1.Minimum = ints
ToolStripProgressBar1.Style = ProgressBarStyle.Continuous
Try
For z As Integer = ints To inte
ToolStripProgressBar1.Value = z
ToolStripStatusLabel1.Text = z & "/" & inte
'do some stuff here
catch etc
next
When the loop is running, sometimes it stops and the progress bar disappears. Any idea why?
Btw the only thing I'm doing in there is running an httpwebrequest and handling the string.
This is likely to do with the fact that you're setting the value of a user interface object (ToolStripProgressBar1) within the BackgroundWorker's DoWork method which is running in it's own thread, separate from the User Interface thread which the ToolStripProgressBar1 is in.
As per the Note on this MSDN page:
You must be careful not to manipulate any user-interface objects in
your DoWork event handler. Instead, communicate to the user interface
through the ProgressChanged and RunWorkerCompleted events.
BackgroundWorker events are not marshaled across AppDomain boundaries.
Do not use a BackgroundWorker component to perform multithreaded
operations in more than one AppDomain.
What you should do is to change the code that's inside the loop (For z As Integer = ints To inte) so that instead of setting the Value and Text properties directly, you call the BackgroundWorker's ReportProgress method. This raises the ProgressChanged event which you can then handle on the main UI thread. It's in here that you can then safely access the properties of User Interface components and objects.
Related
I have an application with a DataGridView on which multiple people could be working at the same time. I want to have each user's current row location displayed via a different colour row in the DataGridView.
Previously I was doing all of this updating via the RowEnter event however the performance is not satisfactory, for obvious reasons.
I'm trying to have a background thread which loops every 10 seconds to populate a DataTable with keys of the other users' locations which then references a key column in the DGV, and if they match, change the DGV row background color else set it to the default.
My current code, below, loops every 10s but it doesn't actually update the DGV.
Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ActiveThread = True
dgvThread = New Thread(AddressOf UpdateDGVFromThread) With {
.IsBackground = True}
dgvThread.Start()
End Sub
Public Sub UpdateDGVFromThread()
Do While ActiveThread = True
'Sets table with key values
dtUsers = CLS_USERS.GetUsers(User)
'Loop through them
For Each row As DataRow In dtUsers.Rows
intSeq = row("SEQUENCE")
'Loop through each DGV row and compare the values
For Each dgv_row As DataGridViewRow In dgvCandList.Rows
dgvCandList.BeginInvoke(
Sub()
If dgv_row.Cells("CURRENT_CAND_SQ").Value = intSeq Then
dgv_row.DefaultCellStyle.BackColor = Color.DarkCyan
Else
dgv_row.DefaultCellStyle.BackColor = Color.Cyan
End If
End Sub)
Next
Next
Thread.Sleep(10000)
Loop
End Sub
I tried using dgv.Invoke() rather than .BeginInvoke() but this seemed to lock up the UI thread constantly and only the DGV was unlocked.
Can anyone point me in the right direction?
The BeginInvoke method is used to asynchronously invoke a method delegate on the thread that created the Control's handle. The UI thread, here. It's signature is:
Public Function BeginInvoke (method As Delegate) As IAsyncResult
The method Delegate is then declared in the same thread where the Control invoked has been created.
The delegate should then be declared like this:
In the UI thread:
Delegate Sub MyUpdateDelegate()
Public Sub MyUpdateMethod()
[SomeControl].Text = "Updated Text"
End Sub
In another thread:
Private Sub InvokeFromAnotherThread()
'Prefer the Parent Form as marshaller
Me.BeginInvoke(New MyUpdateDelegate(AddressOf MyUpdateMethod))
'(...)
'You can also use a Control, but the Parent Form is better
[SomeControl].BeginInvoke(New MyUpdateDelegate(AddressOf MyUpdateMethod))
End Sub
Using an anonymous method in-place won't cut it.
There's a shortcut, provided by the MethodInvoker delegate:
MethodInvoker provides a simple delegate that is used to invoke a
method with a void parameter list. This delegate can be used when
making calls to a control's Invoke method, or when you need a simple
delegate but do not want to define one yourself.
Using a MethodInvoker delegate, there's no need to declare a delegate in the UI thread. An anonymous method can be used here, it will be invoked in the UI thread:
Private Sub InvokeFromAnotherThread()
'(...)
BeginInvoke(New MethodInvoker(Sub() [SomeControl].Text = "Updated Text"))
'(...)
End Sub
Or:
Private Sub InvokeFromAnotherThread()
'(...)
BeginInvoke(New MethodInvoker(
Sub()
[SomeControl].Text = "Updated Text"
[SomeOtherControl].BackColor = Color.Red
End Sub))
'(...)
End Sub
Why I suggested a Timer:
The thread you're using has one task only: update a Control in the UI thread and then sleep.
To perform this task, it needs to invoke a method in the UI thread. If the reason why the thread has been created is to avoid blocking the UI thread, a Timer will do the same thing. A System.Windows.Forms.Timer, specifically, will raise its Tick event in the UI thread, without cross-thread calls.
The practical effect is more or less the same.
I made a file search program in visual studio on windows 10 using .net lang,
My problem starts from form1 with a "dim frm2 as form2 = new form2" call,
after the new form being shown i start a while loop on form1 that feeds data into a listbox in form 2:
1)form1 call form2 and show it.
2)form1 start a while loop.
3)inside the while loop data being fed to listbox1 in frm2
Now everything works on windows 10, the while loop can run as much as it needs without any trouble, the window can loose focus and regain focus without showing any "Not Responding.." msgs or white\black screens..
But, when i take the software to my friend computer which is running windows 7, install all required frameworks and visual studio itself, run it from the .sln in debug mode, and do the same search on the same folder the results are:
1) the while loop runs smoothly as long as form 2 dont loose focus
(something that doesnt happen on windows 10)
2) when i click anywhere on the screen the software loose focus what
causes 1) to happen (black screen\white screen\not responding etc..)
3) if i wait the time needed for the loop and dont click anywhere else
it keeps running smoohtly, updating a label like it should with the
amount of files found.. and even finish the loop with 100% success
(again unless i click somewhere)
Code Example:
Sub ScanButtonInForm1()
Dim frm2 As Form2 = New Form2
frm2.Show()
Dim AlreadyScanned As HashSet(Of String) = New HashSet(Of String)
Dim stack As New Stack(Of String)
stack.Push("...Directoy To Start The Search From...")
Do While (stack.Count > 0)
frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
frm2.Label4.Refresh()
Dim ScanDir As String = stack.Pop
If AlreadyScanned.Add(ScanDir) Then
Try
Try
Try
Dim directoryName As String
For Each directoryName In System.IO.Directory.GetDirectories(ScanDir)
stack.Push(directoryName)
frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
frm2.Label4.Refresh()
Next
frm2.ListBox1.Items.AddRange(System.IO.Directory.GetFiles(ScanDir, "*.*", System.IO.SearchOption.AllDirectories))
Catch ex5 As UnauthorizedAccessException
End Try
Catch ex2 As System.IO.PathTooLongException
End Try
Catch ex4 As System.IO.DirectoryNotFoundException
End Try
End If
Loop
End Sub
My conclusions was simple!
1) windows 7 dont support live ui (label) update from a while loop
called from a button...
2) windows 7 could possibly support a new
thread running the same loop
i think mabye if i run all the code in a thread mabye the ui will remain responsive
(by the way the UI is not responsive in windows 10 but i still see
the label refresh and nothing crashes when form loose focus..)
so i know how to do that but i also know that if i do that a thread will not be able to update a listbox or a label in a form and refresh it..
so the thread will need to update an external file with the data and the form2 will need to read that data live from the file but will it make the same problems? i have no idea what to do.. can use some help and tips. THANK YOU!
I must menttion the fact that the loop is working on windows 10 without a responsive UI means i cant click on any button but i can
still see the label refresh BUT on windows 7 everything works the same
UNLESS i click somewhere, no matter where i click on windows the loop
crashes
im using framework 4.6.2 developer
While I'm glad you found a solution, I advise against using Application.DoEvents() because it is bad practice.
Please see this blog post: Keeping your UI Responsive and the Dangers of Application.DoEvents.
Simply put, Application.DoEvents() is a dirty workaround that makes your UI seem responsive because it forces the UI thread to handle all currently available window messages. WM_PAINT is one of those messages which is why your window redraws.
However this has some backsides to it... For instance:
If you were to close the form during this "background" process it would most likely throw an error.
Another backside is that if the ScanButtonInForm1() method is called by the click of a button you'd be able to click that button again (unless you set Enabled = False) and starting the process once more, which brings us to yet another backside:
The more Application.DoEvents()-loops you start the more you occupy the UI thread, which will cause your CPU usage to rise rather quickly. Since every loop is run in the same thread your processor cannot schedule the work over different cores nor threads, so your code will always run on one core, eating as much CPU as possible.
The replacement is, of course, proper multithreading (or the Task Parallel Library, whichever you prefer). Regular multithreading actually isn't that hard to implement.
The basics
In order to create a new thread you only need to declare an instance of the Thread class and pass a delegate to the method you want the thread to run:
Dim myThread As New Thread(AddressOf <your method here>)
...then you should set its IsBackground property to True if you want it to close automatically when the program closes (otherwise it keeps the program open until the thread finishes).
Then you just call Start() and you have a running background thread!
Dim myThread As New Thread(AddressOf myThreadMethod)
myThread.IsBackground = True
myThread.Start()
Accessing the UI thread
The tricky part about multithreading is to marshal calls to the UI thread. A background thread generally cannot access elements (controls) on the UI thread because that might cause concurrency issues (two threads accessing the same control at the same time). Therefore you must marshal your calls to the UI by scheduling them for execution on the UI thread itself. That way you will no longer have the risk of concurrency because all UI related code is run on the UI thread.
To marhsal calls to the UI thread you use either of the Control.Invoke() or Control.BeginInvoke() methods. BeginInvoke() is the asynchronous version, which means it doesn't wait for the UI call to complete before it lets the background thread continue with its work.
One should also make sure to check the Control.InvokeRequired property, which tells you if you already are on the UI thread (in which case invoking is extremely unnecessary) or not.
The basic InvokeRequired/Invoke pattern looks like this (mostly for reference, keep reading below for shorter ways):
'This delegate will be used to tell Control.Invoke() which method we want to invoke on the UI thread.
Private Delegate Sub UpdateTextBoxDelegate(ByVal TargetTextBox As TextBox, ByVal Text As String)
Private Sub myThreadMethod() 'The method that our thread runs.
'Do some background stuff...
If Me.InvokeRequired = True Then '"Me" being the current form.
Me.Invoke(New UpdateTextBoxDelegate(AddressOf UpdateTextBox), TextBox1, "Status update!") 'We are in a background thread, therefore we must invoke.
Else
UpdateTextBox(TextBox1, "Status update!") 'We are on the UI thread, no invoking required.
End If
'Do some more background stuff...
End Sub
'This is the method that Control.Invoke() will execute.
Private Sub UpdateTextBox(ByVal TargetTextBox As TextBox, ByVal Text As String)
TargetTextBox.Text = Text
End Sub
New UpdateTextBoxDelegate(AddressOf UpdateTextBox) creates a new instance of the UpdateTextBoxDelegate that points to our UpdateTextBox method (the method to invoke on the UI).
However as of Visual Basic 2010 (10.0) and above you can use Lambda expressions which makes invoking much easier:
Private Sub myThreadMethod()
'Do some background stuff...
If Me.InvokeRequired = True Then '"Me" being the current form.
Me.Invoke(Sub() TextBox1.Text = "Status update!") 'We are in a background thread, therefore we must invoke.
Else
TextBox1.Text = "Status update!" 'We are on the UI thread, no invoking required.
End If
'Do some more background stuff...
End Sub
Now all you have to do is type Sub() and then continue typing code like if you were in a regular method:
If Me.InvokeRequired = True Then
Me.Invoke(Sub()
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
ProgressBar1.Value += 1
End Sub)
Else
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
ProgressBar1.Value += 1
End If
And that's how you marshal calls to the UI thread!
Making it simpler
To make it even more simple to invoke to the UI you can create an Extension method that does the invoking and InvokeRequired check for you.
Place this in a separate code file:
Imports System.Runtime.CompilerServices
Public Module Extensions
''' <summary>
''' Invokes the specified method on the calling control's thread (if necessary, otherwise on the current thread).
''' </summary>
''' <param name="Control">The control which's thread to invoke the method at.</param>
''' <param name="Method">The method to invoke.</param>
''' <param name="Parameters">The parameters to pass to the method (optional).</param>
''' <remarks></remarks>
<Extension()> _
Public Function InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object()) As Object
If Parameters IsNot Nothing AndAlso _
Parameters.Length = 0 Then Parameters = Nothing
If Control.InvokeRequired = True Then
Return Control.Invoke(Method, Parameters)
Else
Return Method.DynamicInvoke(Parameters)
End If
End Function
End Module
Now you only need to call this single method when you want to access the UI, no additional If-Then-Else required:
Private Sub myThreadMethod()
'Do some background stuff...
Me.InvokeIfRequired(Sub()
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
End Sub)
'Do some more background stuff...
End Sub
Returning objects/data from the UI with InvokeIfRequired()
With my InvokeIfRequired() extension method you can also return objects or data from the UI thread in a simple manner. For instance if you want the width of a label:
Dim LabelWidth As Integer = Me.InvokeIfRequired(Function() Label1.Width)
Example
The following code will increment a counter that tells you for how long the thread has run:
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Dim CounterThread As New Thread(AddressOf CounterThreadMethod)
CounterThread.IsBackground = True
CounterThread.Start()
Button1.Enabled = False 'Make the button unclickable (so that we cannot start yet another thread).
End Sub
Private Sub CounterThreadMethod()
Dim Time As Integer = 0
While True
Thread.Sleep(1000) 'Wait for approximately 1000 ms (1 second).
Time += 1
Me.InvokeIfRequired(Sub() Label1.Text = "Thread has been running for: " & Time & " seconds.")
End While
End Sub
Hope this helps!
The reason your application is freezing is that you are doing all the work on the UI thread. Check out Async and Await. It uses threading in the background but makes it way easier to manage. An example here:
https://stephenhaunts.com/2014/10/14/using-async-and-await-to-update-the-ui-thread/
I want to use a backgroundworker to poll a hardware sensor very frequently without leaving my UI inoperable.
Because the backgroundworker simply polls until interrupted - runtime is purely dictated by the user interrupting it - it has no change in progress so to speak.
If I call ReportProgress with a constant value, e.g. ReportProgress(1), will this still call ProgressChanged? I require ProgressChanged to update the UI in accordance with the latest poll data.
The value passed as first parameter to ReportProgress just serves at your code on the UI thread to display the advancement of your background task.
It has no importance for the execution of the call to ProgressChanged.
If you need to communicate some different data to your ProgressChanged event you could use the overload of ReportProgress that takes two arguments and allows to pass the instance of a custom object as second parameter.
In this very trivial example, I have defined a class named WorkingStatus with just one property that I change in the DoWork method, then I pass an instance of this class to the ProgressChanged event. Of course your WorkingStatus class could be more complex with all the informations that you want to display on the UI thread
public class WorkingStatus
public Current as Integer
'.... other properties as needed....
End Class
Sub Main
Dim bkw = new BackgroundWorker()
bkw.WorkerReportsProgress = true
AddHandler bkw.ProgressChanged, AddressOf bgw_ProgressChanged
AddHandler bkw.DoWork, AddressOf bgw_DoWork
bkw.RunWorkerAsync()
' This loop just to avoid the immediate close of the example
Dim counter = 0
While (bkw.IsBusy)
counter+=1
Console.WriteLine("IsBusy " & counter.ToString())
Thread.Sleep(150)
End While
End Sub
private sub bgw_DoWork(sender as object, e as DoWorkEventArgs)
Dim bgw = DirectCast(sender, BackgroundWorker)
Dim sts = new WorkingStatus() With {.Current = 0}
' A simulation of your inner working
for i = 0 to 10
Thread.Sleep(5000)
sts.Current+=1
bgw.ReportProgress(1, sts)
Next
Console.WriteLine("Background DoWork ENDED")
End Sub
private sub bgw_ProgressChanged(sender as object, e as ProgressChangedEventArgs)
Dim sts = DirectCast(e.UserState, WorkingStatus)
Console.WriteLine("Progress:" & e.ProgressPercentage.ToString() & ", Status=" & sts.Current)
End Sub
I am writing a program that will allow me to run a number of different hardware test routines.
The routines can be quite time consuming, lasting up to 30mins. During this time, I have to control a selection of test equipment to set up conditions and take measurements.
I was thinking that using a background worker to carry out the tasks would be ideal, and allow the UI to stay responsive. This worked well until one of the routines requires me to take measurements every 1.5seconds. I am using a system timer to trigger these events. The timer is created and started in the doWork sub of the background worker, however, I find that the delegate is running in the main(UI) thread and not in the background worker thread as I thought.
Am I doing something wrong? I have attached the main parts of a simplified program that has the same structure.
Private Sub getMeasurement()
' Runs in backgroundWorker thread
Me.TextBox2.Text = (System.DateTime.Now - startTime).TotalSeconds.ToString
startTime = System.DateTime.Now
Debug.Print("Thread name is " & Thread.CurrentThread.Name & ", ID = " & Thread.CurrentThread.ManagedThreadId)
End Sub
Private Sub OnTimedEvent()
'Runs in own thread, Calls getMeasurement which runs in BackgroundWorker thread
Thread.CurrentThread.Name = "OTE"
Debug.Print("In OnTimedEvent, thread = " & Thread.CurrentThread.Name & ", ID = " & Thread.CurrentThread.ManagedThreadId)
Dim ServiceTimerDelegate As New ServiceTimerDelegate(AddressOf getMeasurement)
Me.BeginInvoke(ServiceTimerDelegate)
End Sub
Private Sub backgroundWorker1_DoWork(ByVal sender As System.Object, _
ByVal e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Thread.CurrentThread.Name = "BW1"
mTimer = New Timers.Timer(1490) ' 14.9secs (allow for some latency)
AddHandler mTimer.Elapsed, New Timers.ElapsedEventHandler(AddressOf OnTimedEvent)
Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
Dim i As Integer
'main timer for measurements every 1.5secs (may change to take interval from UI)
Select Case e.Argument
Case "Sunday"
mTimer.Start()
Debug.Print("Thread in DoWork = " & Thread.CurrentThread.Name & ", ID = " & Thread.CurrentThread.ManagedThreadId)
startTime = System.DateTime.Now
'main loop for temperature ramping
For i = 20 To 70
If (worker.CancellationPending = True) Then
e.Cancel = True
Else
Thread.Sleep(500)
worker.ReportProgress((i - 19) * (100 / 50))
i += 1
End If
Next
Case Else
e.Cancel = True
End Select
End Sub
Three things are confusing here, and make me think that the code you've posted above doesn't actually use the BackgroundWorker for its function.
The DoWork event always runs in a separate thread from the UI. That's the entire point of it. ReportProgress runs in the UI thread, but you're not making use of that.
You don't actually appear to be running the BackgroundWorker at all in the above code. Rather, you seem to be using a ServiceTimerDelegate - not 100% sure I know what that is. You've defined a BackgroundWorker within your DoWork handler (which makes no sense), but you never seem to be actually hooking a BackgroundWorker up to that handler nor calling RunWorkerAsync on it.
Even if you were running getMeasurement() in a BackgroundWorker, it should fail - you're altering the contents of a UI item, which should cause an exception due to InvalidCrossThreadAccess. If you need to alter a UI control, use the ReportProgress event, which occurs within the UI thread.
I keep running into situations where I don't know what event I have to listen to in order to execute my code at the correct time. Is there any way to get a log of all events that is raised? Any way to filter that log based on what object raised the event?
EDIT: Final solution:
Private Sub WireAllEvents(ByVal obj As Object)
Dim parameterTypes() As Type = {GetType(System.Object), GetType(System.EventArgs)}
Dim Events = obj.GetType().GetEvents()
For Each ev In Events
Dim handler As New DynamicMethod("", Nothing, parameterTypes, GetType(main))
Dim ilgen As ILGenerator = handler.GetILGenerator()
ilgen.EmitWriteLine("Event Name: " + ev.Name)
ilgen.Emit(OpCodes.Ret)
ev.AddEventHandler(obj, handler.CreateDelegate(ev.EventHandlerType))
Next
End Sub
And yes, I know this is not a good solution when you actually want to do real stuff that triggers off the events. There are good reasons for the 1 method - 1 event approach, but this is still useful when trying to figure out which of the methods you want to add your handlers to.
The only way that I can think of is to use Reflection to enumerate all of the events and wire up a generic handler which would be a PITA.
Is the problem with Framework events? If so, Microsoft does a pretty good job of giving event life-cycle/call order.
Edit
So here's a global event capture routine:
Private Sub WireAllEvents(ByVal obj As Object)
'Grab all of the events for the supplied object
Dim Events = obj.GetType().GetEvents()
'This points to the method that we want to invoke each time
Dim HandlerMethod = Me.GetType().GetMethod("GlobalHandler")
'Loop through all of the events
For Each ev In Events
'Wire in a handler for the event
ev.AddEventHandler(obj, [Delegate].CreateDelegate(ev.EventHandlerType, Me, HandlerMethod))
Next
End Sub
Public Sub GlobalHandler(ByVal sender As Object, ByVal e As EventArgs)
'Probably want to do something more meaningful here than just tracing
Trace.WriteLine(e)
End Sub
To wire it in just call WireAllEvents(Me.DataGridView1) supplying your object. Almost all MS events use sender/e (including DataGridView) format but if for some reason it doesn't I think this code will error out. But I just tested it with both a DataGridView and Form and it worked as expected.