Vb.net - Cross-threading exception by closing a form - vb.net

I'm working on an application to read something from a serial port (COMM-port).
In short, it works like this: when you work in a bar or restaurant, before you can enter something in the register, you have to scan a sort of card. If this card returns a good number, you can enter something.
So, there has to be a form that listens to the serial port and checks whether someone scans a card and if it's a card with good rights.
If the person has the good rights, the form can be closed and another form is called.
Now, in code:
Here, the MenuForm is loaded (the form that has to be accesible after the correct code was read). I call the frmWaiterKey to show up.
Private Sub frmMenu_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim oForm As frmWaiterKey = New frmWaiterKey()
oForm.ShowDialog()
End Sub
The code of the class frmWaiterKey:
Private Sub frmWaiterKey_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
nameArray = SerialPort.GetPortNames
OpenComPort()
AddHandler myComPort.DataReceived, SerialDataReceivedEventHandler1
End Sub
Sub OpenComPort()
Try
' Get the selected COM port's name
' from the combo box.
If Not myComPort.IsOpen Then
myComPort.PortName = _
nameArray(0).ToString()
' Get the selected bit rate from the combo box.
myComPort.BaudRate = CInt(9600)
' Set other port parameters.
myComPort.Parity = Parity.None
myComPort.DataBits = 8
myComPort.StopBits = StopBits.One
myComPort.Handshake = Handshake.None
'myComPort.ReadTimeout = 3000
'myComPort.WriteTimeout = 5000
' Open the port.
myComPort.Open()
End If
Catch ex As InvalidOperationException
MessageBox.Show(ex.Message)
Catch ex As UnauthorizedAccessException
MessageBox.Show(ex.Message)
Catch ex As System.IO.IOException
MessageBox.Show(ex.Message)
End Try
End Sub
Sub CloseComPort()
Using myComPort
If (Not (myComPort Is Nothing)) Then
' The COM port exists.
If myComPort.IsOpen Then
' Wait for the transmit buffer to empty.
Do While (myComPort.BytesToWrite > 0)
Loop
End If
End If
End Using
End Sub
Private SerialDataReceivedEventHandler1 As New SerialDataReceivedEventHandler(AddressOf DataReceived)
' Specify the routine that runs when
' a DataReceived event occurs at myComPort.
' This routine runs when data arrives at myComPort.
Friend Sub DataReceived(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs)
Dim newReceivedData As String
' Get data from the COM port.
newReceivedData = myComPort.ReadExisting
newReceivedData = newReceivedData.Trim()
MsgBox(newReceivedData)
If newReceivedData.Equals("00150324294764") Then
CloseComPort()
Me.Close()
End If
End Sub
I get an error in the last line: Me.Close()
I get the point: I call the form frmWaiterKey from the frmMenu and can't close it here...
But I have no idea how to solve this problem.
I hope someone can help me or tell me what I'm doing wrong.

First, you need to make a method like this:
Private Sub CloseMe()
If Me.InvokeRequired Then
Me.Invoke(New MethodInvoker(AddressOf CloseMe))
Exit Sub
End If
Me.Close()
End Sub
Then, close your form by calling that method, like this:
If newReceivedData.Equals("00150324294764") Then
CloseComPort()
CloseMe()
End If
The reason this is necessary is because all UI activity in WinForms must be performed from the same thread. Since the DataReceived method is being called from another thread, it must get back onto the UI thread before it can close the form. The InvokeRequired property returns true if you are on any thread other than the UI thread, and the Invoke method invokes the given method from the UI thread.

Related

Close form in DataRecieved event

