为什么从计时器引发 PropertyChanged 事件会导致 COMException?

Why Does Raising PropertyChanged events from a timer cause an COMException?

我正在使用 XAML 开发通用 Windows 平台应用程序,该应用程序在 Windows 10 IoT Core 下的 Raspberry Pi 上运行。该应用程序驱动 I2C 总线上的温度传感器。传感器 class 是 MLX90614Thermometer。传感器使用 DispatcherTimer 每 100 毫秒(大约)读取一次读数并更新移动平均值。当移动平均值的值变化超过指定阈值时,传感器会引发 ValueChanged 事件并在事件参数中提供新值。

在我的 ViewModel class、TemperatureSensorViewModel 中,我订阅了传感器的 ValueChanged 事件并使用它来更新名为 AmbientChannel1 的绑定属性和 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;
        }
    }

...这里是 Ambient...

的示例数据绑定
    <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): The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))
   at System.Runtime.InteropServices.WindowsRuntime.PropertyChangedEventArgsMarshaler.ConvertToNative(PropertyChangedEventArgs managedArgs)
   at System.ComponentModel.PropertyChangedEventHandler.Invoke(Object sender, PropertyChangedEventArgs e)
   at GalaSoft.MvvmLight.ObservableObject.RaisePropertyChanged(String propertyName)
   at GalaSoft.MvvmLight.ViewModelBase.RaisePropertyChanged[T](String propertyName, T oldValue, T newValue, Boolean broadcast)
   at GalaSoft.MvvmLight.ViewModelBase.Set[T](String propertyName, T& field, T newValue, Boolean broadcast)
   at TA.UWP.Devices.Samples.ViewModel.TemperatureSensorViewModel.set_Channel1(Double value)
   at TA.UWP.Devices.Samples.ViewModel.TemperatureSensorViewModel.HandleSensorValueChanged(Object sender, SensorValueChangedEventArgs e)
   at TA.UWP.Devices.MLX90614Thermometer.RaiseValueChanged(UInt32 channel, Double value)
   at TA.UWP.Devices.MLX90614Thermometer.SampleAllChannels()
   at TA.UWP.Devices.MLX90614Thermometer.b__37_0()
   at System.Threading.Tasks.Task.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at TA.UWP.Devices.MLX90614Thermometer.d__37.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at TA.UWP.Devices.MLX90614Thermometer.d__38.MoveNext()

瓦特?我不明白这里发生了什么。谁能看出问题出在哪里?

我认为运行时不会对 PropertyChanged 事件进行任何形式的编组,这确实有点令人恼火。

您可能想要执行以下操作:

public double Ambient
{
    get { return ambientTemperature; }
    private set
    { 
        Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                Set(nameof(Ambient), ref ambientTemperature, value);
            });            
    }
}

在 UI 线程上实现此目的。

经过进一步研究,认为我可以回答我自己的问题...

我似乎对 PropertyChanged 事件自动编组到 UI 线程做了一个无效的假设。我在有关 WPF 的文章中的一些地方读到了这一点,但正如@Clemens 在评论中指出的那样,这不是我们正在谈论的 WPF,它是通用 Windows 平台,它是 Windows 运行时 (WinRT)。

  • 重点学习:XAML in UWP ≠ WPF考虑通用 Windows 应用程序时不能依赖 WPF 文档.

然后我发现了 which has similarities to mine, specifically that the poster has made my mistake of assuming that he's dealing with WPF. The accepted answer led me to this other question regarding the MVVM Light Toolkit's DispatcherHelper class,它可用于将任何代码编组到调度程序线程。

所以,看来我必须自己做线程编组(我真的很讨厌Windows编程的这方面,我希望微软能做出线程安全的UI技术!)。

所以我更新了属性以使用此模式:

    public double Ambient
    {
        get { return ambientTemperature; }
        private set
        {
            ambientTemperature = value;
            DispatcherHelper.CheckBeginInvokeOnUI(() => RaisePropertyChanged());
        }
    }

这似乎按预期工作。

我想很多人都会陷入这个泥潭,所以我把这个答案留在这里,希望人们在需要的时候能找到它。