我正在使用XAML开发一个通用Windows平台应用程序,该应用程序在Windows 10 IoT Core
下的Raspberry Pi上运行。该应用程序驱动I2C总线上的温度传感器。传感器类别为MLX90614Thermometer
。传感器使用DispatcherTimer
每隔100毫秒(大约)读取一次读数,并更新移动平均值。当移动平均值的变化超过指定阈值时,传感器会引发ValueChanged
事件,并在事件args中提供新值。
在我的ViewModel类TemperatureSensorViewModel
中,我订阅传感器的ValueChanged
事件,并使用它更新名为Ambient
、Channel1
和Channel2
的绑定属性。这些属性绑定到XAML UI中的文本块。这是事件处理程序:
void HandleSensorValueChanged(object sender, SensorValueChangedEventArgs e)
{
switch (e.Channel)
{
case 0:
Ambient = e.Value;
break;
case 1:
Channel1 = e.Value;
break;
case 2:
Channel2 = e.Value;
break;
}
}
这里是CCD_ 10的示例数据绑定。。。
<TextBlock x:Name="Ambient" Grid.Row="1" Text="{Binding Path=Ambient}" Style="{StaticResource FieldValueStyle}" />
我使用的是MVVM Light Toolkit,所以我的属性是这样实现的(只显示了Ambient
,但除了名称之外,其他都是相同的):
public double Ambient
{
get { return ambientTemperature; }
private set { Set(nameof(Ambient), ref ambientTemperature, value); }
}
MVVM Light Toolkit提供Set()
方法,该方法会自动引发正在设置的属性的PropertyChanged
通知。
如果我响应按钮按下从传感器读取单个样本,这将正常工作。不过,只要我启用自动采样模式(基于定时器),它就会开始抛出COMExceptions
。因此,这一定是与计时器有关的某种线程问题。
现在,如果我理解正确的话,运行时应该自动将PropertyChanged
通知封送到UI线程上;从堆栈跟踪来看,情况似乎确实如此。然而,我最终得到了COMException
。啊。
System.Runtime.InteropServices.COMException(0x8001010E):应用程序调用了一个为其他线程整理的接口。(HRESULT出现异常:0x8001010E(RPC_E_WRONG_THREAD))位于System.Runtime.InteropServices.WindowsRuntime.PropertyChangedEventArgsMarshaler.ConvertToNative(PropertyChangedEventArgs managedArgs)位于System.ComponentModel.PropertyChangedEventHandler.Invoke(对象发送方,PropertyChangedEventArgs e)在GalaSoft.MvvvmLight.ObsObservableObject.RisePropertyChanged(字符串属性名称)在GalaSoft.MvvvmLight.ViewModelBase.RisePropertyChanged[T](字符串属性名称,T旧值,T新值,布尔广播)在GalaSoft.MvvvmLight.ViewModelBase.Set[T](字符串属性名称,T&field,T newValue,布尔广播)在TA.UWP.Devices.Samples.ViewModel.TemperatureSensorViewModel.set_Channel1(双值)在TA.UWP.Devices.Samples.ViewModel.TemperatureSensorViewModel.HandSensorValueChanged(Object sender,SensorValueChangedEventArgs e)在TA.UWP.Devices.MLX90614Thermometer.RiseValueChanged(UInt32通道,双值)在TA.UWP.Devices.MLX90614温度计.SampleAllChannels()位于TA.UWP.Devices.MLX90614温度计.b_37_0()位于System.Threading.Tasks.Task.InerInvoke()位于System.Threading.Tasks.Task.Execute()---从引发异常的前一位置开始的堆栈结尾跟踪---在System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务任务)位于System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务任务)位于System.Runtime.CompilerServices.TaskAwaiter.GetResult()在TA.UWP.Devices.MLX90614温度计.d_37.MoveNext()---从引发异常的前一位置开始的堆栈结尾跟踪---在System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(任务任务)位于System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(任务任务)位于System.Runtime.CompilerServices.TaskAwaiter.GetResult()在TA.UWP.Devices.MLX90614温度计.d_38.MoveNext()
瓦特?我不明白这里发生了什么。有人能看到可能是什么问题吗?
经过进一步的研究,我想我可以回答我自己的问题。。。
关于PropertyChanged事件被自动编组到UI线程,我似乎做了一个无效的假设。我在一些关于WPF的文章中读到了这一点,但正如@Clemens在评论中指出的那样,这不是我们谈论的WPF,而是通用Windows平台,它是Windows运行时(WinRT)的衍生物。
- 关键学习:UWP中的XAML≠WPF 在考虑通用Windows应用程序时,不能依赖WPF文档
然后我发现这个问题和我的有相似之处,特别是海报犯了我的错误,认为他在处理WPF。接受的答案让我想到了另一个关于MVVM Light Toolkit的DispatcherHelper
类的问题,该类可用于将任何代码封送到调度器线程上。
所以,我似乎必须自己进行线程编组(我真的很讨厌Windows编程的这一方面,我希望微软能开发出线程安全的UI技术!)。
因此,我更新了我的属性以使用以下模式:
public double Ambient
{
get { return ambientTemperature; }
private set
{
ambientTemperature = value;
DispatcherHelper.CheckBeginInvokeOnUI(() => RaisePropertyChanged());
}
}
现在,这似乎如预期的那样起作用。
我认为很多人都会陷入这种困境,所以我把这个答案留在这里,希望人们能在需要的时候找到它。
我不认为运行时对PropertyChanged
事件进行任何编组,这确实有点令人恼火。
你可能想做一些类似的事情:
public double Ambient
{
get { return ambientTemperature; }
private set
{
Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
Set(nameof(Ambient), ref ambientTemperature, value);
});
}
}
以便在UI线程上实现这一点。