VB.net, Invoke, delegates, and threading. Can't figure out how to use them across classes - vb.net

Long story short, I'm having a hell of a time trying to figure out how to use invoke and/or delegates to update the userform from a separate class when using threading. I'm quite sure it's something silly and obvious to someone with more experience. I know a delegate is probably required, but all my efforts seem to only work when it's being called from main thread. I've been looking around the internet for half the day, and there's just something I'm not getting.
Here's some pseudo-code as an example:
This option works:
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t1 As New Threading.Thread(AddressOf Count)
t1.IsBackground = True
t1.Start(100)
End Sub
Private Sub Count(ByVal Max As Object)
If TypeOf Max Is Integer Then
Count(CInt(Max))
End If
End Sub
Private Sub SetLabelText(ByVal text As String)
If Label1.InvokeRequired Then
Label1.Invoke(New Action(Of String)(AddressOf SetLabelText), text)
Else
Label1.Text = text
End If
End Sub
Private Sub Count(ByVal Max As Integer)
For i = 1 To Max
SetLabelText(CStr(i))
Threading.Thread.Sleep(200)
Next
End Sub
End Class
While this (one of my 1000 efforts of slightly different variation) does not. Practically speaking, I just tried to separate one of the subs into its own class for this example, but it's otherwise the same as I could make it:
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t1 As New Threading.Thread(AddressOf Count)
t1.Start(100)
End Sub
Private Sub Count(ByVal Max As Object)
If TypeOf Max Is Integer Then
Dim class2 As New class2
class2.Count(CInt(Max))
End If
End Sub
Private Delegate Sub SetTextBoxTextInvoker(text As String)
Sub SetLabelText(ByVal text As String)
'or me.label1, form1.label1 or anything else I can try!
If Me.InvokeRequired Then
Me.Invoke(New SetTextBoxTextInvoker(AddressOf SetLabelText), _
text)
Else
Me.Label1.Text = text
End If
End Sub
End Class
Public Class class2
Sub Count(ByVal Max As Integer)
For i = 1 To Max
form1.SetLabelText(CStr(i))
Threading.Thread.Sleep(200)
Next
End Sub
End Class
From what I can tell, it appears that the if statement for invokerequired in the Sub "SetLabelText" never gets triggered. My best guess is that I'm not referring to the userform correctly when checking for the invokerequired parameter? Or I need to feed something else to the delegate? I'm just getting frustrated with messing around with the million little variables I might be getting wrong. Thanks in advance for any help you can provide and let me know if you need more info.

I'm not certain I understand what you are trying to do, but building upon your code, you can set the label safely ("thread-safely") by using the following code:
Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t1 As New Threading.Thread(AddressOf Count)
t1.IsBackground = True
t1.Start(100)
End Sub
Private Sub Count(ByVal Max As Object)
If TypeOf Max Is Integer Then
Dim class2 As New Class2
class2.Count(CInt(Max), AddressOf SetLabelText)
End If
End Sub
Private Sub SetLabelText(ByVal text As String)
If Label1.InvokeRequired Then
Label1.Invoke(New SetText(AddressOf SetLabelText), text)
Else
Label1.Text = text
End If
End Sub
End Class
Public Class Class2
Sub Count(ByVal Max As Integer, SetTextMethod As SetText)
For i = 1 To Max
SetTextMethod.Invoke((CStr(i)))
Threading.Thread.Sleep(200)
Next
End Sub
End Class
Public Delegate Sub SetText(text As String)
I created a Delegate called "SetText"; when the form calls the count function in your class, you can pass an instance of the delegate that references the SetLabelText method. Within that method you can then safely set the label text either directly or indirectly via Invoke along with a new instance of the delegate.
Something you definitely don't want to do is reference your form from your class(i.e. "form1.SetLabelText(CStr(i))"); that can create a real nightmare as the project grows in size and requirements change!
If I've misunderstood your question or not answered it properly, please do post back.

First off I would suggest using the Task Parrallel Library instead of threads. It's easier to understand and work with. For example,
Dim countTask as New Task(Sub() Count(10))
Dim displayTask = countTask.ContinueWith(Sub()
Me.Invoke(Sub() Label.Text = "10"
End Sub)
countTask.Start()
This example assumes you are calling this from the form itself. You can use this method to return values from the first task to the second. If you need a more detail example and want more examples check out http://msdn.microsoft.com/en-us/library/hh228603.aspx. If you need further help I can always throw something up on GitHub or blog about it. Good Luck.

Related

VB - Find child form from parent

I am in a project with multiple form.
I create a TicTacToe form here :
Private Sub MenuTicTacToe(ByVal sender As Object, ByVal e As System.EventArgs)
Dim page As Form = New TicTacToe
page.Show(Me)
End Sub
Here is a TicTacToe form:
Public Class TicTacToe
Public opponent as String
'Some code where user set opponent
Public Function Receive(S As String)
if string = opponent
'Some code
End Function
End Class
I would like to call my function Receive in my main form
If i do:
TicTactoe.Receive(S)
It call a instance of Receive where opponent does not exist.
I would like to find the oppened form of TicTacToe and call Receive
Thanks
Comments in line
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
page.Receive("Joe")
End Sub
'A form level variable to hold a reference to the instance of TicTacToe
'Although vb.net can use default instances, you have created an explicit
'instance of TicTacToe so you need to keep a reference if you want to
'refer to this instance.
Private page As TicTacToe
Private Sub MenuTicTacToe(ByVal sender As Object, ByVal e As System.EventArgs)
page = New TicTacToe()
page.Show(Me)
End Sub
Partial Public Class TicTacToe
Inherits Form
Public opponent As String
'Functions must be declared as a Type
'If you do not need a return value use a Sub
Public Function Receive(S As String) As String
Dim someString As String = ""
If S = opponent Then
'Do something
End If
'There must be a return Value
Return someString
End Function
End Class
Use this to show the form
Dim page As TicTacToe
Private Sub MenuTicTacToe(ByVal sender As Object, ByVal e As System.EventArgs)
page = New TicTacToe
page.Show(Me)
End Sub
Then you can use
page.Receive(S)
Edit
To use multiple forms
For Each f As TicTacToe in Application.OpenForms().OfType(Of TicTacToe)
f.Receive (S)
Next
In C#, you'd need a new instance, but as you are in VB, the compiler already does that for you.
What you are currently doing, is creating a new instance of the TicTacToe form and showing it:
Private Sub MenuTicTacToe(ByVal sender As Object, ByVal e As System.EventArgs)
Dim page As Form = New TicTacToe
page.Show(Me)
End Sub
But you don't save that instance anywhere. Then, in your next piece of code, you are using a different instance, which is the static one created by the compiler:
TicTacToe.Receive(S) // TicTacToe is the static instance
Therefore, you end up calling two different instances, which explains why there is no opponent set.
To get around this problem, do not create a new instance. In your Private Sub MenuTicTacToe, just use the instance created by the compiler, and you won't have this problem, just like this:
Private Sub MenuTicTacToe(ByVal sender As Object, ByVal e As System.EventArgs)
TicTacToe.Show(Me)
End Sub
Hope this helps.

Can't Remove Listview Item From Second Form

I am having an issue when trying to delete ListView Items from a second form.
For example, if I use the following command on Form1 it works:
Listview1.SelectedItems(0).Remove
However, if I attempt to remove from Form2 like so:
Form1.Listview1.SelectedItems(0).Remove
I get the following error:
"Invalid argument=value of '0' is not valid for 'index'. Parameter name: index"
I then tried to get a count of items from the listview on Form2 and it gives me a return of 0
Form1.Listview1.Items.Count
I'm not sure what my problem is.
Update
I have posted a brief example of my code (using your suggestion as I can understand it):
frmShowMessages
Private Sub ViewMessage()
Dim frm As New frmViewMailMessage
frm.Show()
End Sub
Public Sub DeleteItem(ByVal index As Integer)
lsvReceivedMessages.Items(index).Remove()
End Sub
frmViewMessage
Private instanceForm as frmShowMessages
Private Sub frmViewMailMessage_Load(sender As Object, e As EventArgs) Handles MyBase.Load
instanceForm = New frmShowMessages()
End Sub
Private Sub cmdDelete_Click(sender As Object, e As EventArgs) Handles cmdDelete.Click
instanceForm.DeleteItem(_index)
End Sub
Hopefully my code can help identify where my issue is.
In VB.net usually you get a default Form instance for each of your Form. Probably you are creating an instance of Form1 and then you are trying to access ListView1 of default instance.
E.g.
Sub ButtonClick()
Dim f As New Form1()
f.Show()
' at this point if you access f's ListView you will get correct count
f.ListView1.Items.Count
' however if you try to access default instance it will NOT have any item
Form1.ListView.Items.Count
End Sub
It means your instance f is NOT equal to default Form1 instance.
Solution can be, make the f variable as class level variable and use it everywhere. Or if Form1 will have only 1 instance, then you can use the default instance everywhere.
Personally I would NOT go with direct control accessing over forms. I would create a Public method which should return the data as list to the caller, in this case your Form2.
UPDATED-2:
As per your given scenario, I am simplifying things for you, and doing implementation using Event.
Public Class frmShowMessages
Private Sub btnOpenMessage_Click(sender As System.Object, e As System.EventArgs) Handles btnOpenMessage.Click
Dim frmView As New frmViewMessage(Me.ListView1.SelectedItems(0).Index)
AddHandler frmView.MessageDeleted, AddressOf DeleteMessageHandler
frmView.Show()
End Sub
Private Sub DeleteMessageHandler(sender As Object, e As frmViewMessage.MessageDeletedEventArgs)
Me.ListView1.Items.RemoveAt(e.MessageIndex)
End Sub
End Class
Public Class frmViewMessage
' a class which will be used for Event communication
Public Class MessageDeletedEventArgs
Inherits EventArgs
Public Property MessageIndex As Integer
Public Sub New(ByVal iIndex As Integer)
MyBase.New()
Me.MessageIndex = iIndex
End Sub
End Class
' main event which will alert the parent that a message deletion should be done
Public Event MessageDeleted As EventHandler(Of MessageDeletedEventArgs)
' private variable that will hold the MessageIndex
Private Property MessageIndex As Integer
' method that is responsible to raise event
Protected Overridable Sub OnMessageDeleted()
RaiseEvent MessageDeleted(Me, New MessageDeletedEventArgs(Me.MessageIndex))
End Sub
' we want to create this Form using the MessageIndex of ListView
Public Sub New(ByVal iMessageIndex As Integer)
Me.InitializeComponent()
Me.MessageIndex = iMessageIndex
End Sub
' the delete button will raise the event to indicate parent that
' a deletion of message should be done
Private Sub btnDelete_Click(sender As System.Object, e As System.EventArgs) Handles btnDelete.Click
Me.OnMessageDeleted()
End Sub
End Class

Form cannot refer to itself through its default instance on shared call

Consider the following code, in a brand new WinForms .NET 4.0 application, with default settings:
Public Class Form1
Private Sub AAA()
Form1.AAA(Nothing) 'cannot refer to itself through its default instance; use 'Me' instead.
End Sub
Private Shared Sub AAA(str As String)
End Sub
End Class
I am getting this error:
{FORM_CLASS_NAME} cannot refer to itself through its default instance; use 'Me' instead.
I also get this warning at the same line:
Access of shared member, constant member, enum member or nested type through an instance; qualifying expression will not be evaluated.
Assuming default instance is meant here, it ends up in an infinite loop - VS suggests to change Me.AAA() to Form1.AAA(), and then back. AAA() works in both.
Converting Private Sub AAA() to Shared solves the error. It seems like from Microsoft's point of view, all overloads must be shared, if at least one is. Or you get this default instance confusion. Why?
To clarify, I do not want to use default instance here, just do a shared call.
If anyone encountered the same situation, please advise.
Creating a variable alias that has the same name as the type of the Form class is without a doubt the single most disastrous VB.NET problem. But it was necessary to give VB6 developers a fighting chance to move to VB.NET.
The workaround is to stop trying to be explicit about what method you want to call. This compiles fine and is unambiguous, at least in your snippet:
Private Sub AAA()
AAA(Nothing) '' fine
End Sub
If that really, really hurts then simply swapping the two methods removes the ambiguity:
Private Shared Sub AAA(str As String)
End Sub
Private Sub AAA()
Form1.AAA(Nothing) '' fine
End Sub
Can you get away with this? Your usage will be very similar Form1.AAA() vs. code.AAA().
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
code.AAA()
End Sub
Private Class code
Public Shared Sub AAA()
End Sub
End Class
End Class
EDIT
Given the new information in the OP - another solution to your issue may be to use optional parameters -- ie :
Private Shared Sub AAA(Optional ByVal str As String = Nothing)
Also - the resolution works out in the "right" way if you simply change the ordering of the declarations -- this avoids the compiler error:
Private Shared Sub AAA(ByVal str As String)
End Sub
Private Sub AAA()
Form1.AAA(Nothing)
End Sub
--
Keeping this below because it can be helpful in other circumstances
Perhaps your larger application did something like this - VB is full of messes like this you can get yourself into. This will compile but it will crash :
Public Class Form1
Private Shared Sub AAA()
Form1.Text = "this"
End Sub
Private Sub Label1_TextChanged(sender As System.Object, _
e As System.EventArgs) _
Handles Label1.TextChanged
Form1.AAA()
End Sub
End Class
Just the same, this actually is "fine" (I use the term loosely)...
Public Class Form1
Private Shared dont As Boolean = True
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) _
Handles MyBase.Load
dont = False
End Sub
Private Shared Sub AAA()
If Not dont Then Form1.Text = "this"
End Sub
Private Sub Label1_TextChanged(sender As System.Object, _
e As System.EventArgs) _
Handles Label1.TextChanged
Form1.AAA()
End Sub
End Class
This is because the text changed handler will fire before Form1 completes loading (ie : during InitializeComponent()!) and will refer to the default instance which is not yet finished being created - so VB tries to create a new one for you so that you can call the shared method which spins you down the infinite loop.
Oddly, the Load handler is "fine" (again, loosely) to call Form1.AAA() in - as in your opening code - because the default instance (Form1 the instance of Form1 the Class) is finished creation at that point and another won't be created to satisfy the call. Any other code path, however, that starts in the shared call and ultimately ends up touching any instance data, no matter how torturous the path, will loop around and crash.
See also : Why is there a default instance of every form in VB.Net but not in C#?
Unclear what you are trying to accomplish overall. In the OP Form1.AAA should be just AAA.
Private Sub AAA()
AAA(Nothing)
End Sub
Private Sub AAA(str As String)
If str IsNot Nothing Then MsgBox(str) ' else ???
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
AAA()
AAA("hello")
End Sub

Do Delegate arguments Need to Match Event Signature?

Please have a look at the code below:
Public Delegate Sub TestButtonClick(ByVal test As Integer)
Public Class Person
Private Name As String
Private ID As Integer
Public Event ButtonClick As TestButtonClick
Public Sub DelegateTest1(ByVal Test As Integer)
MsgBox(Test)
End Sub
Public Sub ChangeName()
RaiseEvent ButtonClick(1)
End Sub
Public Sub DelegateTest2()
MsgBox("Delegate Test 2")
End Sub
Public Sub DelegateTest3()
MsgBox("Delegate Test 3")
End Sub
End Class
Public Class Form1
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim p1 As Person = New Person
AddHandler p1.ButtonClick, AddressOf p1.DelegateTest1
AddHandler p1.ButtonClick, AddressOf p1.DelegateTest2
AddHandler p1.ButtonClick, AddressOf p1.DelegateTest3
p1.ChangeName()
End Sub
End Class
The output is:
1
DelegateTest2
DelegateTest3
I do not understand why this application compiles i.e. the delegate accepts an integer in its signature but Person.DelegateTest2 and Person.DelegateTest3 do not.
If I change Person.DelegateTest2() to the following then I do get an error as I would expect:
Public Sub DelegateTest2(ByVal Test As Integer, ByVal Test2 As Integer)
MsgBox("Delegate Test 2")
End Sub
Why does the Delegate allow you to pass zero arguments when it has arguments i.e. an integer in my case?
Don't forget that VB.NET inherits all the legacy baggage from the beloved VB. You could make it strict by putting the following to the top of your file so that it behaves as a real .NET programming language and not some hybrid crap:
Option Strict On
Also I would recommend you setting this to be the default option so that you don't find yourself in the wilderness.

