La lecture de SerialPort cause une erreur car elle ne possède pas de thread
-
22-07-2019 - |
Question
J'ai une simple application Windows WPF qui tente de lire un port série avec le System.IO.Ports.SerialPort
.
Lorsque j'essaie de lire les données entrantes dans l'événement DataReceived
, j'obtiens une exception indiquant que je n'ai pas accès au fil. Comment le résoudre?
J'ai ceci dans la classe de fenêtre 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
Lorsque l'événement DataReceived
se déclenche, l'exception suivante apparaît sur 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)
La solution
BIENVENUE DANS LE MONDE MAGIQUE DU MULTI THREADING !!!
Ce qui se passe, c'est que tous les éléments de votre interface utilisateur (instances de classe) ne peuvent être accessibles / mis à jour que par le thread d'interface utilisateur. Je n’entrerai pas dans les détails de cette affinité avec les fils, mais c’est un sujet important et vous devriez le vérifier.
L'événement où des données arrivent sur le port série se produit sur un thread différent de celui de l'interface utilisateur . Le thread d'interface utilisateur a une pompe de messages qui gère les messages Windows (comme les clics de souris, etc.). Votre port série n'envoie pas de messages Windows. Lorsque des données arrivent sur le port série, un thread complètement différent de celui de votre interface utilisateur est utilisé pour traiter ce message.
Ainsi, dans votre application, la méthode mSerialPort_DataReceived s'exécute dans un thread différent de celui de votre interface utilisateur. Vous pouvez utiliser la fenêtre de débogage des threads pour vérifier cela.
Lorsque vous essayez de mettre à jour votre interface utilisateur, vous essayez de modifier un contrôle avec une affinité de thread pour le thread d'interface utilisateur à partir d'un thread différent, qui lève l'exception que vous avez vue.
TL; DR: Vous essayez de modifier un élément de l'interface utilisateur en dehors du fil de l'interface utilisateur. Utilisez
txtSerialOutput.Dispatcher.Invoke
pour exécuter votre mise à jour sur le fil de l'interface utilisateur. Il y a un exemple ici pour savoir comment faire cela dans le contenu de la communauté de ceci page.
Dispatcher invoquera votre méthode sur le thread d'interface utilisateur (il enverra un message Windows à l'interface utilisateur en indiquant "Hai guize, exécutez cette méthode kthx"), et votre méthode pourra alors mettre à jour en toute sécurité l'interface utilisateur à partir du thread d'interface utilisateur.
Autres conseils
D'après la réponse fournie par Fera , j'ai maintenant résolu mon problème. Je pensais que le problème était d'accéder à mSerialPort.ReadExisting ()
, alors que le problème était d'accéder à l'élément graphique txtSerialOutput
à partir de l'événement DataReceived
, qui s'exécute dans un thread séparé.
J'ai ajouté ceci:
Private mBuffer As String = ""
Delegate Sub DelegateSetUiText()
Private Sub UpdateUiFromBuffer()
txtSerialOutput.Text = mBuffer
End Sub
... et j'ai remplacé l'événement DataReceived
par ceci:
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’UI ne peut être mise à jour que par le fil principal de l’application. Le rappel asynchrone pour l'événement de port série est traité en arrière-plan sur un thread distinct. Comme Will l'a mentionné, vous pouvez utiliser Dispatcher.Invoke pour mettre en file d'attente un changement de propriété de composant d'interface utilisateur sur le thread d'interface utilisateur.
Cependant, puisque vous utilisez WPF, il existe une solution plus élégante & amp; solution idiomatique utilisant des liaisons. En supposant que les données que vous recevez sur le port série ont une valeur significative pour vos objets métier, vous pouvez faire en sorte que l'événement DataReceived mette à jour une propriété dans un objet, puis lie l'interface utilisateur à cette propriété.
En code brut:
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
Ensuite, dans votre fichier XAML, vous pouvez associer la zone de texte à la propriété d'objet suivante:
<TextBox Text="{Binding Path=SerialData}"/>
Cela suppose que le DataContext est défini sur une instance de votre classe MySerialData. Ce qui est génial avec ce travail de plomberie supplémentaire, c’est que WPF gérera automatiquement tout le marshaling inter-thread pour vous, de sorte que vous n’aurez plus à vous soucier du thread qui appelle les modifications de l’UI. Le moteur de liaison de WPF le fait simplement. travail. De toute évidence, s'il ne s'agit que d'un projet à la volée, cela ne vaut peut-être pas le code supplémentaire. Cependant, si vous faites beaucoup de communication asynchrone et mettez à jour l'interface utilisateur, cette fonctionnalité de WPF vous permettra d'économiser de la vie et d'éliminer une grande classe de bogues communs aux applications multithreads. Nous utilisons WPF dans une application fortement threadée effectuant beaucoup de communication TCP et les liaisons dans WPF étaient excellentes, en particulier lorsque les données qui arrivent sur le réseau sont destinées à mettre à jour plusieurs emplacements de l'interface utilisateur, car il n'est pas nécessaire que la vérification du threading soit omniprésente. votre code.