I've got an application that I've developed in VB 2010. It consists of a series of Windows forms and a listener process which listens to input from a MIDI device (a keyboard etc). This listener process runs in a separate thread.
Using a callback function and the invoke method I can update the value of a text box on one of the forms with the value of the notes played on the MIDI device (code below).
What I can't seem to do is to trigger a button press on one of the buttons so that a piece of code runs on the same form using another callback function and invoke.
I've Googled around and can't find any working examples so I'm not sure that this is possible - I hope it is because this is the last 1% of my project !
Here's what works to update a text box on the form, what would I need to modify in order to click a button on the same form called btn_NextSection ?
' Because the MIDI listener process runs in a different thread
' it can read the states of controls on the parent form but it
' cannot modify them so we need to identify the parent process
' and update the text box as that one instead
' Set up the callback
Private Delegate Sub SetNextSectionTextCallback(ByVal [text] As String)
' Updates NextSection
Private Sub SetNextSectionText(ByVal [text] As String)
' Function to see if the thread that tries to update the
' text box is the same as the one that started it. If it
' isn't then locate parent process and update as that one
If callerInstance.txt_NextSection.InvokeRequired Then
Dim d As New SetNextSectionTextCallback(AddressOf SetNextSectionText)
callerInstance.Invoke(d, New Object() {[text]})
Else
callerInstance.txt_NextSection.Text = [text]
End If
End Sub
Thanks in advance !
Assuming that you want to run the code, not simulate the button click, try something like this. Invoke requires a function so you may need to create a dummy return object.
Private Sub btn_NextSection_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles btn_NextSection.Click
NextSection()
End Sub
Private Function NextSection() As Object
'do something on the UI thread
Return Nothing ' to satisfy the requirements of Invoke
End Function
Private Sub AnotherThreadHere()
'call NextSection on the UI thread
Application.Current.Dispatcher.Invoke(Windows.Threading.DispatcherPriority.Background, New Action(Function() NextSection()))
End Sub
Related
I have a program, where a user fills out a form, then clicks a button that opens a new window on top of the form. That window waits on data from a Port and executes a DataReceived Event Handler.
I want this event to process the data (into a simple string) and then run a method from the main form window, that will compile the form data with the input data and send it further.
However, when executing the method from the Event Handler I cannot access the values of the form - it produces errors, as if all the form elements were completely blank.
It seems the event is running in a new thread, which cannot access the main instance of the form. Is there any way of running the event on the main thread? Or give it the access? I have tried to use global variables, but even they are blank when read from the event... I have been stuck on this for almost two weeks now and I am completely lost...
How can I access the values of form fields inside that event? What is the simplest way of doing this?
EDIT: I want to add more detail on the problem. I have tried following some advice online and tried to use Delegates. This is simplified version of what I have (still not working):
There are two classes: frmMain and frmPortReader.
On start, frmMain opens in a new window. It contains a combobox (cbInput) and button (btnSend). Upon clicking btnSend:
If frmPortReader.IsHandleCreated Then
frmPortReader.Close()
End If
frmPortReader.Show()
The frmPortReader does some things (irrelevant to this problem), but it's main function is to trigger an event when the port receives data.
AddHandler readerSerial.DataReceived, AddressOf DataReceivedHandler
Private Sub DataReceivedHandler(sender As Object, e As SerialDataReceivedEventArgs)
' Larger code that reads the actual input, processes it, and assigns it to the variable indata
frmMain.ExecuteEvent(indata)
End Sub
Then, in the frmMain class:
Delegate Sub MF(inputData As String)
Dim DisplayData As New MF(AddressOf TestDisplay)
Private Sub TestDisplay(indata As String)
MsgBox("Received data: " & indata & "Currently Selected Index: " & cbInput.SelectedIndex)
End Sub
Public Sub ExecuteEvent(inData As String)
MsgBox("Before Delegate: " & cbInput.selectedIndex)
Dim paramsArray(1) As Object
paramsArray(0) = inData
BeginInvoke(DisplayData, paramsArray)
End Sub
When executing, the ExecuteEvent method displays two MessageBoxes with the data it reads from the form (selected index of ComboBox). The first one, being read directly without a Delegate, produces -1, while the one inside Delegate produces error: "cannot call invoke or begininvoke on a control until the window handle is created"
I think Hursey nailed it. You're using the default instance of frmMain when that is not the form that is being displayed on your screen.
Change:
frmPortReader.Show()
To:
frmPortReader.Show(Me)
Now, in frmPortReader, you can get a reference to the instance of frmMain using the Owner property:
Private Sub DataReceivedHandler(sender As Object, e As SerialDataReceivedEventArgs)
' Larger code that reads the actual input, processes it, and assigns it to the variable indata
Dim main As frmMain = DirectCast(Me.Owner, frmMain)
main.ExecuteEvent(indata)
End Sub
I made a file search program in visual studio on windows 10 using .net lang,
My problem starts from form1 with a "dim frm2 as form2 = new form2" call,
after the new form being shown i start a while loop on form1 that feeds data into a listbox in form 2:
1)form1 call form2 and show it.
2)form1 start a while loop.
3)inside the while loop data being fed to listbox1 in frm2
Now everything works on windows 10, the while loop can run as much as it needs without any trouble, the window can loose focus and regain focus without showing any "Not Responding.." msgs or white\black screens..
But, when i take the software to my friend computer which is running windows 7, install all required frameworks and visual studio itself, run it from the .sln in debug mode, and do the same search on the same folder the results are:
1) the while loop runs smoothly as long as form 2 dont loose focus
(something that doesnt happen on windows 10)
2) when i click anywhere on the screen the software loose focus what
causes 1) to happen (black screen\white screen\not responding etc..)
3) if i wait the time needed for the loop and dont click anywhere else
it keeps running smoohtly, updating a label like it should with the
amount of files found.. and even finish the loop with 100% success
(again unless i click somewhere)
Code Example:
Sub ScanButtonInForm1()
Dim frm2 As Form2 = New Form2
frm2.Show()
Dim AlreadyScanned As HashSet(Of String) = New HashSet(Of String)
Dim stack As New Stack(Of String)
stack.Push("...Directoy To Start The Search From...")
Do While (stack.Count > 0)
frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
frm2.Label4.Refresh()
Dim ScanDir As String = stack.Pop
If AlreadyScanned.Add(ScanDir) Then
Try
Try
Try
Dim directoryName As String
For Each directoryName In System.IO.Directory.GetDirectories(ScanDir)
stack.Push(directoryName)
frm2.Label4.Text = "-- Mapping Files... -- Folders Left:" + stack.Count.ToString + " -- Files Found:" + frm2.ListBox1.Items.Count.ToString + " --"
frm2.Label4.Refresh()
Next
frm2.ListBox1.Items.AddRange(System.IO.Directory.GetFiles(ScanDir, "*.*", System.IO.SearchOption.AllDirectories))
Catch ex5 As UnauthorizedAccessException
End Try
Catch ex2 As System.IO.PathTooLongException
End Try
Catch ex4 As System.IO.DirectoryNotFoundException
End Try
End If
Loop
End Sub
My conclusions was simple!
1) windows 7 dont support live ui (label) update from a while loop
called from a button...
2) windows 7 could possibly support a new
thread running the same loop
i think mabye if i run all the code in a thread mabye the ui will remain responsive
(by the way the UI is not responsive in windows 10 but i still see
the label refresh and nothing crashes when form loose focus..)
so i know how to do that but i also know that if i do that a thread will not be able to update a listbox or a label in a form and refresh it..
so the thread will need to update an external file with the data and the form2 will need to read that data live from the file but will it make the same problems? i have no idea what to do.. can use some help and tips. THANK YOU!
I must menttion the fact that the loop is working on windows 10 without a responsive UI means i cant click on any button but i can
still see the label refresh BUT on windows 7 everything works the same
UNLESS i click somewhere, no matter where i click on windows the loop
crashes
im using framework 4.6.2 developer
While I'm glad you found a solution, I advise against using Application.DoEvents() because it is bad practice.
Please see this blog post: Keeping your UI Responsive and the Dangers of Application.DoEvents.
Simply put, Application.DoEvents() is a dirty workaround that makes your UI seem responsive because it forces the UI thread to handle all currently available window messages. WM_PAINT is one of those messages which is why your window redraws.
However this has some backsides to it... For instance:
If you were to close the form during this "background" process it would most likely throw an error.
Another backside is that if the ScanButtonInForm1() method is called by the click of a button you'd be able to click that button again (unless you set Enabled = False) and starting the process once more, which brings us to yet another backside:
The more Application.DoEvents()-loops you start the more you occupy the UI thread, which will cause your CPU usage to rise rather quickly. Since every loop is run in the same thread your processor cannot schedule the work over different cores nor threads, so your code will always run on one core, eating as much CPU as possible.
The replacement is, of course, proper multithreading (or the Task Parallel Library, whichever you prefer). Regular multithreading actually isn't that hard to implement.
The basics
In order to create a new thread you only need to declare an instance of the Thread class and pass a delegate to the method you want the thread to run:
Dim myThread As New Thread(AddressOf <your method here>)
...then you should set its IsBackground property to True if you want it to close automatically when the program closes (otherwise it keeps the program open until the thread finishes).
Then you just call Start() and you have a running background thread!
Dim myThread As New Thread(AddressOf myThreadMethod)
myThread.IsBackground = True
myThread.Start()
Accessing the UI thread
The tricky part about multithreading is to marshal calls to the UI thread. A background thread generally cannot access elements (controls) on the UI thread because that might cause concurrency issues (two threads accessing the same control at the same time). Therefore you must marshal your calls to the UI by scheduling them for execution on the UI thread itself. That way you will no longer have the risk of concurrency because all UI related code is run on the UI thread.
To marhsal calls to the UI thread you use either of the Control.Invoke() or Control.BeginInvoke() methods. BeginInvoke() is the asynchronous version, which means it doesn't wait for the UI call to complete before it lets the background thread continue with its work.
One should also make sure to check the Control.InvokeRequired property, which tells you if you already are on the UI thread (in which case invoking is extremely unnecessary) or not.
The basic InvokeRequired/Invoke pattern looks like this (mostly for reference, keep reading below for shorter ways):
'This delegate will be used to tell Control.Invoke() which method we want to invoke on the UI thread.
Private Delegate Sub UpdateTextBoxDelegate(ByVal TargetTextBox As TextBox, ByVal Text As String)
Private Sub myThreadMethod() 'The method that our thread runs.
'Do some background stuff...
If Me.InvokeRequired = True Then '"Me" being the current form.
Me.Invoke(New UpdateTextBoxDelegate(AddressOf UpdateTextBox), TextBox1, "Status update!") 'We are in a background thread, therefore we must invoke.
Else
UpdateTextBox(TextBox1, "Status update!") 'We are on the UI thread, no invoking required.
End If
'Do some more background stuff...
End Sub
'This is the method that Control.Invoke() will execute.
Private Sub UpdateTextBox(ByVal TargetTextBox As TextBox, ByVal Text As String)
TargetTextBox.Text = Text
End Sub
New UpdateTextBoxDelegate(AddressOf UpdateTextBox) creates a new instance of the UpdateTextBoxDelegate that points to our UpdateTextBox method (the method to invoke on the UI).
However as of Visual Basic 2010 (10.0) and above you can use Lambda expressions which makes invoking much easier:
Private Sub myThreadMethod()
'Do some background stuff...
If Me.InvokeRequired = True Then '"Me" being the current form.
Me.Invoke(Sub() TextBox1.Text = "Status update!") 'We are in a background thread, therefore we must invoke.
Else
TextBox1.Text = "Status update!" 'We are on the UI thread, no invoking required.
End If
'Do some more background stuff...
End Sub
Now all you have to do is type Sub() and then continue typing code like if you were in a regular method:
If Me.InvokeRequired = True Then
Me.Invoke(Sub()
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
ProgressBar1.Value += 1
End Sub)
Else
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
ProgressBar1.Value += 1
End If
And that's how you marshal calls to the UI thread!
Making it simpler
To make it even more simple to invoke to the UI you can create an Extension method that does the invoking and InvokeRequired check for you.
Place this in a separate code file:
Imports System.Runtime.CompilerServices
Public Module Extensions
''' <summary>
''' Invokes the specified method on the calling control's thread (if necessary, otherwise on the current thread).
''' </summary>
''' <param name="Control">The control which's thread to invoke the method at.</param>
''' <param name="Method">The method to invoke.</param>
''' <param name="Parameters">The parameters to pass to the method (optional).</param>
''' <remarks></remarks>
<Extension()> _
Public Function InvokeIfRequired(ByVal Control As Control, ByVal Method As [Delegate], ByVal ParamArray Parameters As Object()) As Object
If Parameters IsNot Nothing AndAlso _
Parameters.Length = 0 Then Parameters = Nothing
If Control.InvokeRequired = True Then
Return Control.Invoke(Method, Parameters)
Else
Return Method.DynamicInvoke(Parameters)
End If
End Function
End Module
Now you only need to call this single method when you want to access the UI, no additional If-Then-Else required:
Private Sub myThreadMethod()
'Do some background stuff...
Me.InvokeIfRequired(Sub()
TextBox1.Text = "Status update!"
Me.Text = "Hello world!"
Label1.Location = New Point(128, 32)
End Sub)
'Do some more background stuff...
End Sub
Returning objects/data from the UI with InvokeIfRequired()
With my InvokeIfRequired() extension method you can also return objects or data from the UI thread in a simple manner. For instance if you want the width of a label:
Dim LabelWidth As Integer = Me.InvokeIfRequired(Function() Label1.Width)
Example
The following code will increment a counter that tells you for how long the thread has run:
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Dim CounterThread As New Thread(AddressOf CounterThreadMethod)
CounterThread.IsBackground = True
CounterThread.Start()
Button1.Enabled = False 'Make the button unclickable (so that we cannot start yet another thread).
End Sub
Private Sub CounterThreadMethod()
Dim Time As Integer = 0
While True
Thread.Sleep(1000) 'Wait for approximately 1000 ms (1 second).
Time += 1
Me.InvokeIfRequired(Sub() Label1.Text = "Thread has been running for: " & Time & " seconds.")
End While
End Sub
Hope this helps!
The reason your application is freezing is that you are doing all the work on the UI thread. Check out Async and Await. It uses threading in the background but makes it way easier to manage. An example here:
https://stephenhaunts.com/2014/10/14/using-async-and-await-to-update-the-ui-thread/
I have got a windows form with a Combobox and two datepickers objects in it. I want upon user selection of the values to pass the variables values to another windows form.
All I've seen is how to do this with showDialog method on an instance of the class, however this doesn't work for me as the user has to select a user from Combobox and pick the date range and click on search button.
A quick straight forward help will be appreciated as my time is running close to the deadline.
Thanks.
you can do something like this
1- creation of public event, singleton approach
Public Class EventClass
Private Sub EventClass()
End Sub
Public Shared Sub Invoke(sender as object, value as object)
RaiseEvent OnValueChange(sender,value) ' be sure OnValueChange is not nothing first
End Sub
Public Shared Event OnValueChange(sender as object,value as object)
End Class
2- raising the event, in the form that containing the combobox and datepicker
Handle the event of the combo on selected index changed
inside the event of combo raise the event as
EventClass.Invoke(ComboBox1,ComboBox1.SelectedValue)
and in the case of DateTimePicker use
EventClass.Invoke(DateTimePicker1,DateTimePicker.Date)
3- in the form that you want to pass the values to
public sub EventClass_OnValueChange(sender as object, value as object) handles EventClass.OnValueChange
' do your code here but be sure that this form was created before the form that contains the invoke or it will not be fired
end sub
hope this will help you
I recently was dealing with this error: BeginInvokeStackflowError
I am using threading,and according to my research it is because within the threading .start() event it calls .invoke. If that is done in the mainform_Load event, before it is ready, then you get a BeginInvoke error.
So I've move my code from the load to the shown event. However, there is a lot of stuff going on in the background that I don't want the user to see. Is there a way in my code to extend the splashscreen I have to wait until the mainwindow shown is finished for only the first time?
Private Sub MainWindow_Shown(sender As Object, e As EventArgs) Handles Me.Shown
'update table /search network
updateTable()
'clean
cleanupTable()
'fix label
updateLabel()
End Sub
Your app can be started other than the default "MainForm" method provided by the VB Application Framework. This will use a Sub Main as the starting point allowing you to control what forms show and when, and what happens before that:
' IF your form is declared here, it will be
' available to everything. e.g.:
' Friend mainfrm As Form1
Public Sub Main()
' this must be done before any forms/controls are referenced
Application.EnableVisualStyles()
' the main form for the app
' just "mainfrm = New Form1" if declared above
Dim mainfrm As New Form1
' eye candy for the user
Dim splash As New SplashScreen1
splash.Show()
' your code here to do stuff.
' you can also invoke your procedures on the main form
' mainfrm.FirstTimeSetup()
' for demo purposes
System.Threading.Thread.Sleep(2500)
' close/hide the splash once you are done
splash.Close()
' see note
' start the app with the main form
Application.Run(mainfrm)
End Sub
Add a module to the project, typically "program"
Add your Sub Main
Go to Project Properties, uncheck use Application Framework
Select Sub Main in the the StartUp object drop down
If you declare the splash screen as Friend at the top, you can truly extend it until all the form's load event is complete and close it there/then.
There is no way to extend the splash screen if you've implemented it using the project settings, however you can use the splash screen form as the initial form instead of your main form. As for waiting until the thread has finished to show the form (or hide the splash screen) consider using a public Boolean in the main form and change it to True once the thread has completed. You can use a timer on the splash screen to check for this Boolean change and then change the form's opacity back to 1.
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.