i have form that captures a hex code from a serial port. i want to close the form as soon as the hex is captured. but i get an error and cant close the form from DataReceived event.
my code :
Public hex As String
Dim sp As SerialPort
Private Sub SerialPort1_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
Dim t_hex As String = sp.ReadLine()
If Len(t_hex) < 21 Then
Exit Sub
End If
t_hex = Mid(t_hex, 11, 11)
hex = t_hex
sp.Close()
Me.Close() '' ERROR LINE
End Sub
the error :
An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll
Additional information: Cross-thread operation not valid: Control 'Dlg_CardRead' accessed from a thread other than the thread it was created on.
what is the correct way of approaching this and closing the form ?
thanks
found the answer :
Public hex As String
Dim sp As SerialPort
Private Sub SerialPort1_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
Dim t_hex As String = sp.ReadLine()
If Len(t_hex) < 21 Then
Exit Sub
End If
t_hex = Mid(t_hex, 11, 11)
hex = t_hex
sp.Close()
CloseMe()
End Sub
Private Sub CloseMe()
If Me.InvokeRequired Then
Me.Invoke(New MethodInvoker(AddressOf CloseMe))
Exit Sub
End If
Me.Close()
End Sub
EDIT :
the problem with the first code was that i was calling methods from Win Forms thread inside another thread. so in another words DataReceived event runs in another thread and turns out i can't use Me.Close() inside that thread.
The InvokeRequired property returns true if you are on any thread other than the UI thread, and the Invoke method invokes the given method from the UI thread.

How to put received serial text into multiple text boxes?

