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)
È stato utile?

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top