La lettura di SerialPort causa errori a causa del mancato possesso del thread
-
22-07-2019 - |
Domanda
Ho una semplice applicazione Windows WPF che prova a leggere una porta seriale con System.IO.Ports.SerialPort
.
Quando provo a leggere i dati in arrivo nell'evento DataReceived
, ricevo un'eccezione che dice che non ho accesso al thread. Come lo risolvo?
Ho questo nella classe della finestra di WPF:
Public WithEvents mSerialPort As New SerialPort()
Private Sub btnConnect_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnConnect.Click
With mSerialPort
If .IsOpen Then
.Close()
End If
.BaudRate = 4800
.PortName = SerialPort.GetPortNames()(0)
.Parity = Parity.None
.DataBits = 8
.StopBits = StopBits.One
.NewLine = vbCrLf
.Open()
End With
End Sub
Private Sub mSerialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles mSerialPort.DataReceived
If e.EventType = SerialData.Chars Then
txtSerialOutput.Text += mSerialPort.ReadExisting()
End If
End Sub
Protected Overrides Sub Finalize()
If mSerialPort.IsOpen Then
mSerialPort.Close()
End If
mSerialPort.Dispose()
mSerialPort = Nothing
MyBase.Finalize()
End Sub
Quando si attiva l'evento DataReceived
, ottengo la seguente eccezione su mSerialPort.ReadExisting ()
:
System.InvalidOperationException was unhandled
Message="The calling thread cannot access this object because a different thread owns it."
Source="WindowsBase"
StackTrace:
at System.Windows.Threading.Dispatcher.VerifyAccess() at System.Windows.Threading.DispatcherObject.VerifyAccess() at System.Windows.DependencyObject.GetValue(DependencyProperty dp) at System.Windows.Controls.TextBox.get_Text() at Serial.Serial.mSerialPort_DataReceived(Object sender, SerialDataReceivedEventArgs e) in D:\SubVersion\VisionLite\Serial\Serial.xaml.vb:line 24 at System.IO.Ports.SerialPort.CatchReceivedEvents(Object src, SerialDataReceivedEventArgs e) at System.IO.Ports.SerialStream.EventLoopRunner.CallReceiveEvents(Object state) at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack) at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)
Soluzione
BENVENUTO NEL MONDO MAGICO DEL MULTITREADING !!!
Quello che sta accadendo è che tutti i tuoi elementi dell'interfaccia utente (istanze di classe) sono accessibili / aggiornati solo dal thread dell'interfaccia utente. Non entrerò nei dettagli su questa affinità di thread, ma è un argomento importante e dovresti verificarlo.
L'evento in cui i dati arrivano sulla porta seriale si sta verificando su un thread diverso dal thread dell'interfaccia utente . Il thread dell'interfaccia utente ha un pump dei messaggi che gestisce i messaggi di Windows (come clic del mouse, ecc.). La tua porta seriale non invia messaggi Windows. Quando i dati arrivano sulla porta seriale viene utilizzato un thread completamente diverso dal thread dell'interfaccia utente per elaborare quel messaggio.
Quindi, all'interno dell'applicazione, il metodo mSerialPort_DataReceived viene eseguito in un thread diverso rispetto al thread dell'interfaccia utente. Puoi utilizzare la finestra di debug dei thread per verificarlo.
Quando si tenta di aggiornare l'interfaccia utente, si sta tentando di modificare un controllo con affinità di thread per il thread dell'interfaccia utente da un thread diverso, il che genera l'eccezione che si è vista.
TL; DR: stai tentando di modificare un elemento dell'interfaccia utente al di fuori del thread dell'interfaccia utente. Usa
txtSerialOutput.Dispatcher.Invoke
per eseguire l'aggiornamento sul thread dell'interfaccia utente. C'è un esempio qui su come farlo nel contenuto della community di questo pagina.
Il Dispatcher invocherà il tuo metodo sul thread dell'interfaccia utente (invia un messaggio di windows all'interfaccia utente dicendo " Hai guize, esegui questo metodo kthx ") e il tuo metodo può quindi aggiornare in modo sicuro l'interfaccia utente dal thread dell'interfaccia utente.
Altri suggerimenti
Basato sulla risposta di Will , ora ho risolto il mio problema. Pensavo che il problema fosse accedere a mSerialPort.ReadExisting ()
, mentre il problema stava davvero accedendo all'elemento GUI txtSerialOutput
dall'evento DataReceived
, che viene eseguito in un thread separato.
Ho aggiunto questo:
Private mBuffer As String = ""
Delegate Sub DelegateSetUiText()
Private Sub UpdateUiFromBuffer()
txtSerialOutput.Text = mBuffer
End Sub
... e ho modificato l'evento DataReceived
in questo:
Private Sub mSerialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles mSerialPort.DataReceived
If e.EventType = SerialData.Chars Then
mBuffer += mSerialPort.ReadExisting()
txtSerialOutput.Dispatcher.Invoke(New DelegateSetUiText(AddressOf UpdateUiFromBuffer))
End If
End Sub
L'interfaccia utente può essere aggiornata solo dal thread dell'applicazione principale. Il callback asincrono per l'evento della porta seriale viene gestito dietro le quinte su un thread separato. Come accennato da Will, puoi usare Dispatcher.Invoke per mettere in coda una modifica della proprietà del componente UI sul thread UI.
Tuttavia, poiché stai usando WPF, c'è un & amp; soluzione idiomatica usando attacchi. Supponendo che i dati che ricevi sulla porta seriale abbiano un valore significativo per i tuoi oggetti business, puoi fare in modo che l'evento DataReceived aggiorni una proprietà in un oggetto e quindi associ l'interfaccia utente a quella proprietà.
In codice approssimativo:
Public Class MySerialData
Implements System.ComponentModel.INotifyPropertyChanged
Public Event PropertyChanged(sender as Object, e as System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotfifyPropertyChanged.PropertyChanged
private _serialData as String
Public Property SerialData() As String
Get
Return _serialData
End Get
Set(value as String)
If value <> _serialData Then
_serialData = value
RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("SerialData"))
End If
End Property
Quindi, nel tuo file XAML, puoi associare la casella di testo a questa proprietà dell'oggetto:
<TextBox Text="{Binding Path=SerialData}"/>
Ciò presuppone che DataContext sia impostato su un'istanza della classe MySerialData. La cosa grandiosa di fare questo ulteriore extra di impianto idraulico è che WPF ora gestirà automaticamente tutto il marshalling cross-thread per te, quindi non devi preoccuparti di quale thread sta invocando le modifiche dell'interfaccia utente, il motore di associazione in WPF lo fa solo lavoro. Ovviamente, se questo è solo un progetto da buttare via, potrebbe non valere la pena aggiungere un po 'di codice iniziale. Tuttavia, se stai facendo molta comunicazione asincrona e stai aggiornando l'interfaccia utente, questa funzione di WPF ti salva la vita ed elimina una grande classe di bug comuni alle applicazioni multi-thread. Usiamo WPF in un'applicazione pesantemente filettata che fa molta comunicazione TCP e i collegamenti in WPF erano fantastici, specialmente quando i dati che passano sul cavo sono pensati per aggiornare diversi punti dell'interfaccia utente poiché non è necessario che il controllo del threading sia sparso ovunque il tuo codice.