1

I am using a label on multiple forms which display the weather data which is called from a WCF service. I wish to have this update every minute to display the updated weather data without interfering with user interaction.

I get the following error:

"Must create DependencySource on same Thread as the DependencyObject. "

I have a View Model for getting the weather data asynchronously which inherits from ViewModelBase to handle property changed events. The properties from the ViewModel are bound to the label

ViewModel for weather

public class WeatherDataVM : ViewModelBase
{
    private string _windString;
    private SolidColorBrush _windState;
    private DispatcherTimer _timer;


    public WeatherDataVM()
    {
        _timer = new DispatcherTimer(DispatcherPriority.Render);
        _timer.Interval = TimeSpan.FromSeconds(10);
        _timer.Tick += async (sender, args) => {await Task.Run(() => GetWindAsync()); };
        //_timer.Tick += _timer_Tick;
        _timer.Start();
        GetWind();
    }

    private void GetWind()
    {
        var weatherFromService = Services.Instance.EmptyStackService.GetWeather();
        var windSpeed = Convert.ToDouble(weatherFromService.Windspeed);
        var maxGust = Convert.ToDouble(weatherFromService.Max_Gust_In_Last_Min);

        var windSpeedMPH = Math.Round(windSpeed * 1.15078, 1);
        var maxGustMPH = Math.Round(maxGust * 1.15078, 1);

        var windString = $"W/S: {windSpeedMPH}({maxGustMPH})";

        var windState = new Color();
        if (windSpeed >= 40)
            windState = Color.FromRgb(255, 64, 64);
        else if (windSpeed >= 24)
            windState = Color.FromRgb(255, 212, 128);
        else
            windState = Color.FromRgb(0, 255, 0);
        _windState = new SolidColorBrush(windState);

        _windString = windString;


    }

    private async Task GetWindAsync()
    {
        var weatherFromService = Services.Instance.EmptyStackService.GetWeather();
        var windSpeed = Convert.ToDouble(weatherFromService.Windspeed);
        var maxGust = Convert.ToDouble(weatherFromService.Max_Gust_In_Last_Min);

        var windSpeedMPH = Math.Round(windSpeed * 1.15078, 1);
        var maxGustMPH = Math.Round(maxGust * 1.15078, 1);

        var windString = $"W/S: {windSpeedMPH}({maxGustMPH})";

        var windState = new Color();
        if (windSpeed >= 40)
            windState = Color.FromRgb(255, 64, 64);
        else if (windSpeed >= 24)
            windState = Color.FromRgb(255, 212, 128);
        else
            windState = Color.FromRgb(0, 255, 0);

        WindState = new SolidColorBrush(windState);
        WindString = windString;

    }


    public string WindString
    {
        get { return _windString; }

        set
        {
            if (_windString == value)
                return;
            _windString = value;
            OnPropertyChanged("WindString");
        }
    }

    public SolidColorBrush WindState
    {
        get { return _windState; }

        set
        {
            if (_windState == value)
                return;
            _windState = value;
            OnPropertyChanged("WindState");
        }

    }
}

ViewModelBase

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Xaml on view for label

<Label x:Name="lblWeather" Content="{Binding WindString}" Foreground="black" Background="{Binding WindState}" Style="{DynamicResource SmallLabel}"  />

code behind view in constructor

lblWeather.DataContext = new WeatherDataVM();

the weather label should change each time the timer ticks. Instead it throws an error.

KJSR
  • 1,679
  • 6
  • 28
  • 51
  • Looks like you're attempting to update UI on a non UI thread, which will error because UI has thread affinity. You should await a Task lt t gt where t contains data you need for the UI. Then update the ui, back on the ui thread. – Andy Aug 08 '19 at 15:35
  • You should remove `SolidColorBrush` from your `ViewModel` and use numbers or string instead, then apply a colour through a style or a converter. Then you can retrieve information however you want. This is why having UI elements in ViewModel is a bad idea, it always bites back! – XAMlMAX Aug 08 '19 at 16:04
  • 1
    SolidColorBrush is not a UI element. It is a Freezable and as such explicitly designed to be created in a thread other than the UI thread. No problem at all with using it in a view model. – Clemens Aug 08 '19 at 16:28

1 Answers1

1

You can create a brush on a background thread if you freeze it:

var brush = new SolidColorBrush(windState);
brush.Freeze();
WindState = brush;

But it doesn't make much sense to use a DispatcherTimer if you just call Task.Run in the Tick event handler.

Provided that your event handler only creates brushes and don't manipulate any UI elements directly (it certainly shouldn't since it's implemented in a view model), you could use a System.Timer.Timer. Its Elapsed event is queued for execution on a thread pool thread where you can query the service without blocking the UI.

mm8
  • 163,881
  • 10
  • 57
  • 88