I am doing a serial communication project and would like to have the received string go into a text box based on which button was clicked to send the initial string and invoke a response.
The code for the ReceivedText is:
PrivateSub ReceivedText(ByVal [text] As String)
Button1.Clear()
Button2.Clear()
If Button1.InvokeRequired Then
RichTextBox1.text = [text].Trim("!")
End If
If Button2.InvokeRequired Then
RichTextBox2.Text = [text].Trim("!")
End If
EndSub
This just results in the received string going into both of the boxes instead of one or the other.
Is there any way to get the text to go in the appropriate box?
The key to remember is that .Net treats all serial comm as threads. Let me give you a simple example of updating textboxes from one of my programs that reads data from a scale.
Private Sub ComScale_DataReceived(sender As System.Object, e As System.IO.Ports.SerialDataReceivedEventArgs) Handles ComScale.DataReceived
If ComScale.IsOpen Then
Try
' read entire string until .Newline
readScaleBuffer = ComScale.ReadLine()
'data to UI thread because you can't update the GUI here
Me.BeginInvoke(New EventHandler(AddressOf DoScaleUpdate))
Catch ex As Exception : err(ex.ToString)
End Try
End If
End Sub
You'll note a routine DoScaleUpdate is invoked which does the GUI stuff:
Public Sub DoScaleUpdate(ByVal sender As Object, ByVal e As System.EventArgs)
Try
'getAveryWgt just parses what was read into something like this {"20.90", "LB", "GROSS"}
Dim rst() As String = getAveryWgt(readScaleBuffer)
txtWgt.Text = rst(0)
txtUom.Text = rst(1)
txttype.Text = rst(2)
Catch ex As Exception : err(ex.ToString)
End Try
End Sub
You CAN make it much more complicated if you choose (see post #15 of this thread for an example) but this should be enough to do what you need.

Closing form with Gif throws InvalidOperationException

This is clearly a problem of me not understanding how to properly setup a UI thread, but I can't figure out how to fix it.
I have a datagridview where I click a button, get the information from the network, and then display it on the datagridview with the new data. While it is on the network I have a form I show with an updating gif, a form I called "loading". Within that form I have the gif updating using the typical OnFrameChanged and m_isAnimating code that is on the internet.
However, no matter what format I use, I always get this exception caught here:
Public loader As New Loading
Private Sub OnFrameChanged(ByVal o As Object, ByVal e As EventArgs)
Try ' If animation is allowed call the ImageAnimator UpdateFrames method
' to show the next frame in the animation.
Me.Invalidate()
If m_IsAnimating Then
ImageAnimator.UpdateFrames()
Me.Refresh()
'Draw the next frame in the animation.
Dim aGraphics As Graphics = PictureBox1.CreateGraphics
aGraphics.DrawImage(_AnimatedGif, New Point(0, 0))
aGraphics.Dispose()
End If
Catch ex As InvalidOperationException
End Try
End Sub
And it usually says something along the lines of "was accessed from a thread it wasn't created on" or "Cannot access a disposed object. Object name: 'PictureBox'."
But I don't know why that is, since I am creating a new instance here every time. Here's the button's code:
Private Sub btnSlowSearch_Click(sender As Object, e As EventArgs) Handles btnSlowSearch.Click
Me.Cursor = Cursors.WaitCursor
'get datatable
loader.Show()
BWorkerLoadProp.RunWorkerAsync() 'go get data on network
'bworker will update datagridview with new data
'wait for worker to finish
If BWorkerLoadProp.IsBusy Then
Threading.Thread.Sleep(1)
End If
loader.Close()
End Sub
I realize it isn't very good code, but I have tried putting the loader inside the background worker, I have tried whatever. But no matter what the exception is called.
What's the proper way to show another updating form as I do background work?
The behavior documented is difficult to reproduce.
Probably something between the thread switching causes a call to OnFrameChanged after the call to close in the btnSlowSearch_Click.
In any case logic seems to suggest to call the ImageAnimator.StopAnimate in the close event of the form that shows the animation
So looking at your comment above I would add the following to your animator form
// Not needed
// Public loader As New Loading
Private Sub OnFrameChanged(ByVal o As Object, ByVal e As EventArgs)
Try
Me.Invalidate()
If m_IsAnimating Then
ImageAnimator.UpdateFrames()
Me.Refresh()
'Draw the next frame in the animation.
Dim aGraphics As Graphics = PictureBox1.CreateGraphics
aGraphics.DrawImage(_AnimatedGif, New Point(0, 0))
aGraphics.Dispose()
End If
Catch ex As InvalidOperationException
.. do not leave this empty or remove altogether
End Try
End Sub
Private Sub Form_Closing(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
... if you need to stop the closing you should do it here without stopping the animation
If m_IsAnimating Then
ImageAnimator.StopAnimate(AnimatedGif, _
New EventHandler(AddressOf Me.OnFrameChanged))
m_isAnimating = False
End If
End Sub
This is certainly not the only way to do this but I will provide you the simplest working example in hopes that it will help you to correct your own application.
1) Create a new vb.net windows forms application and add a button (Button1) onto the form.
2) Change the Form1 code to this:
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
If fLoading Is Nothing Then ' can only show one loading screen at a time
Dim oLoadingThread As clsLoadingThread = New clsLoadingThread ' creat new thread
oLoadingThread.ShowWaitScreen() ' show the loading screen
'-----------------------------------------
' your real processing would go here
'-----------------------------------------
For i As Int32 = 0 To 999999
Application.DoEvents()
Next
'-----------------------------------------
oLoadingThread.CloseLoadingScreen() ' we are done processing so close the loading form
oLoadingThread = Nothing ' clear thread variable
End If
End Sub
End Class
Public Class clsLoadingThread
Dim oThread As System.Threading.Thread
Private Delegate Sub CloseLoadingScreenDelegate()
Public Sub ShowWaitScreen()
' create new thread that will open the loading form to ensure animation doesn't pause or stop
oThread = New System.Threading.Thread(AddressOf ShowLoadingForm)
oThread.Start()
End Sub
Private Sub ShowLoadingForm()
Dim fLoading As New frmLoading
fLoading.ShowDialog() ' Show loading form
If fLoading IsNot Nothing Then fLoading.Dispose() : fLoading = Nothing ' loading form should be closed by this point but dispose of it just in case
End Sub
Public Sub CloseLoadingScreen()
If fLoading.InvokeRequired Then
' Since the loading form was created on a seperate thread we need to invoke the thread that created it
fLoading.Invoke(New CloseLoadingScreenDelegate(AddressOf CloseLoadingScreen))
Else
' Now we can close the form
fLoading.Close()
End If
End Sub
End Class
Module Module1
Public fLoading As frmLoading
End Module
3) Add a new form and call it frmLoading. Add a picturebox to the form and set the image to your updating gif.
4) Change the frmLoading code to this:
Public Class frmLoading
Private Sub frmLoading_Load(sender As Object, e As EventArgs) Handles Me.Load
fLoading = Me ' ensure that the global loading form variable is set here so we can use it later
End Sub
Private Sub frmLoading_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
fLoading = Nothing ' clear the global loading form since the form is being disposed
End Sub
End Class
Normally I would add the clsLoadingThread Class and Module1 Module to their own files but it's easier to show the code to you this way.

