Multi-Threading (Calling Sub From WorkerThread) - vb.net

Need to call a sub that is coded written inside the block of form1 form an external worker thread. This is what I have written:
In Form1:
Public Delegate Sub UpdateControlDelegate(ByVal C As Label, ByVal txt As String)
Private Sub UpdateControl(ByVal C As Label, ByVal txt As String)
If C.InvokeRequired Then
C.Invoke(New UpdateControlDelegate(AddressOf UpdateControl), New Object() {C, txt})
Else
C.Text = txt
End If
End Sub
Public Sub DoStuff()
'we do some stuff then when it comnes time update a certain control:
Call UpdateControl(MyLabel, "My Text For The Label)
End Sub
In The workerThread that is located in a class:
Public Class MyClass
Public Sub UpdateData
Call Form1.DoStuff
End Sub
End Class
Does this look correct? The most simplest terms on what I am trying to achieve:
WorkerThread to call a Sub that is located in Class Form1
and that sub contains code that updates a couple controls in Form1.

After doing a little more research. I have figured it out. The initial code I have written is correct. The only thing missing is a reference to the form I need to update.
Here is the COMPLETE solution when needing to run a SUB from the UI that is called from the Worker Thread:
Public Class MyClass
'working thread is being within the subs of this class
Public MyForm1111 As Form1 '<------ The variable in this class that will reference to the form1 that we need
Public Sub MySubThatIsOnAWorkerThread
MyForm1111.DoStuff '<==== must call MyForm1111.DoStuff and NOT Form1.DoStuff
End Sub
End Class
The Sub Located In Form1:
Public Class Form1
Public Delegate Sub UpdateControlDelegate(ByVal C As Label, ByVal txt As String) 'Required Delegate
Private Sub UpdateControl(ByVal C As Label, ByVal txt As String) 'Sub to update controls
If C.InvokeRequired Then
C.Invoke(New UpdateControlDelegate(AddressOf UpdateControl), New Object() {C, txt})
Else
C.Text = txt
End If
End Sub
Public Sub DoStuff() 'the sub we need to call from the worker thread
'do some calculations and code
Call UpdateControl(MyLabel, "Some Text For Label")
End Sub
Private Sub Form1_Load()
MyClass.MyForm1111 = Me <==== Set the reference here in your Form1_Load
End Sub
End Class

Related

Blank ListView when calling Invoke

listView is owned by the class Form1. The subroutine anotherThread in a separate class transmission is started in a thread by a subroutine in Form1. Form1 owns another public subroutine addItemsListView, which uses Invoke.
When transmission.anotherThread calls addItemsListView, the subroutine runs, but listView remains blank.
Have tried delegates, invokes etc. inside each class but the same problem.
Class Form1
Property myTransmission = New transmission
Private Sub aSubRoutine() Handles MyBase.Load
Dim t As New Threading.Thread(
Sub()
myTransmission.anotherThread()
End Sub
)
t.Start()
End Sub
Public Sub addItemsListView(ByVal items As String())
If listView.InvokeRequired Then
listView.Invoke(Sub() addItemsListView(items))
Else
For each item In Items
listView.Items.Add(item)
Next
End If
End Sub
End Class
Class transmission
Public Sub anotherThread()
Form1.addItemsListView(New String() {"abc", "def"})
End Sub
End Class
So I expect "abc" and "def" to be in the listView but it remains completely blank. If I step through the code, however, everything seems to be running smoothly.
You aren't talking to your existing form. Pass it as a reference:
Public Sub anotherThread(inForm As Form1)
inForm.addItemsListView(New String() {"abc", "def"})
End Sub
then include your form when you call it:
myTransmission.anotherThread(Me)

Handling a timer from a class

I want some values in a class to decrease whenever the timer in the main form ticks. I am creating multiple instances of the same class as my program is a simulation application and I am not storing these instances in an array or any list in that matter. I simply declare them and add their picture box to the controls on the main form. However I am hoping to have a sub routine inside the class that triggers whenever the timer in the main form ticks. I thought of something like this:
Public Class Jimmy
Dim _a As Integer = 10
Sub decreseNum(sender As Object, e As EventArgs) Handles mainapp.tmrLog.Tick
_a -= 1
End Sub
End Class
with mainapp being the name of the main form and tmrLog being the timer I want to associate my sub routine with. However the above code doesn't work
You could try defining a local reference to the timer in the Jimmy class:
Public Class Jimmy
Dim _a As Integer = 10
Private WithEvents tmr As Timer
Public Sub New(ByRef MainTmr As Timer)
tmr = MainTmr
End Sub
Sub decreseNum(sender As Object, e As EventArgs) Handles tmr.Tick
_a -= 1
End Sub
End Class
If you want all your classes react to timer.elapsed event, just sign up for it. The program below is fully operational. It is example what you can do to have your children to react to timer events of single parent/timer
Imports System
imports system.timers
Public Module Module1
Public Sub Main()
dim mc as new MainClass()
mc.CreateChildren(5)
System.Threading.Thread.Sleep(60000) ' wait and monitor output of childern
mc.Stop()
Console.WriteLine("All should stop now...")
Console.Read()
End Sub
End Module
public class MainClass 'This class could be your form
private _timer as new Timer(5000)
public sub CreateChildren(count as integer)
For i as integer = 1 to count
dim c as new Child(i)
Addhandler _timer.Elapsed, addressof c.DoWhentimerTicks
next
Console.WriteLine("timer should run now...")
_timer.Start()
end sub
public sub [Stop]()
_timer.Stop()
End Sub
End class
public class Child
private _myNO as integer
public sub new (no as integer)
_myNo = no
end sub
public sub DoWhentimerTicks(sender as object , e as ElapsedEventArgs)
Console.WriteLine(string.format("Child #{0} just ticked. Time = {1}", _myNo, e.signaltime))
end sub
End class
I found my solution, posting here for further reference.
My situation was trying to have my timer in the mainform triggering a sub in a class, and I used the following solution.
Class:
Sub addHandlesToSub
AddHandler Form1.Timer1.Tick, AddressOf subToBeTriggered
End Sub
Sub subToBeTriggered(sender As Object, e As EventArgs)
'My code
End Sub
The parameters in subToBeTriggered are useful when you want to remove the handler with
RemoveHandler Form1.Timer1.Tick, AddressOf subToBeTriggered
Otherwise, there will be an error without the parameters.
Thanks for all the answers though.

Access variable in Shared Sub

is there a way to access a variable in Form_Load from an event handler?
Please dont mind the code, this is just a representation of my question.
Public Class Form
Public Sub Form_Load()
Dim x as string
x = MyClass.MethodGetValue()
End Sub
Private Shared Sub OnChanged()
MyClass2.MethodGetValue(x)
End Sub
End Class
It's about the scope of the variable. In your situation you need a class variable. This allows it to be used anywhere inside of this class.
Public Class Form1
Private x As Object 'pick the datatype that matches your needs
Public Sub Form_Load()
x = MyClass.MethodGetValue()
End Sub
Private Sub OnChanged()
MyClass2.MethodGetValue(x)
End Sub
End Class

Safe ThreadPool Queueing with Parameters in VB.NET (WinForms)

I know how to use BackgroundWorker (gui object in WinForms designer), and to manually instantiate Threads that elevate the custom event to the UI, however, I am having some trouble figuring out how to use the ThreadPool object (simplest form) to handle elevating an event to the form for "safe" UI manipulation.
Example is as follows :
Form1.vb
Public Class Form1
WithEvents t As Tools = New Tools
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
t.Unzip("file 1", "foo")
t.Unzip("file 2", "foo")
t.Unzip("file 3", "foo")
t.Unzip("file 4", "foo")
t.Unzip("file 5", "foo")
t.Unzip("file 6", "foo")
t.Unzip("file 7", "foo")
t.Unzip("file 8", "foo")
t.Unzip("file 9", "foo")
End Sub
Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
End Sub
End Class
( add a multiline textbox, and a button to this form for the demo )
Tools.vb
Imports System
Imports System.Threading
Imports System.IO.Compression
Public Class Tools
#Region "Zip"
Private _zip As System.IO.Compression.ZipFile
Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)
Public Class ZipInfo
Public Property ZipFile As String
Public Property Path As String
End Class
Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Destination
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Folder
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Shared Sub ThreadUnzip(ZipInfo As Object)
RaiseEvent UnzipComplete(ZipInfo)
End Sub
Shared Sub ThreadZip(ZipInfo As Object)
RaiseEvent ZipComplete(ZipInfo)
End Sub
#End Region
End Class
What this code should do, is as follows :
On Button1_Click, add 9 items to the ThreadPool
On each thread completion (order is irrelevant), raise an event that elevates to Form1
The event being raised on Form1 should be UI safe, so I can use the information being passed to the ZipCompleted / UnzipCompleted events in the Textbox. This should be generic, meaning the function that raises the event should be reusable and does not make calls to the form directly. (aka, I do not want a "custom" sub or function in Tools.vb that calls specific elements on Form1.vb . This should be generic and reusable by adding the class to my project and then entering any "custom" form code under the event being raised (like when Button1_Click is raised, even though it's threaded, the other form interactions are not part of the Button1 object/class -- they are written by the coder to the event that is raised when a user clicks.
If you want to ensure that an object that has no direct knowledge of your UI raises its events on the UI thread then use the SynchronizationContext class, e.g.
Public Class SomeClass
Private threadingContext As SynchronizationContext = SynchronizationContext.Current
Public Event SomethingHappened As EventHandler
Protected Overridable Sub OnSomethingHappened(e As EventArgs)
RaiseEvent SomethingHappened(Me, e)
End Sub
Private Sub RaiseSomethingHappened()
If Me.threadingContext IsNot Nothing Then
Me.threadingContext.Post(Sub(e) Me.OnSomethingHappened(DirectCast(e, EventArgs)), EventArgs.Empty)
Else
Me.OnSomethingHappened(EventArgs.Empty)
End If
End Sub
End Class
As long as you create your instance of that class on the UI thread, its SomethingHappened event will be raised on the UI thread. If there is no UI thread then the event will simply be raised on the current thread.
Here's a more complete example, which includes a simpler method for using a Lambda Expression:
Imports System.Threading
Public Class Form1
Private WithEvents thing As New SomeClass
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Me.thing.DoSomethingAsync()
End Sub
Private Sub thing_DoSomethingCompleted(sender As Object, e As IntegerEventArgs) Handles thing.DoSomethingCompleted
MessageBox.Show(String.Format("The number is {0}.", e.Number))
End Sub
End Class
''' <summary>
''' Raises events on the UI thread after asynchronous tasks, assuming the instance was created on a UI thread.
''' </summary>
Public Class SomeClass
Private ReadOnly threadingContext As SynchronizationContext = SynchronizationContext.Current
Public Event DoSomethingCompleted As EventHandler(Of IntegerEventArgs)
''' <summary>
''' Begin an asynchronous task.
''' </summary>
Public Sub DoSomethingAsync()
Dim t As New Thread(AddressOf DoSomething)
t.Start()
End Sub
Protected Overridable Sub OnDoSomethingCompleted(e As IntegerEventArgs)
RaiseEvent DoSomethingCompleted(Me, e)
End Sub
Private Sub DoSomething()
Dim rng As New Random
Dim number = rng.Next(5000, 10000)
'Do some work.
Thread.Sleep(number)
Dim e As New IntegerEventArgs With {.Number = number}
'Raise the DoSomethingCompleted event on the UI thread.
Me.threadingContext.Post(Sub() OnDoSomethingCompleted(e), Nothing)
End Sub
End Class
Public Class IntegerEventArgs
Inherits EventArgs
Public Property Number() As Integer
End Class
You should register from the Form to events of the Tools class (you already have these events defined), of course the actual event will be fired under a non-UI thread, so the code it executes during the callback will only be able to update the UI via an Invoke()
You want to simply raise the event in the Tools class, the Invoke needs to be done because you want to update the UI, the Tools class should be concerned about that.
Change your event handling like so:
Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
End Sub
To register to the event from the view: (this would go in the Button1_Click event
AddHandler t.UnzipComplete, AddressOf t_UnzipComplete
Make sure you only register to the event one time
Does this solve your issue?
Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
If TextBox1.InvokeRequired Then
TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
Else
TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
End If
End Sub
You could create a callback to do the invoking in a safer way. Something like this:
Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String, _
ByVal SafeCallback As Action(Of ZipInfo))
And then the calling code does this:
t.Unzip("file 1", "foo", Sub (zi) TextBox1.Invoke(Sub () t_UnzipComplete(zi)))
Personally I think it is better - and more conventional - to invoke on the event handler, but you could do it this way.
Okay, so here is what I came up with using a combination of the information from everyone contributing to this question -- all excellent and VERY helpful answers, which helped lead me to the final solution. Ideally, I would like this as a straight "class", but I can accept a UserControl for this purpose. If someone can take this and do exactly the same thing with a class, that would definitely win my vote. Right now, I will really have to consider which one to vote for.
Here is the updated Tools.vb
Imports System
Imports System.Threading
Imports System.Windows.Forms
Imports System.IO.Compression
Public Class Tools
Inherits UserControl
#Region "Zip"
Private _zip As System.IO.Compression.ZipFile
Private threadingContext As SynchronizationContext = SynchronizationContext.Current
Private Delegate Sub EventArgsDelegate(ByVal e As ZipInfo)
Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)
Public Class ZipInfo
Public Property ZipFile As String
Public Property Path As String
End Class
Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Destination
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Folder
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Private Sub ThreadUnzip(ZipInfo As Object)
If Me.InvokeRequired Then
Me.Invoke(New EventArgsDelegate(AddressOf ThreadUnzip), ZipInfo)
Else
RaiseEvent UnzipComplete(ZipInfo)
End If
End Sub
Private Sub ThreadZip(ZipInfo As Object)
If Me.InvokeRequired Then
Me.Invoke(New EventArgsDelegate(AddressOf ThreadZip), ZipInfo)
Else
RaiseEvent ZipComplete(ZipInfo)
End If
End Sub
#End Region
End Class
If you drop this on Form1.vb, and select/activate the UnzipComplete/ZipComplete events, you will find that they will interact with the UI thread without having to pass a Sub, or Invoke, etc, from the Form. It is also generic, meaning it is unaware of what form elements you will be interacting with so explicit invoking such as TexBox1.Invoke() or other element specific calls are not required.

referencing form button from another form

I have two forms both with the same buttons on, and I want to have it so that if I click the button both buttons will do the same thing i.e. they are referencing each other on different forms. the way i found was:
Public Class Form2
Dim form1 As New form1
Private Sub Button2_Click
form1.backcolor=black
form2.backcolor=black
end sub
end class
then
Public Class Form1
Dim form2 As New form2
Private Sub Button1_Click
form1.backcolor=black
form2.backcolor=black
end sub
end class
only this doesn't work as there is an error:An unhandled exception of type 'System.StackOverflowException' occurred in System.Windows.Forms.dll as far as i can see there is no infinite loop or stack over flow.
any help would be greatly appreciated.
You have an infinite loop, because each time one of the forms is instantiated, it is instantiating the other. Creating a Form1 will create a Form2, then Form2 immediately creates another Form1 and so on and so on...
Change your code to this:
Public Class Form2
Private Sub Button2_Click
Dim form1 As New Form1
form1.backcolor=black
form2.backcolor=black
End sub
End class
Public Class Form1
Private Sub Button1_Click
form1.backcolor=black
Dim form2 As New Form2
form2.backcolor=black
End sub
End class
Now it will only create the other class instances when you click a button.
Like Karl Anderson said, there is a infinite loop in your code. His solution will create a new form every time you click the button. If you don't want this behavior, I think that the best approach is to use the mediator pattern. And it will be much more easy if you want to add new actions and new forms.
The code will look something like this:
Public Class Mediator
Private forms As New List(Of BaseForm)
Public Sub RegisterForm(form As BaseForm)
forms.Add(form)
End Sub
Public Sub ChangeAllFormsBackColorToBlack()
For Each form In forms
form.ChangeBackColorToBlack()
Next
End Sub
End Class
Public Class BaseForm
Private med As Mediator
Public Sub New(med As Mediator)
Me.med = med
Me.med.RegisterForm(Me)
End Sub
Public Sub ChangeBackColorToBlack()
backcolor = black
End Sub
Public Sub OnButtonClick()
Me.med.ChangeAllFormsBackColorToBlack()
End Sub
End Class
Public Class Form2
Inherits BaseForm
Public Sub New(med As Mediator)
MyBase.New(med)
End Sub
Private Sub Button2_Click()
Me.OnButtonClick()
End Sub
End Class
Public Class Form1
Inherits BaseForm
Public Sub New(med As Mediator)
MyBase.New(med)
End Sub
Private Sub Button1_Click()
Me.OnButtonClick()
End Sub
End Class
Module MediatorDemo
Sub Main()
Dim med As New Mediator
Dim f1 As New Form1(med)
Dim f2 As New Form2(med)
f1.OnButtonClick()
End Sub
End Module