Ошибка чтения SerialPort из-за отсутствия владельца потока
-
22-07-2019 - |
Вопрос
У меня есть простое приложение Windows WPF, которое пытается прочитать последовательный порт с помощью System.IO.Ports.SerialPort
. Р>
Когда я пытаюсь прочитать входящие данные в событии DataReceived
, я получаю исключение о том, что у меня нет доступа к потоку. Как мне это решить?
У меня есть это в классе окна 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
Когда срабатывает событие DataReceived
, я получаю следующее исключение для 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)
Решение
ДОБРО ПОЖАЛОВАТЬ В ВОЛШЕБНЫЙ МИР МУЛЬТИТРЕЙДА !!!
Что происходит, так это то, что все ваши элементы пользовательского интерфейса (экземпляры классов) могут быть доступны / обновлены только потоком пользовательского интерфейса. Я не буду вдаваться в подробности об этой близости темы, но это важная тема, и вы должны это проверить.
Событие, когда данные поступают через последовательный порт, происходит в потоке , отличном от потока пользовательского интерфейса . В потоке пользовательского интерфейса есть насос сообщений, который обрабатывает сообщения Windows (например, щелчки мыши и т. Д.). Ваш последовательный порт не отправляет сообщения Windows. Когда данные поступают на последовательный порт, для обработки этого сообщения используется совершенно другой поток из потока пользовательского интерфейса.
Итак, в вашем приложении метод mSerialPort_DataReceived выполняется в потоке, отличном от потока вашего пользовательского интерфейса. Вы можете использовать окно отладки Threads, чтобы проверить это.
Когда вы пытаетесь обновить свой пользовательский интерфейс, вы пытаетесь изменить элемент управления с привязкой потока для потока пользовательского интерфейса из другого потока, который выдает исключение, которое вы видели.
TL; DR: вы пытаетесь изменить элемент пользовательского интерфейса вне потока пользовательского интерфейса. Используйте
txtSerialOutput.Dispatcher.Invoke
чтобы запустить обновление в потоке пользовательского интерфейса. Здесь приведен пример того, как это сделать, в контенте этого сообщества. стр.
Диспетчер вызовет ваш метод в потоке пользовательского интерфейса (он отправляет оконное сообщение для пользовательского интерфейса, говорящее «Привет, гайзер, запустите этот метод kthx»), и ваш метод сможет затем безопасно обновить пользовательский интерфейс из потока пользовательского интерфейса. р>
Другие советы
Я добавил это:
Private mBuffer As String = ""
Delegate Sub DelegateSetUiText()
Private Sub UpdateUiFromBuffer()
txtSerialOutput.Text = mBuffer
End Sub
... и я изменил событие DataReceived
на следующее:
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
Пользовательский интерфейс может обновляться только основным потоком приложения. Асинхронный обратный вызов для события последовательного порта обрабатывается за кулисами в отдельном потоке. Как уже упоминалось, вы можете использовать Dispatcher.Invoke, чтобы поставить в очередь изменение свойства компонента пользовательского интерфейса в потоке пользовательского интерфейса.
Тем не менее, поскольку вы используете WPF, есть более элегантный & amp; идиоматическое решение с использованием привязок. Предполагая, что данные, которые вы получаете через последовательный порт, имеют какое-то существенное значение для ваших бизнес-объектов, вы можете заставить событие DataReceived обновить свойство в объекте и затем связать пользовательский интерфейс с этим свойством.
В грубом коде:
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
Затем в своем XAML-файле вы можете привязать текстовое поле к этому свойству объекта:
<TextBox Text="{Binding Path=SerialData}"/>
Предполагается, что для DataContext задан экземпляр вашего класса MySerialData. Самое замечательное в этом дополнительном бите - то, что WPF теперь будет автоматически обрабатывать весь процесс межпотокового маршалинга, поэтому вам не нужно беспокоиться о том, какой поток вызывает изменения пользовательского интерфейса, механизм связывания в WPF просто делает это Работа. Очевидно, что если это простой проект, он может не стоить лишнего кода. Однако, если вы выполняете много асинхронного взаимодействия и обновляете пользовательский интерфейс, эта функция WPF действительно спасает жизнь и устраняет большой класс ошибок, общих для многопоточных приложений. Мы используем WPF в многопоточных приложениях, выполняющих много TCP-коммуникаций, и привязки в WPF были великолепны, особенно когда данные, передаваемые по проводам, предназначены для обновления нескольких мест в пользовательском интерфейсе, так как вам не нужно разбрасывать проверку потоков ваш код.