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)
¿Fue útil?

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top