Error de causa de lectura de SerialPort debido a que no posee hilo
-
22-07-2019 - |
Pregunta
Tengo una aplicación simple de Windows WPF que intenta leer un puerto serie con el System.IO.Ports.SerialPort
.
Cuando intento leer los datos entrantes en el evento DataReceived
, recibo una excepción que dice que no tengo acceso al hilo. ¿Cómo lo resuelvo?
Tengo esto en la clase de ventana 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
Cuando se activa el evento DataReceived
, obtengo la siguiente excepción en 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)
Solución
¡BIENVENIDO AL MUNDO MÁGICO DE LA LECTURA MÚLTIPLE!
Lo que sucede es que todos los elementos de la interfaz de usuario (instancias de clase) solo pueden ser accedidos / actualizados por el hilo de la interfaz de usuario. No entraré en detalles sobre esta afinidad de hilos, pero es un tema importante y deberías echarle un vistazo.
El evento en el que ingresan datos en el puerto serie está ocurriendo en un subproceso diferente al de la interfaz de usuario . El hilo de la interfaz de usuario tiene una bomba de mensajes que maneja los mensajes de Windows (como clics del mouse, etc.). Su puerto serie no envía mensajes de Windows. Cuando los datos ingresan en el puerto serie, se utiliza un subproceso completamente diferente de su subproceso de interfaz de usuario para procesar ese mensaje.
Entonces, dentro de su aplicación, el método mSerialPort_DataReceived se está ejecutando en un hilo diferente al de su hilo UI. Puede usar la ventana de depuración de subprocesos para verificar esto.
Cuando intentas actualizar tu IU, estás intentando modificar un control con afinidad de hilo para el hilo de UI desde un hilo diferente, lo que arroja la excepción que has visto.
TL; DR: está intentando modificar un elemento de la IU fuera del hilo de la IU. Usar
txtSerialOutput.Dispatcher.Invoke
para ejecutar su actualización en el hilo de la interfaz de usuario. Aquí hay un ejemplo sobre cómo hacer esto en el contenido de la comunidad de este página.
El despachador invocará su método en el subproceso de la interfaz de usuario (envía un mensaje de Windows a la interfaz de usuario que dice "Hai guize, ejecute este método kthx"), y su método puede actualizar de forma segura la interfaz de usuario desde el subproceso de la interfaz de usuario.
Otros consejos
Basado en la respuesta de Will , ahora he resuelto mi problema. Pensé que el problema era acceder a mSerialPort.ReadExisting ()
, mientras que el problema realmente era acceder al elemento GUI txtSerialOutput
desde el evento DataReceived
, que se ejecuta en un hilo separado.
Agregué esto:
Private mBuffer As String = ""
Delegate Sub DelegateSetUiText()
Private Sub UpdateUiFromBuffer()
txtSerialOutput.Text = mBuffer
End Sub
... y cambié el evento DataReceived
a esto:
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
La interfaz de usuario solo puede ser actualizada por el hilo principal de la aplicación. La devolución de llamada asíncrona para el evento del puerto serie se maneja detrás de escena en un hilo separado. Como Will mencionó, puede usar Dispatcher.Invoke para poner en cola un cambio de propiedad del componente de la interfaz de usuario en el hilo de la interfaz de usuario.
Sin embargo, como estás usando WPF, hay un & amp; más elegante Solución idiomática mediante enlaces. Suponiendo que los datos que recibe en el puerto serie tienen algún valor significativo para sus objetos comerciales, puede hacer que el evento DataReceived actualice una propiedad en un objeto y luego vincule la IU a esa propiedad.
En código aproximado:
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
Luego, en su archivo XAML, puede vincular el cuadro de texto a esta propiedad de objeto:
<TextBox Text="{Binding Path=SerialData}"/>
Esto supone que DataContext está establecido en una instancia de su clase MySerialData. Lo mejor de hacer este poco más de fontanería es que WPF ahora se encargará automáticamente de todo el marshaling de hilos cruzados para que no tenga que preocuparse por qué hilo está invocando cambios en la interfaz de usuario, el motor de unión en WPF simplemente lo hace trabajo. Obviamente, si esto es solo un proyecto descartable, puede que no valga la pena el código adicional. Sin embargo, si está haciendo mucha comunicación asincrónica y actualizando la interfaz de usuario, esta característica de WPF es un protector de la vida real y elimina una gran clase de errores comunes a las aplicaciones de subprocesos múltiples. Usamos WPF en una aplicación muy enhebrada que hace mucha comunicación TCP y los enlaces en WPF fueron geniales, especialmente cuando los datos que llegan a través del cable están destinados a actualizar varios lugares en la interfaz de usuario, ya que no es necesario tener una verificación de subprocesos esparcida por todas partes su código.