VB.NET Animated image class that uses a timer to advance frames - is this a good approach?

VB.NET, .NET 4
Hello,
I am writing an industrial control front-end that has some "fancy" graphics to indicate the states of some machinery. For example, I indicate that some heaters are on by showing red wavy arrows emanating out from a picture of a heater. I accomplished this by creating a class that inherits from PictureBox and using a timer to advance the images:
Public Class AnimatedPictureBox
Inherits PictureBox
Private WithEvents Timer As New Timers.Timer
Public Property Interval As Double
Get
Return Timer.Interval
End Get
Set(ByVal value As Double)
Timer.Interval = value
End Set
End Property
Public ImageList As New List(Of Image)
Private NextImageIndex As Integer = 0
Public Sub New(ByVal Interval As Integer)
MyBase.New()
Timer = New Timers.Timer
Me.Interval = Interval
Me.SizeMode = PictureBoxSizeMode.StretchImage
Me.Visible = True
End Sub
Public Sub New()
Me.New(100)
End Sub
Public Sub BeginAnimation(ByVal [Loop] As Boolean)
Timer.Start()
End Sub
Public Sub BeginAnimation()
BeginAnimation([Loop]:=True)
End Sub
Public Sub StopAnimation()
Timer.Stop()
End Sub
Private Sub Timer_Elapsed(ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs) Handles Timer.Elapsed
InvokeControl(Me, _
Sub(x)
x.Image = x.ImageList(NextImageIndex)
x.NextImageIndex += 1
If x.NextImageIndex > x.ImageList.Count - 1 Then x.NextImageIndex = 0
End Sub)
End Sub
End Class
Where InvokeControl is a subroutine in a module that handles cross-thread marshalling of controls:
Private Sub InvokeControl(Of T As Control)(ByVal Control As T, ByVal Action As Action(Of T))
If Control.InvokeRequired Then
Try
Control.Invoke(New Action(Of T, Action(Of T))(AddressOf InvokeControl), New Object() {Control, Action})
Catch ex As Exception
'..Handle the error..
End Try
Else
Action(Control)
End If
End Sub
My question is "Is this an alright way to go about this or is there some obvious better way?" I'm not too good at programming and am worried that having several of these objects with their embedded timers taxing the CPU. Any suggestions would be greatly appreciated!
Thanks in advance,
Brian
P.S. I went with animated GIFs for some other animation stuff, but here, I wanted to be able to handle image formats that can handle a larger palette than a GIF (if that makes sense). In other words, when I tried to save some of my animations as GIFs, the image quality took an unacceptable hit.
I think this is a good approach. The timers give you a lot of flexibility, and like Mike said, they won't take any CPU time to speak of.