I've managed to get the data I want in a thread, however I'm having trouble getting my head around passing the data back into my main thread (GUI).
I'm grabbing data from a network stream in a thread, then need to pass it to my main thread in order to pass to other classes etc.
I've seen it mentioned to use backgroundworker for this, but as I'm expecting it to gather data periodically and never stop, I thought a separate thread for this would be best, but I'm very new to multithreading.
If threading is the correct way to go, how can I pass data from it back to my main thread in order to use this for other stuff? I've seen delegates and events mentioned a lot but can't see how I'd pass data with these?
Thanks
Please study this example, and let me know if it fits your requirements:
Controls required: lstItems (ListBox), btnStart (Button), btnStop (Button), Timer1 (Timer).
Form1 code:
Public Class Form1
Dim p_oStringProducer As StringProducer
Private Sub btnGo_Click(sender As Object, e As EventArgs) Handles btnGo.Click
p_oStringProducer = New StringProducer
p_oStringProducer.Start()
Timer1.Enabled = True
End Sub
Private Sub btnStop_Click(sender As Object, e As EventArgs) _
Handles btnStop.Click
Timer1.Enabled = False
p_oStringProducer.Stop()
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Dim asQueue As Concurrent.ConcurrentQueue(Of String) =
p_oStringProducer.MessageQueue
While asQueue.Count > 0
Dim sItem As String = Nothing
asQueue.TryDequeue(sItem)
lstItems.Items.Add(sItem)
End While
End Sub
End Class
StringProducer code:
Imports System.Threading.Tasks
Public Class StringProducer
Private p_fKeepRunning As Boolean
Private p_oTask As task
Private p_aMessageQueue As Concurrent.ConcurrentQueue(Of String)
Private p_iNextMessageId As Integer
Public ReadOnly Property MessageQueue As _
Concurrent.ConcurrentQueue(Of String)
Get
Return p_aMessageQueue
End Get
End Property
Sub New()
p_oTask = New Task(AddressOf TaskBody)
p_aMessageQueue = New Concurrent.ConcurrentQueue(Of String)
p_iNextMessageId = 0
End Sub
Public Sub Start()
p_fKeepRunning = True
p_oTask.Start()
End Sub
Public Sub [Stop]()
p_fKeepRunning = False
End Sub
Private Sub TaskBody()
While p_fKeepRunning
Threading.Thread.Sleep(2000)
p_aMessageQueue.Enqueue("Message #" & p_iNextMessageId)
p_iNextMessageId += 1
End While
End Sub
Protected Overrides Sub Finalize()
MyBase.Finalize()
Me.Stop()
End Sub
End Class
This was not extensively tested, but it should give you a head start.
Related
I have three forms in total and i want to trigger one of the button on form 3 to be triggered automatically when form 1 loaded
Form 1
Public Class frmIOMain
' In This Form load I want to trigger the above mentioned button
Private Sub IOMain_Load(sender As Object, e As System.EventArgs) Handles Me.Load
' I want to Trigger the Above mentioned button here when my form is loaded
' But it is not working for me
frmUpdateDueDates.cmdUpdate_Click(Nothing, Nothing)
End Sub
End Class
Form 2
Public Class TestEquipmentManagement
Public EquipementTable As New DataTable("EquipmentTable")
Public EquiTypeSelection As String
Public EquiManufacturerSelection As String
Public EquiList_PK As New List(Of Integer)
Dim targetEquipmentList As New List(Of Model.equipment)
Private equipDB As Model.Entities = Nothing
Public Shared viewManager As ViewManager
Private equipment As New List(Of Model.equipment)
'Dim WithEvents excNewPFM As New VBACom
Public EquipCalTable As New DataTable("EquipCalTable")
Public Sub New()
Dim todayplusoneyear As Date
todayplusoneyear = Date.Today
todayplusoneyear = todayplusoneyear.AddYears(1)
'Assign current db
equipDB = frmIOMain.db
End Sub
End Class
Form 3
Public Class frmUpdateDueDates
Private EquipmentUpdates As UpdateCalibrationsViewModel
Private _success As Boolean = False
Public Sub New(db As Entities)
' Dieser Aufruf ist für den Designer erforderlich.
InitializeComponent()
EquipmentUpdates = New UpdateCalibrationsViewModel(db, New CAQ23(), False)
'Add Handlers
AddHandler EquipmentUpdates.OnProgressChanged, AddressOf progressChangedHandler
AddHandler EquipmentUpdates.OnInfotextChanged, AddressOf infoTextChangedHandler
prgUpdates.Maximum = EquipmentUpdates.intProgressMax
End Sub
Public Sub cmdUpdate_Click(sender As Object, e As EventArgs) Handles cmdUpdate.Click
cmdUpdate.Enabled = False
_success = EquipmentUpdates.startUpdating()
cmdCancel.Text = "Close"
End Sub
End Class
I want "cmdUpdate_Click" Button which is on form 3 to be triggered when my form 1 is loaded
Can Anyone tell me how i can do that?
Firstly, create an instance of the form, instead of using its default form instance. Calling a click handler across forms isn't a good idea. The handler may use the arguments sender As Object, e As EventArgs and from outside of the containing class, you can't assume you know that. Better practice would be to create a method which performs the click within the form, such as
Public Class frmUpdateDueDates
Public Sub cmdUpdateClick()
cmdUpdate.PerformClick()
End Sub
Private Sub cmdUpdate_Click(sender As Object, e As EventArgs) Handles cmdUpdate.Click
cmdUpdate.Enabled = False
_success = EquipmentUpdates.startUpdating()
cmdCancel.Text = "Close"
End Sub
End Class
Public Class frmIOMain
Private myFrmUpdateDueDates As frmUpdateDueDates
Private Sub IOMain_Load(sender As Object, e As System.EventArgs) Handles Me.Load
myFrmUpdateDueDates = New FrmUpdateDueDates()
myFrmUpdateDueDates.Show()
'myFrmUpdateDueDates.cmdUpdate_Click(Nothing, Nothing)
myFrmUpdateDueDates.cmdUpdateClick()
End Sub
End Class
And you can change the access modifier of the click handler back to Private
Even better would be to put the work into a different method which the click handler calls. Then the other form doesn't even need to know the button exists. Such as
Public Class frmUpdateDueDates
Public Sub DoUpdating()
cmdUpdate.Enabled = False
_success = EquipmentUpdates.startUpdating()
cmdCancel.Text = "Close"
End Sub
Private Sub cmdUpdate_Click(sender As Object, e As EventArgs) Handles cmdUpdate.Click
DoUpdating()
End Sub
End Class
Public Class frmIOMain
Private myFrmUpdateDueDates As frmUpdateDueDates
Private Sub IOMain_Load(sender As Object, e As System.EventArgs) Handles Me.Load
myFrmUpdateDueDates = New FrmUpdateDueDates()
myFrmUpdateDueDates.Show()
'myFrmUpdateDueDates.cmdUpdate_Click(Nothing, Nothing)
myFrmUpdateDueDates.DoUpdating()
End Sub
End Class
Have anyone idea why my code return "" inside my Textbox ? :-)
This i have in main Class
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button2.Click
Dim My_Thread as Threading.Thread
My_Thread = New Threading.Thread(AddressOf Module1.MyTest)
My_Thread.Start()
End Sub
And this in module1
Sub MyTest()
Dim TestingValue as string = "Test"
MainForm.Textbox1.Text = TestingValue
End sub
Invoke all time crash code and another try return "" inside texbox1 :-/
Create a Sub Class with public declarations to any object you want on MainForm, then pass the class as a parameter to the module. This would be a cleaner approach than passing the entire Form class instance. Then using the method that Jimi has suggested you can setyou textboxs without cross thread violation.
Public Class MainForm
Public Class PassToModule
Public TxBx1 As TextBox = MainForm.TextBox1
Public TxBx2 As TextBox = MainForm.TextBox2
End Class
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim PassToModule As New PassToModule
Dim My_Thread As Threading.Thread
My_Thread = New Threading.Thread(AddressOf MyTest)
My_Thread.Start(PassToModule)
End Sub
End Class
Module Module1
Dim FromMainForm As MainForm.PassToModule
Sub MyTest(PasToModule As MainForm.PassToModule)
FromMainForm = PasToModule
FromMainForm.TxBx1.BeginInvoke(New MethodInvoker(Sub()
FromMainForm.TxBx1.Text = "Test"
FromMainForm.TxBx2.Text = "Test"
End Sub))
End Sub
End Module
I am trying to use a single handler to cover the end of multiple backgroundworker activities and cannot find a way to get the information about the specific backgroundworker using the backgroundworkercompleted event.
My code to catch the event is as below:
Private Sub BGx_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted, BackgroundWorker2.RunWorkerCompleted, BackgroundWorker3.RunWorkerCompleted, BackgroundWorker4.RunWorkerCompleted, BackgroundWorker5.RunWorkerCompleted, BackgroundWorker6.RunWorkerCompleted, BackgroundWorker7.RunWorkerCompleted, BackgroundWorker8.RunWorkerCompleted
'Do work here based on completed Backgroundworker
For BG = 1 To 8
If Not DSWorkers(BG).IsBusy Then
If DStatus(BG) = -2 Then : DStatus(BG) = -1 : End If
End If
Next
Complete()
End Sub
There is nothing on the "Do Work Here" section because I do not know how to capture and have been unable to find details of the backgroundworkercompleted event id.
Please - any pointers as to how I can identify the specific completed BackgroundWorker
As with all event handlers, the sender parameter is a reference to the object that raised the event, so you can access the actual BackgroundWorker that has completed its work via that. If you need some data other than that, you assign it to the e.Result property in the DoWork event handler and get it back from the e.Result property in the RunWorkerCompleted event handler. e.Result works for getting data out of the DoWork event handler much as e.Argument works for getting data in.
Check this out for some examples of using BackgroundWorker objects, including passing data out using e.Result. You might also like to checkout my own BackgroundMultiWorker class, which basically combines the functionality of multiple BackgroundWorker objects into a single BackgroundMultiWorker object. It identifies each task using a token.
EDIT:
Here's an example that may help with this issue and your task in general:
Imports System.ComponentModel
Imports System.Threading
Public Class Form1
Private ReadOnly resultsByWorker As New Dictionary(Of BackgroundWorker, BackgroundWorkerResult)
Private ReadOnly rng As New Random
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'The NumericUpDown is used to select the index of a BackgroundWorker to cancel.
With NumericUpDown1
.DecimalPlaces = 0
.Minimum = 0
.Maximum = 9
End With
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'Create 10 BackgroundWorkers and run them.
For i = 1 To 10
Dim worker As New BackgroundWorker
resultsByWorker.Add(worker, New BackgroundWorkerResult)
AddHandler worker.DoWork, AddressOf workers_DoWork
AddHandler worker.RunWorkerCompleted, AddressOf workers_RunWorkerCompleted
worker.WorkerSupportsCancellation = True
worker.RunWorkerAsync()
Next
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Dim index = Convert.ToInt32(NumericUpDown1.Value)
Dim worker = resultsByWorker.Keys.ToArray()(index)
If worker.IsBusy Then
'Cancel the BackgroundWorker at the specified index.
worker.CancelAsync()
End If
End Sub
Private Sub workers_DoWork(sender As Object, e As DoWorkEventArgs)
Dim worker = DirectCast(sender, BackgroundWorker)
'Do work for a random number of seconds between 10 and 20.
Dim period = rng.Next(10, 20 + 1)
For i = 0 To period
If worker.CancellationPending Then
e.Cancel = True
Return
End If
'Simulate work.
Thread.Sleep(1000)
Next
'The work was completed without being cancelled.
e.Result = period
End Sub
Private Sub workers_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
Dim worker = DirectCast(sender, BackgroundWorker)
Dim result = resultsByWorker(worker)
If e.Cancelled Then
result.WasCancelled = True
Else
result.Result = CInt(e.Result)
End If
Dim workers = resultsByWorker.Keys.ToArray()
If Not workers.Any(Function(bgw) bgw.IsBusy) Then
'All work has completed so display the results.
Dim results As New List(Of String)
For i = 0 To workers.GetUpperBound(0)
worker = workers(i)
result = resultsByWorker(worker)
results.Add($"Worker {i} {If(result.WasCancelled, "was cancelled", $"completed {result.Result} iterations")}.")
Next
MessageBox.Show(String.Join(Environment.NewLine, results))
End If
End Sub
End Class
Public Class BackgroundWorkerResult
Public Property WasCancelled As Boolean
Public Property Result As Integer
End Class
Here is that example reworked to use a single instance of the BackgroundMultiWorker is linked to instead of multiple instances of the BackgroundWorker class.
Imports System.Threading
Public Class Form1
Private WithEvents worker As New BackgroundMultiWorker With {.WorkerSupportsCancellation = True}
Private ReadOnly results(9) As BackgroundWorkerResult
Private ReadOnly rng As New Random
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'The NumericUpDown is used to select the index of a BackgroundWorker to cancel.
With NumericUpDown1
.DecimalPlaces = 0
.Minimum = 0
.Maximum = results.GetUpperBound(0)
End With
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'Create 10 BackgroundWorkers and run them.
For i = 0 To results.GetUpperBound(0)
results(i) = New BackgroundWorkerResult
worker.RunWorkerAsync(i)
Next
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Dim index = Convert.ToInt32(NumericUpDown1.Value)
If worker.IsBusy(index) Then
'Cancel the BackgroundWorker at the specified index.
worker.CancelAsync(index)
End If
End Sub
Private Sub worker_DoWork(sender As Object, e As DoWorkEventArgs) Handles worker.DoWork
'Do work for a random number of seconds between 10 and 20.
Dim period = rng.Next(10, 20 + 1)
For i = 0 To period
If worker.IsCancellationPending(e.Token) Then
e.Cancel = True
Return
End If
'Simulate work.
Thread.Sleep(1000)
Next
'The work was completed without being cancelled.
e.Result = period
End Sub
Private Sub workers_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles worker.RunWorkerCompleted
Dim result = results(CInt(e.Token))
If e.Cancelled Then
result.WasCancelled = True
Else
result.Result = CInt(e.Result)
End If
If Not worker.IsBusy() Then
'All work has completed so display the results.
Dim output As New List(Of String)
For i = 0 To results.GetUpperBound(0)
result = results(i)
output.Add($"Task {i} {If(result.WasCancelled, "was cancelled", $"completed {result.Result} iterations")}.")
Next
MessageBox.Show(String.Join(Environment.NewLine, output))
End If
End Sub
End Class
Public Class BackgroundWorkerResult
Public Property WasCancelled As Boolean
Public Property Result As Integer
End Class
I wrote very simple thread example.
Make normal form1 and drop 1 textbox
run thread work on form load
thread change a textbox backcolor looks like switch on/off
But, it doesn't work.....
Can you tell me why doesn't work??
see my source.
=====================================================================
Imports System.Threading
Public Class Monitor
Public wObj As Worker = New Worker()
Public MyThread As Thread = New Thread(AddressOf wObj.DoWork)
Public Sub ChangeTBColor(pOption As Integer)
If pOption = 1 Then
tb1.BackColor = Color.Aqua
Else
tb1.BackColor = Color.Red
End If
End Sub
Private Sub Monitor_Load(sender As Object, e As EventArgs) Handles Me.Load
MyThread.Start()
Console.WriteLine("Running OrgThread..")
Console.WriteLine("Stop running")
End Sub
Private Sub BtnThreadStop_Click(sender As Object, e As EventArgs) Handles BtnThreadStop.Click
Me.wObj.RequestStop()
End Sub
End Class
Public Class Worker
Private LoopStop As Boolean = True
Public Sub DoWork()
Console.WriteLine("User Thread Start!")
Dim iTemp As Integer = 0
While (LoopStop)
Monitor.ChangeTBColor(iTemp Mod 2)
iTemp = iTemp + 1
Thread.Sleep(500)
End While
Console.WriteLine("User Thread End.!")
End Sub
Public Sub RequestStop()
LoopStop = False
End Sub
End Class
As already mentioned, your starting a new thread for your work, the issue is your trying to change the color for a control that need invoking. With this said, we need a delegate for when the control needs to be invoked... In my example, I used one class that handles this all and works great. Also please make sure to import System.ComponentModel because this is needed for the BackgroundWorker... I used the background worker as it takes all the heavy lifting off you would need...
Imports System.ComponentModel
Imports System.Threading
Public Class Monitor
Delegate Sub SetColor(ByVal clr As Integer) 'Your delegate..
Private WithEvents bw As New BackgroundWorker
Public Sub ChangeTBColor(pOption As Integer)
If Me.tb1.InvokeRequired Then 'Invoke if required...
Dim d As New SetColor(AddressOf ChangeTBColor) 'Your delegate...
Me.Invoke(d, New Object() {pOption})
Else
If pOption = 1 Then
tb1.BackColor = Color.Aqua
Else
tb1.BackColor = Color.Red
End If
End If
End Sub
Private Sub Monitor_Load(sender As Object, e As EventArgs) Handles Me.Load
bw.WorkerSupportsCancellation = True
Console.WriteLine("Running OrgThread..")
bw.RunWorkerAsync()
End Sub
Private Sub bw_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles bw.DoWork
Dim iTemp As Integer = 0
Dim LoopStop As Boolean = True
Console.WriteLine("User Thread Start!")
While (LoopStop)
If Not (bw.CancellationPending) Then
ChangeTBColor(iTemp Mod 2)
iTemp = iTemp + 1
Thread.Sleep(500)
Else
e.Cancel = True
LoopStop = False
End If
End While
End Sub
Private Sub bw_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bw.RunWorkerCompleted
Console.WriteLine("User Thread End.!")
End Sub
Private Sub BtnThreadStop_Click(sender As Object, e As EventArgs) Handles BtnThreadStop.Click
If bw.IsBusy Then
bw.CancelAsync()
Else
Console.WriteLine("Running OrgThread..")
bw.RunWorkerAsync()
End If
End Sub
End Class
Here's my screenshot of my test... This is tried and tested. Please be sure to vote if this helps you!
I'm having trouble getting a progress bar to update. I'd be okay just with a moving marquee bar. Basically, I'm reading a database routine in a SqliteReader.vb class. I'm new to visual basic, and I'm sure I need to use the worker_DoWork routine, but I'm not sure how to expose my variables coming from Form1: graphData, graphComputations, m_debug to the worker_DoWork sub. How is this usually done?
Public Class SqliteReader
Public Sub ReadDataBase
End Sub
End Class
This is updating a graph (zedgraph element) on the main form, Form1.vb. I call the progressbar from the main form like this:
ProgressBar.Initialize(channelArray, computationArray, m_debug)
ProgressBar.vb below:
Partial Public Class ProgressBar
Dim DataAcquisition As New SqliteReader
Dim WithEvents worker As New BackgroundWorker
Public Sub Initialize(ByRef graphData As Channels(), ByRef graphComputations As Computations(), ByVal m_debug As Integer)
DataAcquisition = SqliteReader.GetInstance()
Me.Show()
Me.Update()
Dim Update_Thread As Thread(AddressOf Update_ThreadExecute)
Update_Thread.Priority = ThreadPriority.Normal
Update_Thread.Start()
DataAcquisition.ParseEntireDatabase(graphData, graphComputations, m_debug)
Me.Close()
End Sub
Private Sub ProgressBarStart(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
DataAcquisition = SqliteReader.GetInstance()
progress.Style = ProgressBarStyle.Marquee
worker.WorkerReportsProgress = True
worker.WorkerSupportsCancellation = True
worker.RunWorkerAsync()
End Sub
Private Sub worker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles worker.DoWork
Dim worker As BackgroundWorker = DirectCast(sender, BackgroundWorker)
DataAcquisition = SqliteReader.GetInstance()
' I probably need
' DataAcquisition.ParseEntireDatabase(graphData, graphComputations, m_debug)
' here... but how do I expose graphdata, graphcomputations and m_debug to this sub?
End Sub
Private Sub worker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles worker.ProgressChanged
dataProgress = CInt(((DataAcquisition.currentRow + 1) / DataAcquisition.totalRows) * 100)
progress.Value = dataProgress
End Sub
RunWorkerAsync has a second version that takes a parameter. You can use that to pass any values (or references) your worker needs.
That said, you shouldn't be updating form elements from inside the worker. Your worker should fire the ProgressChanged event when you want the UI to update, and you handle it there. That one also has a version that can send a value back. (Or many values if you send back an array, list, or custom class.)
The last step in this is that you need to actually fire ProgressChanged. DataAcquisition.ParseEntireDatabase may not do that, in which case using it won't allow this method to work.
If graphData, graphComputations, m_debug are already members of ProgressBar and worker_DoWork is a member of ProgressBar, then you have nothing more to do. You should be able to access them directly.
For Rapunzo, above.. My Final Solution was this:
Partial Public Class ProgressBar
Dim _mDataAcquisition As New SqliteReader
Public Property DataProgress As Integer = 0
Dim WithEvents _mProgressWorker As New BackgroundWorker
Public Sub Initialize(ByRef graphData As List(Of Channels), ByRef auxData As List(Of Channels), _
ByRef graphComputations As List(Of Computations))
_mDataAcquisition = SqliteReader.GetInstance()
Show()
Update()
_mDataAcquisition.ParseEntireDatabase(graphData, auxData, graphComputations)
Close()
End Sub
Private Sub ProgressBarStart(ByVal sender As System.Object, ByVal e As EventArgs) Handles MyBase.Load
progress.Style = ProgressBarStyle.Blocks
_mProgressWorker.WorkerReportsProgress = True
_mProgressWorker.WorkerSupportsCancellation = True
_mProgressWorker.RunWorkerAsync()
progress.Visible = True
progress.Maximum = 100
progress.Value = 0
End Sub
Public Sub WorkerProgressChanged()
progress.Value = DataProgress
Invalidate()
End Sub
Private Sub WorkerRunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles _mProgressWorker.RunWorkerCompleted
progress.Visible = False
progress.Value = 0
Close()
End Sub
From there, just call ProgressBar.Initialize to start it
To update:
ProgressBar.DataProgress = CInt((currentIt / totalIt) * 100)
ProgressBar.WorkerProgressChanged()
and to end:
ProgressBar.DataProgress = 100
ProgressBar.WorkerProgressChanged()
Hope this helps.