简体   繁体   中英

WPF MVVM - Correct way of OneWay data binding

I try to grasp the concept of MVVM using WPF, but I am still struggling. To show my problem, I made a very simple example where data binding does not work:

I have a UI with one Button and one TextBlock object. The TextBlock shows 0 at the beginning. Whenever the button is clicked, the value should increase by one.

This is my UI (MainWindow.xaml):

<Window x:Class="CounterTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:counter="clr-namespace:CounterTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <counter:CounterViewModel x:Key="counterviewobj"/>
    </Window.Resources>
    <Grid>
        <TextBlock HorizontalAlignment="Left" Margin="303,110,0,0" Text="{Binding CounterString, Mode=OneWay, Source={StaticResource counterviewobj}}" TextWrapping="Wrap" VerticalAlignment="Top"/>
        <Button Command="{Binding IncCommand, Mode=OneWay, Source={StaticResource counterviewobj}}" Content="+1" HorizontalAlignment="Left" Margin="303,151,0,0" VerticalAlignment="Top" Width="37"/>
    </Grid>
</Window>

I kept the code behind as clean as possible (MainWindow.xaml.cs):

using System.Windows;

namespace CounterTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

A Counter class is defined to do the business logic (Counter.cs).

namespace CounterTest
{
    public class Counter
    {
        public int Cval { get; set; } // This is the value to increment.

        public Counter() { Cval = 0; }

        public void Increment() { Cval++; }
        public bool CanIncrement() { return true; } // Only needed to conform to ICommand interface.
    }
}

I use a lightweight ICommand implementation (RelayCommand.cs):

using System;
using System.Windows.Input;

namespace CounterTest
{
    public class RelayCommand : ICommand
    {
        private Action WhattoExecute;
        private Func<bool> WhentoExecute;
        public RelayCommand(Action What, Func<bool> When)
        {
            WhattoExecute = What;
            WhentoExecute = When;
        }
        public bool CanExecute(object parameter)
        {
            return WhentoExecute();
        }
        public void Execute(object parameter)
        {
            WhattoExecute();
        }

        public event EventHandler CanExecuteChanged;
    }
}

And finally the ViewModel (CounterViewModel.cs):

namespace CounterTest
{
    public class CounterViewModel
    {
        private Counter myCounter = new Counter();
        private RelayCommand _IncCommand;
        public RelayCommand IncCommand
        {
            get { return _IncCommand; }
        }

        public string CounterString
        {
            get { return myCounter.Cval.ToString(); }
        }

        public CounterViewModel()
        {
            _IncCommand = new RelayCommand(myCounter.Increment, myCounter.CanIncrement);
        }
    }
}

Stepping through my code, I see that the command is executed when I press the button, and Cval is increased by one each time. But obviously I don't do the data binding correctly.

What did I try? I see two possible circumventions. The first is to bind the TextBlock directly to the Cval value of the Counter class. But this would violate all principles of MVVM.

The other one is to implement INotifyPropertyChanged, bind the UpdateSourceTrigger to a PropertyChanged event from CounterString, and implement an update event for Cval which the setter from CounterString can subscribe to. But that is extremely cumbersome, and it also makes the OneWay binding in the XAML file moot.

There must be a better way. What do I miss? How can I update the TextBlock when Cval is changed and stick to the MVVM principles?

Your first idea sounds great!

Seriously, not every target of a binding has to be a first-level member on the view model. Its totally OK to go into an object (and you have to for collections!). If you go this route, you just need Counter to implement INPC. So you would just make your counter a public property then:

Text="{Binding CounterProp.CVal}"

If you still don't want to do that, then I would recommend making your command also raise property changed for the calculated property:

   _IncCommand = new RelayCommand(IncrementCounter, myCounter.CanIncrement
   ...

   private void IncrementCounter(object parameter)
   {
        myCounter.Increment(parameter);
        OnPropertyChanged(nameof(CounterString)
   }

But really... the first one is easier to deal with, understand, and maintain. I don't see where it violates any principles of MVVM.

No command knows which binding should update is target right after it fires, thus it does nothing but invokes your method. To make WPF bindings work you should push some data change notifications to them. To change your code I'd highly recommend implemeting the INotifyPropertyChanged interface on your ViewModel . Since your command is updating some property of your Model , the simpliest way for ViewModel to know the fact the Model is updated is to subscribe to the Model notification. Thus you'll have to implement that interface twice. For simplicity I'll implement INotifyPropertyChanged in a some base class and inherit it in your Model & ViewModel : INotifyPropertyChanged:

using System.ComponentModel;

namespace CounterTest {
    public class ModelBase : INotifyPropertyChanged {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) => PropertyChanged?.Invoke(this, e);
        protected void RaisePropertyChanged(string propertyName) => OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }
}

Model:

namespace CounterTest {
    public class Counter : ModelBase {
        private int _cval;

        public int Cval { // This is the value to increment.
            get { return _cval; }
            set {
                if (_cval != value) {
                    _cval = value;
                    RaisePropertyChanged(nameof(Cval));
                }
            }
        }

        public void Increment() { Cval++; }
        public bool CanIncrement() { return true; } // Only needed to conform to ICommand interface. // Mikant: not really needed. not here
    }
}

ViewModel:

using System.ComponentModel;

namespace CounterTest {
    public class CounterViewModel : ModelBase {
        private readonly Counter _myCounter = new Counter();

        public RelayCommand IncCommand { get; }

        public CounterViewModel() {
            IncCommand = new RelayCommand(_myCounter.Increment, () => true);

            _myCounter.PropertyChanged += OnModelPropertyChanged;
        }

        private void OnModelPropertyChanged(object sender, PropertyChangedEventArgs e) {
            switch (e.PropertyName) {
                case nameof(Counter.Cval):
                    RaisePropertyChanged(nameof(CounterString));
                    break;
            }
        }

        public string CounterString => $"Count is now {_myCounter.Cval}";
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM