Interact with form while working in the background - vb.net

The goal is that the form should still be clickable while the code runs (mainly to be able to break the code) AND keeps getting updated by the running code at the same time. First part works, not the second part.
Public Class FormMain
Public PleaseStopAll As Boolean
Private Sub BreakCode_Click(sender As Object, e As EventArgs) Handles BreakCode.Click
PleaseStopAll = True
End Sub
Private Sub Testeur_Click(sender As Object, e As EventArgs) Handles Testeur.Click
PleaseStopAll = False
ThreadPool.QueueUserWorkItem(AddressOf LaunchTask, 0)
End Sub
Sub LaunchTask(o As Object)
TheTask(Me)
End Sub
Public Sub ShowSomeMessage(msg As String
Me.Displayer.text = msg
End Sub
End Class
Module TaskDoer
Sub TheTask(MyMenu As FormMain)
For i As Integer = 0 To 42
whatever() 'some tasks that cannot be interrupted safely
MyMenu.ShowSomeMessage("task #" & CStr(i) & " over") ' Here is the error
If MyMenu.PleaseStopAll Then Exit For
Next
End Sub
End Module
The "status update" line tells that this task cannot interact with its parent task.
If I remove this line, the code runs just fine : code breaks when I ask to, and form remains clickable.
I tried with BackgroundWorker, but it needs actual progression to report something with ReportProgress (in increasing %) do I cannot just give a status (as string)

Pass anything else via a variable or structure
BackgroundWorker1.WorkerReportsProgress = True '<Make sure this is set
BackgroundWorker1.RunWorkerAsync()
Private SomeStatus As String
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
SomeStatus = "Hello"
BackgroundWorker1.ReportProgress(0)
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
ShowSOmeMEssage(SomeStatus)
End Sub
FYI: The progress thing is only so you can call a routine on the main UI thread. You can set as many thread-safe variables as you want on the form from within the backgroundworker. It's just updating the UI that's bothersome, though there is a workaround for that too.

Related

vb.net subroutine not updating module level variable until exit sub

I have come to learn that a module level variable's value will not be altered until a sub routine that changed it exits.
StopBackgroundWorker1 = True
Thread.Sleep(1500)
If BackgroundWorker1Complete = False Then
Exit Sub
End If
in this example, I added a long delay for testing. I'm simply trying to stop and start a background worker safely with vb 2017 new background worker class.
The example above with "StopBackgroundWorker1 = True", I was hoping to stop the worker at a safe place and then continue within that sub with other code.
But what is happening is that the "StopBackgroundWorker1 = True" is not being set "True" until the sub exits.
There must be another way to do what I am trying to do, please help
Ok here is a complete example,
Imports System.ComponentModel
Imports System.Threading
Public Class Form1
Private flag As Boolean = False
Dim Completed As Boolean = False
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
flag = True
'Do
' do loop never see's a true flag
'Loop Until Completed
Thread.Sleep(500)
If Completed = True Then
Label1.BackColor = Color.Red
End If
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Do
Thread.Sleep(25)
Loop Until flag
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As
RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
Completed = True
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles
Button2.Click
Label1.Text = flag.ToString
End Sub
End Class
Now the concept is if you hit button1 and wait for background worker to complete, it should turn lable1 red. but it doesn't. The do loop looking for a true flag will spin forever locking the form up.
I have determined with this example that the flag is not set to true until you exit the sub. Hit Button1 again and lable1 turns red.
Thanks in advance for any answers.
This doesn't answer your question per se but I want to post a large code snippet so I'll post it as an answer. It demonstrates that what you think is the problem is not the problem, i.e. that a field's value changes as soon as you change it, even if that change is made from a BackgroundWorker.DoWork event handler.
Create a new Windows Forms application project, add a Button, a Label and a BackgroundWorker to your form and then paste in this code over the default code of the form:
Imports System.ComponentModel
Imports System.Threading
Public Class Form1
Private flag As Boolean
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
BackgroundWorker1.WorkerReportsProgress = True
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Label1.Text = flag.ToString()
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Thread.Sleep(5000)
flag = True
BackgroundWorker1.ReportProgress(0)
Thread.Sleep(5000)
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Label1.BackColor = Color.Green
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
Label1.BackColor = Color.Red
End Sub
End Class
Run the project and, when the form appears, start clicking the Button at a pace of a few times per second. You'll see that the value of the flag field, as displayed on the Label, changes from False to True as soon as the code to set it is executed in the DoWork event handler. The Label will turn green when that happens, so it's easy to spot. You'll know that it didn't wait until the DoWork event handler completes because the Label will turn red at that point.
EDIT: Now that you have provided all the relevant information, the issue is obvious. As I have already said, the moment you set a variable, that is the value of that variable. There's no waiting because there cannot be any waiting because there's nowhere to store a temporary value for the variable.
The reason that it looks otherwise is that your test code is faulty. If you use the debugger then you will see how. When you use a BackgroundWorker, the DoWork event handler is executed on a secondary thread but the RunWorkerCompleted event handler is executed on the UI thread. That means that your DoWork event handler can execute at the same time as your Click event handler for Button1 because they are on different threads, but the RunWorkerCompleted event handler cannot run at the same time, so it has to wait until the Click event handler completes before it can be executed. That means that the code to set the Completed field doesn't get executed until the Click event handler completes. It's not that the field value doesn't change when it's set but rather that it doesn't actually get set. If you place breakpoints on the two lines that access that Completed field then you'll see that.
The mistake you're making is trying to do something in that Click event handler after the DoWork event handler completes. That's wrong. That's exactly what the RunWorkerCompleted event handler is for. That's where you do UI work after the background work completes.
Also, you can get rid of that flag variable. Cancellation functionality is built into the BackgroundWorker class. Look at the CancelAsync method and the CancellationPending property.
Many thanks to "jmcilhinney" for his insights! I have figured out the code I was looking for!
This code allows me to stop and start a background thread safely by allowing the background thread to finish completely before restarting.
During the time that the background thread is stopped, user actions can perform operations without the worry of cross-threading or with thread.abort garbled code conditions.
Finally, stress-free threading!
I wish I could find the doc on MSDN that I read that stated the await async method was far superior to task.run but that's another argument.
This may not be the best code in the world but it works!
And in light of trying to rewrite all code in my project with async and await I'll stick with this!
Public Class Form1
Private flag As Boolean = False
Dim Completed As Boolean = False
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
flag = True
Await Task.Run(Sub()
Do
Loop Until Completed
End Sub)
If Completed = True Then
Label1.BackColor = Color.Red
End If
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Do
Thread.Sleep(250)
Loop Until flag
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As
RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
Completed = True
End Sub
End Class
For all of the academics out there this code is more appropriate.
Imports System.ComponentModel
Imports System.Threading
Public Class Form1
Private Completed As Boolean = False
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
BackgroundWorker1.WorkerSupportsCancellation = True
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles
Button1.Click
BackgroundWorker1.CancelAsync()
Await Task.Run(Sub()
Do
Loop Until Completed
End Sub)
If Completed = True Then
Label1.BackColor = Color.Red
End If
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Do
Thread.Sleep(250)
Loop Until BackgroundWorker1.CancellationPending
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
Completed = True
End Sub
End Class

Changing a Control's property via a delegate

First off, pardon me if my English is bad, I'm not a native English speaker.
I'm fairly new to programming and I'm trying to teach myself VB.NET
I came across a problem while trying to learn about Delegates. (see code below)
What I'm trying to accomplish is to update a specified Control's text property via a thread. However, as soon as I start the thread, I get an ArgumentException Error. I have completely no idea what's wrong. Anybody have an idea what i've done wrong here?
Public Class Form1
Delegate Sub myDelegate1(ByVal s_Name As Control, ByVal s_txt As String)
Public txtUpdate As New myDelegate1(AddressOf upd_ControlTextProperty)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = vbnullstring
End Sub
Private Sub upd_ControlTextProperty(ByVal ControlName As Control, ByVal txt As String)
ControlName.Text = txt
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim thread1 As New Threading.Thread(AddressOf threadstart)
thread1.IsBackground = True
thread1.Start()
End Sub
Private Sub threadstart()
Me.Invoke(Me.txtUpdate, New Object(), {Label1, "This is Label 1"})
End Sub
End Class
As TheValyreanGroup said, your delegate is supposed to accept two arguments, and you pass it three :
Me.Invoke(Me.txtUpdate, New Object(), {Label1, "This is Label 1"})
^-1--------^ ^-2--------^ ^-3-----------------------^
So just remove the New Object() thing, and transform this {Label1, ...} into just a string :
Me.Invoke(Me.txtUpdate, "This is Label 1")
OK Better that way.
On a second hand, what you are doing is not very usefull.
You create a new Thread from your UI Thread.
With this new Thread, you invoke back the UI Thread and you stop your Thread...
Remember that a Control can be updated only by the Thread who created the Form (the UI thread).
Unless you have a good reason to work with your background thread, you can resume your code to :
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = vbnullstring
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Label1.Text = "This is Label 1"
End Sub
End Class
UPDATE
(from comments)
To make it more clear, here is a schema (that I took on https://androidkennel.org/android-networking-tutorial-with-asynctask/, if any restrictions apply I will remove the image)
The Main UI Thread is used for things :
React to user events (clicks, inputs...) and start background threads that will do the process
Update the User Interface when the background thread is over or during the task.
When I say what you're doing is not usefull is because your background thread does not do any processing, it just signals the UI thread to update the UI...
I would try this approach. upd_ControlTextProperty can be called successfully either from the UI thread or your new thread.
Public Class Form1
Delegate Sub myDelegate1(ByVal s_Name As Control, ByVal s_txt As String)
Public txtUpdate As New myDelegate1(AddressOf upd_ControlTextProperty)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = ""
End Sub
Private Sub upd_ControlTextProperty(ByVal ControlName As Control, ByVal txt As String)
If Me.InvokeRequired = True Then
Me.Invoke(txtUpdate, New Object() {ControlName, txt})
Else
ControlName.Text = txt
End If
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim thread1 As New Threading.Thread(AddressOf threadstart)
thread1.IsBackground = True
thread1.Start()
End Sub
Private Sub threadstart()
upd_ControlTextProperty(Label1, "This is Label 1")
End Sub
End Class

How to wait for BackgroundWorker to finish without killing ProgressBar?

The application is doing a lot more than this, but I have narrowed down the issue with the example below.
When bgwDone.WaitOne() is commented out, the progress bar works fine, cancel button is effective, but execution continues before the background process is complete.
When bgwDone.WaitOne() is applied, the ProgressForm is visible but not enabled, so processing cannot be cancelled and progress bar does not refresh, and the most confusing part, Msgbox("1") does not execute. I only see Msgbox("2") after the background worker finishes. I am utterly perplexed.
Imports System.ComponentModel
Public Class Form1
Private WithEvents bgw As BackgroundWorker
Private Event bgwCancelled()
Private bgwDone As New System.Threading.AutoResetEvent(False)
'Allows ProgressForm to cancel execution
Public Sub bgwCancelAsync()
RaiseEvent bgwCancelled()
End Sub
Private Sub bgw_Cancelled_by_ProgressForm() Handles Me.bgwCancelled
bgw.CancelAsync()
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Cursor = Cursors.WaitCursor
bgw = New BackgroundWorker
bgw.WorkerReportsProgress = True
bgw.WorkerSupportsCancellation = True
If bgw.IsBusy = False Then
ProgressForm.Show()
bgw.RunWorkerAsync(10)
End If
'********THIS LINE: bgwDone.WaitOne() MAKES A BIG DIFFERENCE*******
bgwDone.WaitOne()
MsgBox("1")
MsgBox("2")
Cursor = Cursors.Default
End Sub
'BackgroundWorker.RunWorkerAsync raises the DoWork event
Private Sub bgw_DoWork(sender As Object, e As DoWorkEventArgs) Handles bgw.DoWork
Dim numToDo As Integer = CInt(e.Argument)
For n As Integer = 1 To numToDo
If bgw.CancellationPending Then
Exit For
End If
System.Threading.Thread.Sleep(200)
bgw.ReportProgress(n * 10)
Next
bgwDone.Set()
End Sub
'ReportProgress raises the ProgressChanged event
Private Sub bgw_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles bgw.ProgressChanged
ProgressForm.UpdateProgress(e.ProgressPercentage)
End Sub
Private Sub bgw_RunWorkerCompleted(sender As Object,
e As RunWorkerCompletedEventArgs) Handles bgw.RunWorkerCompleted
ProgressForm.Close()
End Sub
And my form with the ProgressBar:
Public Class ProgressForm
Private Sub ButtonCancel_Click(sender As Object, e As EventArgs) Handles ButtonCancel.Click
Form1.bgwCancelAsync()
End Sub
Public Sub UpdateProgress(pct As Integer)
ProgressBar1.Value = pct
ProgressBar1.Refresh()
End Sub
End Class
I am not sure what you are trying to accomplish. But it almost seems like some of your code is trying to defeat the purpose of a BackGroundWorker:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Cursor = Cursors.WaitCursor
bgw = New BackgroundWorker
...
If bgw.IsBusy = False Then
ProgressForm.Show()
bgw.RunWorkerAsync(10)
End If
bgwDone.WaitOne()
MsgBox("1")
MsgBox("2")
Cursor = Cursors.Default
End Sub
The purpose of a BackgroundWorker is to do some long running task on another thread and leave the UI responsive. I am not sure that a task that only "takes several seconds" qualifies as a long running task.
Given that, why use the WaitCursor while the BGW runs? The point to leaving the UI resposive is to allow the user to do other things in the meantime.
The test for bgw.IsBusy can never, ever be true - you just created it 3 lines earlier. Click the button again and you will create another BGW.
The rest of the code in the click looks like you want or expect the code to continue on the next line after the BGW completes. That's not how it works.
If the app cannot continue without those tasks being completed, disable anything that lets the user go elsewhere until the worker completes or:
Forego the worker and put the form in wait mode (Me.UseWaitCursor) until the stuff is loaded. This doesn't rule out a ProgressBar.
A dedicated Progress Form can make sense in cases where the app will use various workers at various times. A StatusBar can contain a ProgressBar and is much more subtle (and perhaps appropriate since it is a status element).
So, revised and using a form instance for the progress reporter:
MainForm
Private WithEvents bgw As BackgroundWorker
Private frmProg As ProgressForm
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
bgw = New BackgroundWorker
End Sub
Private Sub btnLoadAll_Click(sender As Object, e As EventArgs) Handles btnLoadAll.Click
bgw.WorkerReportsProgress = True
bgw.WorkerSupportsCancellation = True
If bgw.IsBusy = False Then
' create ProgressForm instance if needed
If frmProg Is Nothing Then frmProg = New ProgressForm
frmProg.Show()
bgw.RunWorkerAsync(78)
End If
btnLoadAll.Enabled = False
End Sub
Private Sub bgw_DoWork(sender As Object, e As DoWorkEventArgs) Handles bgw.DoWork
' multiple workers can use the same event
Dim thisWorker = DirectCast(sender, BackgroundWorker)
Dim count = Convert.ToInt32(e.Argument)
For n As Integer = 1 To count
If thisWorker.CancellationPending Then
Exit For
End If
' Fake work:
System.Threading.Thread.Sleep(50)
' dont assume the size of the job if
' there are multiple BGW or tasks
thisWorker.ReportProgress(Convert.ToInt32((n / count) * 100))
Next
End Sub
Private Sub bgw_ProgressChanged(sender As Object,
e As ProgressChangedEventArgs) Handles bgw.ProgressChanged
frmProg.UpdateProgress(e.ProgressPercentage)
End Sub
Private Sub bgw_RunWorkerCompleted(sender As Object,
e As RunWorkerCompletedEventArgs) Handles bgw.RunWorkerCompleted
If e.Error IsNot Nothing Then
'... ToDo
ElseIf e.Cancelled Then
'... ToDo
Else
frmProg.Close()
' avoid 'cannot access disposed object':
frmProg = Nothing
Me.btnNextStep.Enabled = True
btnLoadAll.Enabled = True
End If
End Sub
Rather than enabling a "Next" button, the app could automatically proceed. It depends on the app.

VB.NET: Passing variable between forms, error

I want to pass the value of an exact variable to another form...
To be more precise I'm building a game in which a player answer a few questions so I want to pass the variable that contains the correct answer to another form which works as the end game window and displays the score... I'm using this kind of code in the end game window:
Public Class Form2
Public Property CCount As Integer
Private Sub Form5_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = CCcount
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
CCount = 0
Form1.Show()
Me.Close()
End Sub
End class
And this in the main form:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'Lines of code
Form2.CCount +=1
'Lines of code
'Me.Hide()
'Form2.Show()
EndSub
The first time that I'm "playing the game" everything works fine, but if try to replay it the displaying scores never changes... It remains the same as the first run..
Any ideas how I can fix this out?
You assign value of CCount to the Label only ones when form was loaded.
Load event raised only one time.
Put label updating code inside CCount Property Setter
Private _CCount As Integer
Public Property CCount As Integer
Get
Return _CCount
End Get
Set (value As Integer)
If _CCount = value Then Exit Property
_CCount = value
Me.Label1.Text = _CCount.ToString()
End Set
End Property

BackgroundWorker.ReportProgress exception if run in another module

My BackgroundWorker works perfectly in my main form frmMain. But when I run the ReportProgress method in another module, I get exception "This BackgroundWorker states that it doesn't report progress. Modify WorkerReportsProgress to state that it does report progress." This IS set to report progress; this works fine when run the same way in the main module.
Basically, from a module called by my BackgroundWorker, I want to show progress on my main form.
How can I fix this? The only idea I have is to move the code from the module into my main form, but this seems a backward step, which would involve extra work. Am hoping there are easier ways!
Calling code in class frmMain:
Friend WithEvents BackgroundWorker As New System.ComponentModel.BackgroundWorker
Private Sub btnTest_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnTest.Click
' Specify that we do NOT want the background operation to allow cancellation
BackgroundWorker.WorkerSupportsCancellation = False
' Specify that we want the background operation to report progress.
BackgroundWorker.WorkerReportsProgress = True
' Start running the background operation by calling the RunWorkerAsync method.
BackgroundWorker.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker_DoWork(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker.DoWork
Dim result As Boolean
result = MyTest()
End Sub
Private Sub BackgroundWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker.ProgressChanged
Me.Text = e.ProgressPercentage.ToString() & "%"
sspStatus.Text = e.UserState.ToString
End Sub
Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles BackgroundWorker.RunWorkerCompleted
If e.Cancelled = True Then
' The background operation was cancelled
Me.Text = "Cancelled!"
ElseIf e.Error IsNot Nothing Then
' The background operation encountered an error
Me.Text = "Error: " & e.Error.Message
Else
' The background operation completed successfully
Me.text = "Done!"
End If
End Sub
Code which generates the exception in separate module Invoices:
Public Function MyTest() As Boolean
frmMain.BackgroundWorker.ReportProgress(0)
End Function
Am using VB.NET in VS 2010, with .NET 3.5.
Try to set it up as
Public Function MyTest(worker as BackgroundWorker) As Boolean
worker.ReportProgress(0)
End Function
to make sure you are talking to the right worker instance.
(And aside: avoid using classnames for instance fields).