0

I'm trying to learn the MVVM structure. How can I update a variable that changes constantly in another class in the UI.

I created a simple example because the project codes are too much. But I failed.

I would be very grateful if you could tell me where I went wrong. Thanks.

MyModel

public class Temperature : INotifyPropertyChanged
{

    private double _memsTemperature;
    private double _cpuTemperature;
    private double _animalTemperature;

    public double MemsTemperature
    {
        get { return _memsTemperature; }
        set
        {
            _memsTemperature = value;
            OnPropertyChanged("MemsTemperature");
        }
    }

    public double CpuTemperature
    {
        get { return _cpuTemperature; }
        set
        {
            _cpuTemperature = value;
            OnPropertyChanged("CpuTemperature");
        }
    }

    public double AnimalTemperature
    {
        get { return _animalTemperature; }
        set
        {
            _animalTemperature = value;
            OnPropertyChanged("AnimalTemperature");
        }
    }

    System.Windows.Threading.DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();

    public Temperature()
    {
        dispatcherTimer.Tick += DispatcherTimer_Tick;
        dispatcherTimer.Interval = TimeSpan.FromSeconds(1);
        dispatcherTimer.Start();
    }

    private void DispatcherTimer_Tick(object sender, System.EventArgs e)
    {
        MemsTemperature = MemsTemperature + 1;

        CpuTemperature = CpuTemperature + 2;

        AnimalTemperature = AnimalTemperature + 3;
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion   
}

MainWindowViewModel

public class MainWindowViewModel
{
    public double MemTemp { get; set; }

    public MainWindowViewModel()
    {
        MemTemp = new Temperature().MemsTemperature;
    }
}

Main Window Xaml and C# Code

     <TextBlock Text="{Binding MemTemp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
voltac
  • 33
  • 7
  • You shouldn't start the `DispatcherTimer` in your constructor (_constructors should be simple_ and not do any "heavy lifting"). Instead give your viewmodel class a `Start` and `Stop` method. – Dai Aug 10 '21 at 19:32
  • Is there an example of how to bind the start stop method? – voltac Aug 11 '21 at 05:10
  • Yes, using `Microsoft.Xaml.Behaviors.Wpf` to bind events in XAML to `ICommand` properties in your view-models ( https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.Wpf/ ), previously this functionality was part of `System.Windows.Interactivity`. See https://stackoverflow.com/questions/8360209/how-to-add-system-windows-interactivity-to-project – Dai Aug 11 '21 at 06:14
  • I explained that `Microsoft.Xaml.Behaviors.Wpf` **replaces** `System.Windows.Interactivity.dll` - please **thoroughly read** the links I posted. – Dai Aug 12 '21 at 08:13
  • Thank you. I have completed the installation. – voltac Aug 12 '21 at 08:16

3 Answers3

2

The MainWindowViewModel should expose a Temperature property, e.g. like this:

public class MainWindowViewModel
{ 
    public Temperature Temperature { get; } = new Temperature();
}

and the Binding should then look like this:

<TextBlock Text="{Binding Temperature.MemsTemperature}"/>

Neither Mode=TwoWay nor UpdateSourceTrigger=PropertyChanged makes sense on the Binding of a TextBlock's Text property.


The OnPropertyChanged method would simpler and safer be implemented like this:

private void OnPropertyChanged(string propertyName)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
0

You have a XAML page with UI controls that bind to those constantly-changing properties. When you send out the PropertyChanged notifications, the UI control will automatically update itself.

The problem with the code you wrote is that you never bound to the actual temperature. XAML doesn't know how to translate MemTemp into anything other than it's name unless you write a DataTemplate for it.

For example, (assuming a grid) something like this:

<TextBlock Grid.Row="0" Grid.Column="0" Text="Animal: "/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding MemTemp.AnimalTemperature}"/>
Joe
  • 5,394
  • 3
  • 23
  • 54
  • Your answer is correct. I accept his answer as Clemens provides a more understandable and concise explanation. Thanks for your help. – voltac Aug 11 '21 at 05:06
0

I would define an explicit worker class which performs the measurements. This class has an event (OnMeasurement), which can be subscribed in the ViewModel:

// Arguments for the mesurement event (temperature, ...)
public class MeasurementEventArgs : EventArgs
{
    public double Temperature { get; }

    public MeasurementEventArgs(double temperature)
    {
        Temperature = temperature;
    }
}

public class MeasurementWorker
{
    private readonly CancellationTokenSource _tcs = new CancellationTokenSource();

    // Provides an event we can subscribe in the view model.
    public event Action<object, MeasurementEventArgs> OnMeasurement;

    public void Stop()
    {
        _tcs.Cancel();
    }

    // Measurement routine. Perform a measurement every second.
    public async Task Start()
    {
        try
        {
            var rnd = new Random();
            while (!_tcs.IsCancellationRequested)
            {
                var temperature = 20 * rnd.NextDouble();
                OnMeasurement?.Invoke(this, new MeasurementEventArgs(temperature));
                await Task.Delay(1000, _tcs.Token);
            }
        }
        catch (TaskCanceledException) { }
        // TODO: Create an error event to catch exceptions from here.
        catch { }
    }
}

In your MainWindow class you instantiate your viewmodel and your worker:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainWindowViewModel(new MeasurementWorker());
    }

    // Register in XAML with  <Window ... Closing="StopMeasurement">
    public async void StopMeasurement(object sender, System.ComponentModel.CancelEventArgs e)
    {
        var vm = DataContext as MainWindowViewModel;
        await vm.StopMeasurement();
    }
}

In your view model you can subscribe to the worker event and raise OnPropertyChanged in your callback function:

public class MainWindowViewModel : INotifyPropertyChanged
{
    private double _memsTemperature;
    private readonly MeasurementWorker _mw;
    private readonly Task _measurementWorkerTask;

    public double MemsTemperature
    {
        get => _memsTemperature;
        set
        {
            _memsTemperature = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MemsTemperature)));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void ProcessMeasurement(object sender, MeasurementEventArgs args)
    {
        MemsTemperature = args.Temperature;
    }

    // You can call this if you want to stop your measurement. Should be called if you close your app.
    public async Task StopMeasurement()
    {
        _mw.OnMeasurement -= ProcessMeasurement;
        _mw.Stop();
        // Clean shutdown
        await _measurementWorkerTask;
    }

    public MainWindowViewModel(MeasurementWorker mw)
    {
        _mw = mw;
        _mw.OnMeasurement += ProcessMeasurement;
        _measurementWorkerTask = _mw.Start();
    }
}
Michael
  • 1,166
  • 5
  • 4
  • Thank you for your answer. Since I'm just starting out, the structure you shared above seemed very complex. What are the advantages of the above answer over the two answers below? – voltac Aug 11 '21 at 05:04
  • The worker has no dependencies with the user interface. It can be used everywhere, you can also use it to make a console application that outputs the values. The GUI dependent parts are all in the Viewmodel. In software development this is called decoupling. Furthermore, it no longer uses the old threading functions, instead it is using await and async. – Michael Aug 11 '21 at 05:16
  • I understand what you mean. It looks pretty useful. Seems complicated now that I'm just getting started. I've taken my notes and progressed, I'll review what you shared again. Thank you. – voltac Aug 12 '21 at 08:22