Access the values of a form from an EventHandler - vb.net

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

Related

How can i call my function from my main form in another form without this error in vb.net

I am currently developping an app WinForm using vb.net and i have to call a function, situated in my main form, in another form. Here is my function in my main form
Public Sub ImprimCalibrage(t1 As String, t2 As String)
Dim strCalibr As String = Build_str("CAL", "CALIBRAGE", t1, t2)
BuildFile("fileT.txt", strCalibr)
Print()
End Sub
To call it, i have created an attribute in my secondary form like this
ReadOnly form1 = Application.OpenForms("Main")
And now i call my function doing this form1.ImprimCalibrage(TextBox1.Text, TextBox2.Text)
But when i click on the button which supposed excute the function, i got this error
'Object variable or With block variable not set.'
I don't have any idea how to solve it because i dont have with block and my only variable in just a String.
Application.OpenForms("Main") is returning Nothing because at the time it is called (when Form2 is created) , there is no open form named Main in the collection. This Nothing then persists until you try and use it. You can't call a method on Nothing. Even if you later ensure there is a form named Main in the OpenForms collection, it doesn't alter the fact that at the time you looked (and captured) there was nothing
Move the code to just before you need it
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim form1 As Form1 = Application.OpenForms("Main")
form1.ImprimCalibrage(TextBox1.Text, TextBox2.Text)
End Sub

vb.net a forms "new" routine is being called before I even invoke the form

I have a form called "partmanager". On it is a button to show another form "parteditor" to allow editing details of a part. Clicking that button will show the form and pass in a variable to the parteditors "new" routine.
My problem is that when the calling form (partmanager) starts, it immediately calls new routine in the parteditor form before it (partmanager) is even initialized so the parteditor form does not get the string that is supposed to be passed in. Later, when the calling form is visible and I click the button to show the parteditor form, new has already been prematurely called and so is not called again and the form does not get the string passed in.
I hope this makes sense!
I can implement a property in the parteditor form and pass in my variable that way prior to showing the form and that will work, thereby not even requiring a "new" routine in the parteditor forms code.
So my question is, is implementing the property the proper way to pass this variable to the form being called, or am I not properly coding my forms? (I also have an intermediary module called "commands" where I have been defining command procedures, in this case just showing a form.)
any pointers would be appreciated, thanks!
here is the code for the button in the calling form:
Private Sub EditButton_Click(sender As Object, e As EventArgs) Handles EditButton.Click
Commands.EditPart(_PartNumber) 'call the editpart command
Me.Close()
Me.Dispose()
End Sub
here is the code for the form being called:
Public Class PartEditForm
Private _partNumber As String = String.Empty
Public Sub New(partNumber As String)
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_partNumber = partNumber
End Sub
Private Sub PartEditForm_Load(sender As Object, e As EventArgs) Handles Me.Load
Label1.Text = _partNumber
End Sub
End Class
and here is the code in my "commands" module for loading/showing the form:
Public PartEditForm As New PartEditForm(_partNumber)
Public Sub EditPart(partnumber As String)
If PartEditForm.IsDisposed Then
PartEditForm = New PartEditForm(partnumber)
End If
PartEditForm.Show()
End Sub
you can save the routine in a dim variable and get this on the other form and close the first form or hide but you need get in new var and contine where stop before.

VB.NET Windows Forms Variables Passing

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

How to make a loader in a separate thread?

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.

How to trigger WinForms button click from remote thread

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