I'm developing a Universal Windows Platform app using XAML that runs on a Raspberry Pi under Windows 10 IoT Core
. The app drives a temperature sensor that's on the I2C bus. The sensor class is MLX90614Thermometer
. The sensor uses a DispatcherTimer
to take readings every 100 milliseconds (approx) and updates a moving average. When the value of the moving average changes by more than a specified threshold, the sensor raises a ValueChanged
event and provides the new value in the event args.
In my ViewModel class, TemperatureSensorViewModel
, I subscribe to the sensor's ValueChanged
event and use it to update bound properties named Ambient
, Channel1
and Channel2
. These properties are bound to text blocks in the XAML UI. Here is the event handler:
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;
}
}
...and here is a sample data binding for Ambient
...
<TextBlock x:Name="Ambient" Grid.Row="1" Text="{Binding Path=Ambient}" Style="{StaticResource FieldValueStyle}" />
I'm using the MVVM Light Toolkit, so my properties are implemented like this (only Ambient
shown, but the others are identical except in name):
public double Ambient
{
get { return ambientTemperature; }
private set { Set(nameof(Ambient), ref ambientTemperature, value); }
}
The MVVM Light Toolkit provides the Set()
method, which automatically raises the PropertyChanged
notification for the property being set.
This works correctly if I read a single sample from the sensor in response to a button press. As soon as I enable the automatic sampling mode (which is timer based) though, it starts throwing COMExceptions
. So this must be some kind of threading issue related to the timer.
Now, if I understand correctly, the runtime is supposed to marshal PropertyChanged
notifications onto the UI thread automatically; and that does seem to be the case from looking at the stack trace. However, I eventually get a COMException
. Ugh.
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? I don't understand what's happening here. Can anyone see what the problem might be?
After further research, think I can answer my own question...
It seems that I made an invalid assumption about PropertyChanged events being marshalled to the UI thread automatically. I read that in a few places in articles about WPF, but as @Clemens pointed out in a comment, this is not WPF we are talking about, it is the Universal Windows Platform, which is a derivative of the Windows Runtime (WinRT).
Then I found this question 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, which can be used to marshal any code onto the dispatcher thread.
So, it seems that I have to do my own thread marshalling (I really hate this aspect of Windows programming, I wish Microsoft would make a thread-safe UI technology!).
So I have updated my properties to use this pattern:
public double Ambient
{
get { return ambientTemperature; }
private set
{
ambientTemperature = value;
DispatcherHelper.CheckBeginInvokeOnUI(() => RaisePropertyChanged());
}
}
This now appears to work as expected.
I think a lot of people are going to fall into this quagmire so I am leaving this answer here in the hope that people will find it when they need it.
I don't think the runtime does any sort of marshalling for PropertyChanged
events, which is admittedly a bit irritating.
You probably want to do something like:
public double Ambient
{
get { return ambientTemperature; }
private set
{
Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
Set(nameof(Ambient), ref ambientTemperature, value);
});
}
}
to make this happen on the UI thread.
User contributions licensed under CC BY-SA 3.0