在我的第一个 wpf(.NET core)代码中,我有类:
public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
我开始在我的 MainViewModel 中缩短这段代码(工作正常):
public class SerialCommViewModel : ViewModelBase
{
//#region private field
/* field ICommand and other bool field for other function */
SerialPort serial;
/* region: ICommand properties and public Properties, boring coding as always */
//constructor: serial = new SerialPort;
private void StartListening()
{
//set up com port baudrate, stopbits and open...
serial.Open();
serial.DataReceived += new SerialDataReceivedEventHandler(DataReceivedEvent);
//OnPropertyChanged("WindowTitle");
}
private void DataReceivedEvent(object sender, SerialDataReceivedEventArgs e)
{
//check serial port open and get data
if (sender is SerialPort port)
{
byte[] data = new byte[port.BytesToRead];
port.Read(data, 0, data.Length);
string s = Encoding.GetEncoding("Windows-1252").GetString(data);
port.Write("ok");
//OutputText is binded into Textbox
OutputText += s + SelectedLineEnding;
OnPropertyChanged("OutputText");
}
}
效果很好。整个项目的代码占用了太多行,所以我使用 MVVM 工具包社区来重写它。
新项目重写目前有一个按钮绑定命令
TextChangeCommand
和一个TextBox绑定DataReceived
(由MVVM工具包生成)
MainViewModel代码:
public partial class MainWindowViewModel : ObservableObject
{
SerialPort serial;
/*some field for other function*/
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(TextChangeCommand))]
private string _dataReceived;
public MainWindowViewModel()
{
serial = new SerialPort("COM2",115200,Parity.None,8,StopBits.One);
}
[RelayCommand]
public void TextChange()
{
serial.Open();
serial.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
}
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
if (sender is SerialPort port)
{
while (port.BytesToRead > 0)
{
byte[] data = new byte[port.BytesToRead];
port.Read(data, 0, data.Length);
port.Write("ok"); //it work still fine here
DataReceived += data.ToString(); //it break the exception here,
}
我调试了,并在这一行出现异常: System.InvalidOperationException:“调用线程无法访问此对象,因为另一个线程拥有它。”
我也尝试启动一个调用委托,但它似乎不起作用。任何帮助解释和建议表示赞赏。
效果很好。
不,事实并非如此。来自 SerialPort.DataReceived
的文档从 SerialPort 对象接收到数据时,在辅助线程上引发 DataReceived 事件。由于此事件是在辅助线程而不是主线程上引发的,因此尝试修改主线程中的某些元素(例如 UI 元素)可能会引发线程异常。如果需要修改主窗体或控件中的元素,请使用 Invoke 发回更改请求,这将在正确的线程上完成工作。
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
错误正是由于这个问题造成的。在 WPF 中,这实际上意味着您无法从任何后台线程引发 PropertyChanged
。请注意,此错误并不能保证,但无论是否出现错误,从非 UI 线程更新 UI 都是错误的。
在许多情况下,您可以通过将执行移至 UI 线程来解决此问题。有很多方法可以做到这一点,但典型的方法是使用全局调度程序:
Application.Current.Dispatcher.BeginInvoke(()=> DataReceived += data.ToString());
。假设该属性从它的 setter 中引发 PropertyChanged
。
另请注意,每当使用多线程时,都需要确保代码是线程安全的。您可以使用调试器线程窗口来检查在断点处停止时是否正在主线程上运行。
另请注意,如果需要,某些 UI 框架会自动将执行转移到 UI 线程。我建议不要依赖这种行为,因为如果您在没有这样的框架的情况下进行编码,它很容易导致问题。