Open a modal form from a background thread to block UI thread without also blocking background thread

Maybe this is a simple question and I just don't know the correct search terms to find the answer, but my Google-fu has failed me on this one.
My vb.net application has a background thread that controls all the socket communication. Occasionally, I need this communication thread to open up a modal form to display a message and block UI interaction until the communication thread completes a series of tasks at which point, the communication thread will remove the modal form, allowing the user to continue interaction.
Currently, my communications class containing the background thread has two events, StartBlockingTask and EndBlockingTask. My main form has event listeners for these events that call like-named subs. They call code looking like this:
Private Delegate Sub BlockingDelegate(ByVal reason As String)
Private Sub StartBlockingTask(ByVal reason As String)
If Me.InvokeRequired Then
Dim del As New BlockingDelegate(AddressOf StartBlockingTask)
Me.Invoke(del, New Object() {reason})
Else
Try
_frmBlock.lblBlock.Text = reason
_frmBlock.ShowDialog()
Catch ex As Exception
'stuff
End Try
End If
End Sub
Private Sub EndBlockingTask()
If Me.InvokeRequired Then
Dim del As New BlockingDelegate(AddressOf EndBlockingTask)
Me.Invoke(del, New Object() {""})
Else
Try
If (Not _frmBlock Is Nothing) Then
_frmBlock.DialogResult = Windows.Forms.DialogResult.OK
End If
Catch ex As Exception
'stuff
End Try
End If
End Sub
This successfully blocks the UI from interaction, but it also blocks the communications thread so the EndBlockingTask event never actually gets raised. How can I open this modal dialog from the communications thread and allow the communications thread to still continue running?
Thanks in advance!
I disagree.
All that needs to be done is to change Invoke() to BeginInvoke() and you're golden.
This is because Invoke() is actually synchronous which causes it block until ShowDialog() resolves.
Using BeginInvoke() makes it asynchronous and allows the UI to be blocked while the thread continues:
Public Class Form1
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
If Not BackgroundWorker1.IsBusy Then
BackgroundWorker1.RunWorkerAsync()
End If
End Sub
Private Delegate Sub BlockingDelegate(ByVal reason As String)
Private Sub StartBlockingTask(ByVal reason As String)
If Me.InvokeRequired Then
Dim del As New BlockingDelegate(AddressOf StartBlockingTask)
Me.BeginInvoke(del, New Object() {reason})
Else
Try
_frmBlock.lblBlock.Text = reason
_frmBlock.ShowDialog()
Catch ex As Exception
'stuff
End Try
End If
End Sub
Private Sub EndBlockingTask()
If Me.InvokeRequired Then
Dim del As New BlockingDelegate(AddressOf EndBlockingTask)
Me.BeginInvoke(del, New Object() {""})
Else
Try
If (Not _frmBlock Is Nothing) Then
_frmBlock.DialogResult = Windows.Forms.DialogResult.OK
End If
Catch ex As Exception
'stuff
End Try
End If
End Sub
Private Sub BackgroundWorker1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
For i As Integer = 1 To 10
BackgroundWorker1.ReportProgress(i)
System.Threading.Thread.Sleep(1000)
If i = 4 Then
Dim del As New BlockingDelegate(AddressOf StartBlockingTask)
del("bada...")
ElseIf i = 7 Then
Dim del As New BlockingDelegate(AddressOf EndBlockingTask)
del("bing!")
End If
Next
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Label1.Text = e.ProgressPercentage
End Sub
End Class
You are calling the address from within the sub it is created in. The Address needs to be called from outside this sub.
Private Sub StartBlockingTask(ByVal reason As String)
If Me.InvokeRequired Then
Dim del As New BlockingDelegate(AddressOf StartBlockingTask)
Private Sub EndBlockingTask()
If Me.InvokeRequired Then
Dim del As New BlockingDelegate(AddressOf EndBlockingTask)
You need to create two delegates. One for StartBlockingTask and one for EndBlockingTask
This is an example from MSDN,
Delegate Sub MySubDelegate(ByVal x As Integer)
Protected Sub Test()
Dim c2 As New class2()
' Test the delegate.
c2.DelegateTest()
End Sub
Class class1
Sub Sub1(ByVal x As Integer)
MessageBox.Show("The value of x is: " & CStr(x))
End Sub
End Class
Class class2
Sub DelegateTest()
Dim c1 As Class1
Dim msd As MySubDelegate
c1 = New Class1()
' Create an instance of the delegate.
msd = AddressOf c1.Sub1
msd.Invoke(10) ' Call the method.
End Sub
End Class
http://msdn.microsoft.com/en-us/library/5t38cb9x(v=vs.71).aspx
Let me know if this helps.

