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
Related
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.
First off, I do not work with winform development full time so don't bash me too bad...
As the title somewhat depicts, I am having an issue refreshing the controls on a form after an event has been raised and captured.
On "Form1" I have a Dockpanel and am creating two new forms as shown below:
Public Sub New()
InitializeComponent()
dpGraph.DockLeftPortion = 225
dpGraph.BringToFront()
Dim frmT As frmGraphTools = New frmGraphTools()
Dim frmG As frmGraph = New frmGraph()
AddHandler frmT.UpdateGraph, AddressOf frmG.RefreshGraph
frmT.ShowHint = DockState.DockLeft
frmT.CloseButtonVisible = False
frmT.Show(dpGraph)
frmG.ShowHint = DockState.Document
frmG.CloseButtonVisible = False
frmG.Show(dpGraph)
End Sub
Within the frmGraphTools class I have the following delegate, event, and button click event defined:
Public Delegate Sub GraphValueChanged(ByVal datum As Date)
Public Event UpdateGraph As GraphValueChanged
Private Sub btnSaveMach_Click(sender As Object, e As EventArgs) Handles btnSaveMach.Click
RaiseEvent UpdateGraph(dtpJobDate.Value.ToString())
End Sub
Within the frmGraph class I have the following Sub defined:
Public Sub RefreshGraph(ByVal datum As Date)
CreateGraph(datum)
frmGraphBack.dpGraph.Refresh()
End Sub
I have a ZedGraph control on the frmGraph form that is supposed to be refreshed/redrawn upon the button click as defined on frmGraphTools. Everything seems to working, the RefreshGraph Sub within frmGraph is being executed and new data is pushed into the ZedGraph control however, the control never updates. What must be done to get the frmGraph form or the ZedGraph control to update/refresh/redraw properly?
Pass the reference to RefreshGroup method from the correct instance of frmGraph
AddHandler frmT.UpdateGraph, AddressOf frmG.RefreshGraph
also this call should be flagged by the compiler because you are passing a string instead of a Date
RaiseEvent UpdateGraph(dtpJobDate.Value.ToString())
probably you have Option Strict Off
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.
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.
I've been reading around trying to find out why I'd be getting this exception to no avail. I hope someone has seen this before:
I'm using Visual Basic 2010.
Briefly, I have a "Settings Panel" form which takes a while to create (it contains a lot of labels and textboxes), so I create it in another thread.
After it's loaded, it can be viewed by clicking a button which changes the form's visibility to True. I use the following subroutine to handle invokes for my controls:
Public Sub InvokeControl(Of T As Control)(ByVal Control As T, ByVal Action As Action(Of T))
If Control.InvokeRequired Then
Control.Invoke(New Action(Of T, Action(Of T))(AddressOf InvokeControl), New Object() {Control, Action})
Else
Action(Control)
End If
End Sub
Here's the relevant part of my main code (SettingsTable inherits TableLayoutPanel and HelperForm inherits Form):
Public Class ch4cp
Public RecipeTable As SettingsTable
Public WithEvents SettingsWindow As HelperForm
Private Sub ch4cp_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
PanelCreatorThread = New Threading.Thread(AddressOf CreateStartupPanels)
PanelCreatorThread.Start()
End Sub
Private Sub CreateStartupPanels()
SettingsWindow = New HelperForm("Settings Panel")
SettingsTable = New SettingsTable
SettingsTable.Create()
SettingsWindow.Controls.Add(SettingsTable)
End Sub
Private Sub ViewSettingsPanel_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ViewSettingsPanel.CheckedChanged
InvokeControl(SettingsWindow, Sub(x) x.Visible = ViewSettingsPanel.Checked)
End Sub
The SettingsTable.Create() method generates a bunch of Labels and TextBoxes based on the contents of the application settings and adds them to the SettingsTable.
When I click on the ViewSettingsPanel checkbox, I get a cross-thread violation error. Any ideas? I would really appreciate it.
I figured it out. In case anyone else might be running into a similar issue, here was the secret:
In the SettingsTable class, I have a MakeTable method which looks like this:
Private Sub MakeTable()
Me.Visible = False
Me.Controls.Clear()
... add some controls ...
Me.Visible = True
End Sub
I did this so that the control wouldn't flicker if the table was remade while visible. I don't entirely understand why (from reading, I'm guessing it's something like the handles for the child controls weren't being created because they weren't shown after being created, so IsInvokeRequired evaluated to False when it should have been True). The fix was to do this:
Private Sub MakeTable()
If Not IsNothing(Me.Parent) Then If Me.Parent.Visible Then Me.Visible = False
Me.Controls.Clear()
... add some controls ...
Me.Visible = True
End Sub
This way, the child controls are "shown" on the invisible SettingsWindow form and their handles are therefore created. Works just fine now!
Better way to this in VB.NET is to use a Extension it makes very nice looking code for cross-threading GUI Control Calls.
Just add this line of code to any Module you have.
<System.Runtime.CompilerServices.Extension()> _
Public Sub Invoke(ByVal control As Control, ByVal action As Action)
If control.InvokeRequired Then
control.Invoke(New MethodInvoker(Sub() action()), Nothing)
Else
action.Invoke()
End If
End Sub
Now you can write Cross-Thread Control code that's only 1 line long for any control call.
Like this, lets say you want to clear a ComboBox and it's called from threads or without threads you can just use do this now
cboServerList.Invoke(Sub() cboServerList.Items.Clear())
Want to add something after you clear it?
cboServerList.Invoke(Sub() cboServerList.Items.Add("Hello World"))