I am wondering if it is possible to make and Raise an event from a class, to a form? I have a class that just loops through itself continually, but when a condition is met I want a button to become visible on a form. I am looking into different ways of accomplishing this. It is probably worth mentioning that I am multithreading and this loop is being run in a different thread than the UI is at. Which is why I am hoping it is possible to raise an event and that will jump over to the UI thread and make the button visible then come back to where it was in the loop.
Any suggestions or direction is appreciated.
Something like this would work,
Public Class MyClass
Public Event MyEvent()
Sub DoStuff()
RaiseEvent MyEvent
End Sub
Public Class
Public Class MyForm
Public Sub HandleEvent()
'can be called from another thread, so use invoke
Me.Invoke(Sub() MyButton.Visible = true )
End Sub
End Class
Dim mMyClass = New MyClass
Dim mMyForm = New MyForm
AddHandler mMyClass.MyEvent, AddressOf mMyForm.HandleEvent()
As long as you remember to use .Invoke when updating the UI if your event is coming from another thread, there is no issue with having a form handle events from any source.
I think here is a possible solution:
1) pass parent form to the class as a parameter;
2) in parent form, invoke the function
Private Delegate Sub DoStuffDelegate()
Private Sub DoStuff()
If Me.InvokeRequired Then
Me.Invoke(New DoStuffDelegate(AddressOf DoStuff))
Else
btn.Visible = true
End If
End Sub
3) in class, call it as parentform.DoStuff()
check the syntax as I typed it.
Related
I’ve got a problem with form freezes when loading the form from an event. I bet it got to do with threading but sadly I don’t know enough about it to fix it myself :(
Let me explain my project:
I've got a class that hooks into networking events (like new connected e.g.),
which I’ve instanced in a form and declared some events from it.
Public Netstat As New aaNetTool.clsNetworkStatus
AddHandler Netstat.NetworkChanged, AddressOf Network_Changed
Sub Network_Changed()
End Sub
Then I’ve written another class, clsMessage which I want to use to show forms with notifications.
Public Class clsMessage
Private myForm As frmDisplayMessage
Public Sub New(ByVal Title$, ByVal Text$, Optional btnYesAction As Action = Nothing, Optional ByVal ShowTimeSec% = 10)
myForm = New frmDisplayMessage
myForm.Text = Title
myForm.lblText.Text = Text
(...)
myForm.Show()
(...)
End Sub
Now I create a new notification window for debugging purposes with a button from the main form like this:
Dim myMsg As New clsMessage("title", "text", AddressOf MapNetworkdrives, 30)
This works like a charm.
But when I call the notification from my declared event:
Sub Network_Changed()
Dim myMsg As New clsMessage("title", "text", AddressOf MapNetworkdrives, 30)
End Sub
The form with the notification appears but is empty and freezed.
As said before I think this may have to do with my code running on different threads but I just can't figure out how to solve this :(
Thanks in advance for your Time,
Lunex
The clsNetworkStatus.NetworkChanged event appears to be raised from a background thread. Since your notification form is part of the UI you must invoke so that it is executed under the UI thread.
The InvokeRequired property tells you whether you need to invoke or not, so if it's False your code is already running on the UI thread.
You can create an extension method to do the checking for you:
Imports System.Runtime.CompilerServices
Public Module Extensions
<Extension()> _
Public Sub InvokeIfRequired(ByVal Control As Control, ByVal Method As Action)
If Control.InvokeRequired = True Then
Control.Invoke(Method) 'Invoke the method thread-safely.
Else
Method.Invoke() 'Call the method normally (equal to just calling: 'Method()').
End If
End Sub
End Module
Then you'd use it like this:
Sub Network_Changed()
Me.InvokeIfRequired(Sub()
Dim myMsg As New clsMessage("title", "text", AddressOf MapNetworkdrives, 30)
End Sub)
End Sub
I have been banging my head against the wall all day trying to figure this one out.
I am finishing up a program to simply delete files in specific temp folders. I have read that it is sometimes good practice to create separate classes for methods and variables. So I have created a separate class for a couple methods to delete files and folders in a specified directory. I am using a Background Worker in my Form1 class and am calling my deleteFiles() method from my WebFixProcesses class in the DoWork event in the Form1 class. I am using a Background Worker so that I can easily report progress back to a progress bar on my main form.
The files get deleted without an issue but I just can't get the label on my main form to reflect the current file being deleted. the label doesn't change in any way.
I know the formula is correct as I can get this working if the method is in the Form1 class. and I simply use:
Invoke(Sub()
lblStatus.Text = File.ToString
lblStatus.Refresh()
End Sub)
here is my method that I am calling from the WebFixProcesses class:
Public Shared Sub deleteFiles(ByVal fileLocation As String)
For Each file As String In Directory.GetFiles(fileLocation)
Try
fileDisplay.Add(file)
For i = 1 To fileDisplay.Count
file = fileDisplay(i)
Form1.BackgroundWorker1.ReportProgress(CInt(i / fileDisplay.Count) * 100)
Next
IO.File.Delete(file)
Form1.labelText(file)
Form1.labelRefresh()
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
Next
End Sub
labelText() and labelRefresh() are methods from my main form which are using delegates to try to pass information to the control:
Public Sub labelText(ByVal file As String)
If lblStatus.InvokeRequired Then
Dim del As New txtBoxDelegate(AddressOf labelText)
Me.Invoke(del, file)
Else
lblStatus.Text = file.ToString()
End If
End Sub
Public Sub labelRefresh()
If lblStatus.InvokeRequired Then
Dim del As New txtBoxRefDelegate(AddressOf labelRefresh)
Me.Invoke(del)
Else
lblStatus.Refresh()
End If
End Sub
If anyone can help me out to inform me what I may be doing wrong it would be immensely appreciated as my head is in a lot of pain from this. And maybe I am going at it all wrong, and just being stubborn keeping my methods in their own class. But any help would be awesome. Thanks guys!
What Hans wrote on the question comment is true: Form1 is a type, not an instance, but to make things easier to newbye programmes (coming from VB6), M$ did a "mix", allowing you to use the form name as the instance of the form in the main thread.
This however works only if you are on that thread.
If you reference Form1 from another thread, a new instance of Form1 is created.
To solve the issue, add this code to the form:
Private Shared _instance As Form1
Public ReadOnly Property Instance As Form1
Get
Return _instance
End Get
End Property
We will use this property to store the current instance of the form. To do so, add this line to the Load event:
Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
_instance = Me
'other code here
End Sub
Now, from every class, in any thread, if you use
Form1.Instance
...you get the actual form. Now you can Invoke, even from the same form:
Me.instance.Invoke(Sub()
Me.lblStatus.Text = "Hello World"
End Sub)
I have a main form wich is expected to perfom some long operations. In parallel, I'm trying to display the percentage of the executed actions.
So I created a second form like this:
Private Delegate Sub DoubleFunction(ByVal D as Double)
Private Delegate Sub EmptyFunction()
Public Class LoaderClass
Inherits Form
'Some properties here
Public Sub DisplayPercentage(Value as Double)
If Me.InvokeRequired then
dim TempFunction as New DoubleFunction(addressof DisplayPercentage)
Me.Invoke(TempFunction, Value)
Else
Me.PercentageLabel.text = Value
End if
End sub
Public Sub CloseForm()
If Me.InvokeRequired Then
Dim CloseFunction As New EmptyFunction(AddressOf CloseForm)
Me.Invoke(CloseFunction)
Else
Me.Close()
End If
FormClosed = True
End Sub
End class
My main sub, the one which is expected to perform the long operations is in another form as follows:
Private Sub InitApplication
Dim Loader as new LoaderClass
Dim LoaderThread as new thread(Sub()
Loader.ShowDialog()
End sub)
LoaderThread.start()
Loader.DisplayPercentage(1/10)
LoadLocalConfiguration()
Loader.DisplayPercentage(2/10)
ConnectToDataBase()
Loader.DisplayPercentage(3/10)
LoadInterfaceObjects()
Loader.DisplayPercentage(4/10)
LoadClients()
...
Loader.CloseForm()
End sub
The code works almost 95% of the time but sometimes I'm getting a thread exception somewhere in the sub DisplayPercentage. I change absolutely nothing, I just hit the start button again and the debugger continues the execution without any problem.
The exception I get is: Cross-thread operation not valid: Control 'LoaderClass' accessed from a thread other than the thread it was created on event though I'm using : if InvokeRequired
Does anyone know what is wrong with that code please ?
Thank you.
This is a standard threading bug, called a "race condition". The fundamental problem with your code is that the InvokeRequired property can only be accurate after the native window for the dialog is created. The problem is that you don't wait for that. The thread you started needs time to create the dialog. It blows up when InvokeRequired still returns false but a fraction of a second later the window is created and Invoke() now objects loudly against being called on a worker thread.
This requires interlocking, you must use an AutoResetEvent. Call its Set() method in the Load event handler for the dialog. Call its WaitOne() method in InitApplication().
This is not the only problem with this code. Your dialog also doesn't have a Z-order relationship with the rest of the windows in your app. Non-zero odds that it will show behind another window.
And an especially nasty kind of problem caused by the SystemEvents class. Which needs to fire events on the UI thread. It doesn't know what thread is the UI thread, it guesses that the first one that subscribes an event is that UI thread. That turns out very poorly if that's your dialog when it uses, say, a ProgressBar. Which uses SystemEvents to know when to repaint itself. Your program will crash and burn long after the dialog is closed when one of the SystemEvents now is raised on the wrong thread.
Scared you enough? Don't do it. Only display UI on the UI thread, only execute slow non-UI code on worker threads.
Thank you for your proposal. How to do that please ? Where should I
add Invoke ?
Assuming you've opted to leave the "loading" code of the main form in the main UI thread (probably called from the Load() event), AND you've set LoaderClass() as the "Splash screen" in Project --> Properties...
Here is what LoaderClass() would look like:
Public Class LoaderClass
Private Delegate Sub DoubleFunction(ByVal D As Double)
Public Sub DisplayPercentage(Value As Double)
If Me.InvokeRequired Then
Dim TempFunction As New DoubleFunction(AddressOf DisplayPercentage)
Me.Invoke(TempFunction, Value)
Else
Me.PercentageLabel.text = Value
End If
End Sub
End Class
*This is the same as what you had but I moved the delegate into the class.
*Note that you do NOT need the CloseForm() method as the framework will automatically close your splash screen once the main form is completely loaded.
Now, over in the main form, you can grab the displayed instance of the splash screen with My.Application.SplashScreen and cast it back to LoaderClass(). Then simply call your DisplayPercentage() method at the appropriate times with appropriate values:
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
InitApplication()
End Sub
Private Sub InitApplication()
Dim Loader As LoaderClass = DirectCast(My.Application.SplashScreen, LoaderClass)
Loader.DisplayPercentage(1 / 10)
LoadLocalConfiguration()
Loader.DisplayPercentage(2 / 10)
ConnectToDataBase()
Loader.DisplayPercentage(3 / 10)
LoadInterfaceObjects()
Loader.DisplayPercentage(4 / 10)
LoadClients()
' Loader.CloseForm() <-- This is no longer needed..."Loader" will be closed automatically!
End Sub
Private Sub LoadLocalConfiguration()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
Private Sub ConnectToDataBase()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
Private Sub LoadInterfaceObjects()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
Private Sub LoadClients()
System.Threading.Thread.Sleep(1000) ' simulated "work"
End Sub
End Class
If all goes well, your splash screen should automatically display, update with progress, then automatically close when your main form has finished loading and displayed itself.
Me.Invoke(TempFunction, Value)
Should be:
Me.Invoke(TempFunction, new Object(){Value})
because the overload with parameters takes an array of parameters.
Value is on the stack of the function in the current thread. You need to allocate memory on the GC heap and copy the value to that memory so that it is available to the other thread even after the local stack has been destroyed.
I have this text loader class that I'm trying to write tests for. And one of it's methods does a RaiseEvent with a CancelEventArgs parsed in as an argument, so something like this:
Private Sub FireThisEvent()
cancelEvent created here
RaiseEvent HelloWorld(cancelEvent)
If cancelEvent.Cancel Then
'do smthg
End If
End Sub
The handler for HelloWorld event is my UI class that makes a pop-up for the user to
decide Yes or No, which then sets cancelEvent.Cancel to either True or False. And then the above method checks on cancelEvent and does an action accordingly.
My question is, since I'm only testing the loader class (and not the UI), how do I manipulate the cancelEvent after the event is raised so that I can test for when cancelEvent.Cancel is True and then , when it is False. Thank you.
Would I have mock the UI class?
My solution to this was to add an event handler in the test method, so that when the event is raised, the test method will create a CancelEventAgrs and set its Cancel to True/False.
Public Sub TestingMethod()
Dim txt As TextLoader = Nothing
AddHandler TextLoader.LoadingDoneEvent,
(Sub(e As ComponentModel.CancelEventArgs)
e.Cancel = True
End Sub)
txt = New TextLoader()
txt.FireThisEvent()
End Sub
For some reason a background thread in my app can't change any labels, textbox values, etc on my main form. There is no compile errors, when the thread executes nothing happens.
Here is some example code:
Imports System.Threading
Public Class Class1
Dim tmpThread As System.Threading.Thread
Private Sub bgFindThread()
Form1.lblStatus.Text = "test"
End Sub
Public Sub ThreadAction(ByVal Action As String)
If Action = "Start" Then
tmpThread = New System.Threading.Thread(New System.Threading.ThreadStart(AddressOf bgFindThread))
tmpThread.Start()
ElseIf Action = "Abort" Then
If tmpThread.IsAlive = True Then tmpThread.Abort()
End If
End Sub
End Class
Can someone let me know what I'm doing wrong?
AFAIK code above will throw an exception IllegalCrossThreadException, it is because the background thread is not the same as UI thread and background try to set value on other thread. So windows form check every thread that work properly.
You can set Control.CheckForIllegalCrossThreadCalls to false to make it works.
Code below is when setting property is not run
Add into your code
------------------------------
Delegate Sub MyDelegate()
Private Sub RunMyControl()
lblStatus.Text = "test"
End Sub
Change your code
------------------------------
Private Sub bgFindThread
lblStatus.BeginInvoke (New MyDelegate(AddressOf RunMyControl))
End Sub
The method asyncronsly run code from background thread to UI thread.
You can only access UI controls from the UI thread.
I suggest reading this first: http://www.albahari.com/threading/
As others have mentioned, it is forbidden (for good reasons) to update UI elements from a non-UI thread.
The canonical solution is as follows:
Test whether you are outside the UI thread
If so, request for an operation to be performed inside the UI thread
[Inside the UI thread] Update the control.
In your case:
Private Sub bgFindThread()
If lblStatus.InvokeRequired Then
lblStatus.Invoke(New Action(AddressOf bgFindThread))
Return
End If
lblStatus.Text = "test"
End Sub
The only thing that changed is the guard clause at the beginning of the method which test whether we’re inside the UI thread and, if not, requests an execution in the UI thread and returns.
You can use a delegate to update UI controls in a background thread.
Example
Private Delegate Sub bkgChangeControl(ByVal bSucceed As Boolean)
Private dlgChangeControl As bkgChangeControl = AddressOf ChangeControl
Private Sub threadWorker_ChangeControl(ByVal bSucceed As Boolean)
Me.Invoke(dlgChangeControl, New Object() {bSucceed})
End Sub
Private Sub ChangeControl()
Me.lable="Changed"
End Sub
'In your background thread, call threadWorker_ChangeControl.