Почему вызов событий PropertyChanged из таймера вызывает COMException?

Я разрабатываю приложение универсальной платформы Windows с использованием XAML, которое работает на Raspberry Pi под Windows 10 IoT Core. Приложение управляет датчиком температуры, подключенным к шине I2C. Класс датчика MLX90614Thermometer. Датчик использует DispatcherTimer для снятия показаний каждые 100 миллисекунд (приблизительно) и обновляет скользящее среднее значение. Когда значение скользящего среднего изменяется более чем на заданный порог, датчик вызывает событие ValueChanged и предоставляет новое значение в аргументах события.

В моем классе ViewModel TemperatureSensorViewModel я подписываюсь на событие датчика ValueChanged и использую его для обновления связанных свойств с именами Ambient, Channel1 и Channel2. Эти свойства привязаны к текстовым блокам в пользовательском интерфейсе XAML. Вот обработчик события:

    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, поэтому мои свойства реализованы следующим образом (показаны только Ambient, остальные идентичны, кроме названия):

    public double Ambient
    {
        get { return ambientTemperature; }
        private set { Set(nameof(Ambient), ref ambientTemperature, value); }
    }

MVVM Light Toolkit предоставляет метод Set(), который автоматически создает уведомление PropertyChanged для устанавливаемого свойства.

Это работает правильно, если я считываю один образец с датчика в ответ на нажатие кнопки. Однако, как только я включаю режим автоматической выборки (основанный на таймере), он начинает выдавать COMExceptions. Так что это должно быть какая-то проблема с потоками, связанная с таймером.

Теперь, если я правильно понимаю, среда выполнения должна автоматически маршалировать уведомления PropertyChanged в поток пользовательского интерфейса; и это действительно так, если посмотреть на трассировку стека. Однако в итоге я получаю 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? Я не понимаю, что здесь происходит. Кто-нибудь может увидеть, в чем проблема?


person Tim Long    schedule 23.12.2015    source источник
comment
Обратите внимание, что UWP/Windows Runtime и WPF — это две разные платформы. Ваш вопрос не о WPF   -  person Clemens    schedule 23.12.2015
comment
Предложения @Tim: я думаю, вы ищете winrt-xaml, но я думаю, что ваша экспозиция будет лучше, если вы используете два тега: uwp + xaml   -  person chue x    schedule 24.12.2015


Ответы (2)


После дальнейших исследований, думаю, я смогу ответить на свой вопрос...

Кажется, я сделал неверное предположение о том, что события PropertyChanged автоматически маршалируются в поток пользовательского интерфейса. Я читал это в нескольких местах в статьях о WPF, но, как указал @Clemens в комментарии, мы говорим не о WPF, а об универсальной платформе Windows, которая является производной от среды выполнения Windows (WinRT).

  • Основные знания: XAML в UWP ≠ WPF. При выборе универсальных приложений для Windows нельзя полагаться на документацию WPF.

Затем я нашел этот вопрос, который имеет сходство для меня, в частности, что плакат сделал мою ошибку, предположив, что он имеет дело с WPF. Принятый ответ привел меня к этому другому вопросу относительно класса DispatcherHelper MVVM Light Toolkit, который может быть используется для маршалинга любого кода в поток диспетчера.

Таким образом, кажется, что я должен выполнить свою собственную маршализацию потоков (я действительно ненавижу этот аспект программирования для Windows, я бы хотел, чтобы Microsoft создала потокобезопасную технологию пользовательского интерфейса!).

Итак, я обновил свои свойства, чтобы использовать этот шаблон:

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

Теперь это работает, как и ожидалось.

Я думаю, что многие люди попадут в эту трясину, поэтому я оставляю этот ответ здесь в надежде, что люди найдут его, когда он им понадобится.

person Tim Long    schedule 24.12.2015
comment
Ключевое предложение: XAML в UWP ≠ WPF - person Bart; 24.12.2015

Я не думаю, что среда выполнения выполняет какую-либо сортировку событий PropertyChanged, что, по общему признанию, немного раздражает.

Вы, вероятно, хотите сделать что-то вроде:

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

чтобы это произошло в потоке пользовательского интерфейса.

person gregstoll    schedule 24.12.2015