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

时间:2015-12-23 20:15:42

标签: xaml uwp inotifypropertychanged windows-10-iot-core mvvm-toolkit

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

在我的ViewModel类TemperatureSensorViewModel中,我订阅了传感器的ValueChanged事件,并使用它来更新名为AmbientChannel1Channel2的绑定属性。这些属性绑定到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()

WAT?我不明白这里发生了什么。任何人都可以看到问题所在吗?

2 个答案:

答案 0 :(得分:5)

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

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

  • 密钥学习:UWP中的XAML≠WPF 在考虑通用Windows应用 时,您无法依赖WPF文档。

然后我发现this question与我有相似之处,特别是海报错误地假设他正在处理WPF。接受的答案引导我进入另一个question regarding the MVVM Light Toolkit's DispatcherHelper class,它可用于将任何代码编组到调度程序线程上。

所以,似乎我必须进行自己的线程编组(我真的很讨厌Windows编程的这个方面,我希望微软能够创建一个线程安全的UI技术!)。

所以我更新了我的属性以使用这种模式:

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

现在似乎按预期工作了。

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

答案 1 :(得分:2)

我不认为运行时会对PropertyChanged事件进行任何类型的编组操作,这无疑会让人感到恼火。

你可能想做类似的事情:

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

在UI线程上实现这一点。