SerialPort and Control Updating in MDI form

As my title implies i have the following problem, i am receiving data from serial port and i update a richtextbox in a MDI Form with the control.invoke method
(Code in SerialPort.DataReceived Event)
If myTerminal.Visible Then
myTerminal.MyRichTextBox1.Invoke(New MethodInvoker(Sub()
myTerminal.MyRichTextBox1.AppendText(dataLine & vbCrLf)
End Sub))
End If
But as a mdi form it has the ability to close and reopen. So when the serialport is sending data to richtextbox and the user click the close button and the form gets disposed. Then the error "Invoke or BeginInvoke cannot be called on a control until the window handle has been created."... Any Idea????
My regards,
Ribben
That code is not in the SerialPort.DataReceived event it is in the event handler. (Yes, I'm nitpicking, but it points to a solution.) The best thing to do is have the form that owns myTerminal add the handler when it is created and remove the handler when it closes.
Thank you for your answer but unfortunately that's not the solution. First of all my SerialPort Class must inform 2 Forms (Form with richtextbox, Form with Listview) and another class which is responsible for drawing (Unmanaged Directx 9.0c about 4 Forms), so to implement right the serialport class i have made my own events. Again to the problme, it caused because the Serialport.DataReceived everytime it occurs creates a thread in the threadpool and when i dispose the form simply it's too slow to catch up with all the threads and so there is at least one thread which invokes the control which is already disposed!
As a temp solution i came up with (The Below code is in the TerminalForm Class which inherits Form):
Private VisibleBoolean As Boolean = False
Private Index As Integer = 0
Private Sub DataToAppend(ByVal _text As String)
If VisibleBoolean Then
Me.MyRichTextBox1.Invoke(New MethodInvoker(Sub()
Me.MyRichTextBox1.AppendText(_text & vbCrLf)
End Sub))
ElseIf Index = 1 Then
Index = 0
myDispose()
RemoveHandler myserialport.DataToSend2, AddressOf DataToAppend
End If
End Sub
Private Sub Me_Activated(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Activated
VisibleBoolean = True
AddHandler myserialport.DataToSend2, AddressOf DataToAppend
End Sub
Private Sub myDispose()
If Index = 0 And Not Me.IsDisposed Then
Me.Invoke(New MethodInvoker(Sub()
MyBase.Dispose(True)
End Sub))
End If
End Sub
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
End Sub
Protected Overrides Sub OnFormClosing(ByVal e As System.Windows.Forms.FormClosingEventArgs)
Index = 1
VisibleBoolean = False
End Sub
I know i don't like either but at least it's working!
Anyother improvement or suggestion is more