WPF MVVM-OneWay数据绑定的正确方法

时间:2018-08-01 21:54:34

标签: c# wpf mvvm

我尝试使用WPF掌握MVVM的概念,但我仍在努力。为了说明我的问题,我举了一个非常简单的示例,其中数据绑定无效:

我有一个带有一个Button和一个TextBlock对象的UI。 TextBlock的开头显示为0。每当单击按钮时,该值应增加一。

这是我的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>

我保持代码尽可能的干净(MainWindow.xaml.cs):

using System.Windows;

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

定义了Counter类来执行业务逻辑(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.
    }
}

我使用轻量级的ICommand实现(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;
    }
}

最后是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);
        }
    }
}

单步执行代码,我看到按下按钮时该命令已执行,并且Cval每次增加一。但是显然我没有正确进行数据绑定。

我尝试了什么?我看到两种可能的规避措施。第一种是将TextBlock直接绑定到Counter类的Cval值。但这会违反MVVM的所有原理。

另一种方法是实现INotifyPropertyChanged,将UpdateSourceTrigger绑定到CounterString中的PropertyChanged事件,并实现Cval的更新事件,CounterString中的安装程序可以预订该事件。但这非常麻烦,这也使XAML文件中的OneWay绑定变得毫无意义。

必须有更好的方法。我想念什么?更改Cval并遵循MVVM原理时,如何更新TextBlock?

2 个答案:

答案 0 :(得分:2)

您的第一个想法听起来很棒!

严重的是,并非绑定的每个目标都必须是视图模型的第一级成员。完全可以进入一个对象(而且您必须进行收集!)。如果您走这条路线,则只需要Counter即可实现INPC。因此,您只需将柜台设为公共财产即可:

Text="{Binding CounterProp.CVal}"

如果您仍然不想这样做,那么我建议您将命令也提高对计算出的属性更改的属性:

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

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

但是实际上……第一个更易于处理,理解和维护。我看不出它在哪里违反了MVVM的任何原则。

答案 1 :(得分:1)

没有命令知道,该绑定应在触发后立即成为目标,因此除了调用您的方法外,它什么都不做。为了使WPF绑定起作用,您应该向它们推送一些数据更改通知。要更改您的代码,我强烈建议在您的 ViewModel 上实现INotifyPropertyChanged接口。由于您的命令正在更新您的 Model 的某些属性,因此 ViewModel 知道 Model 已更新的事实的最简单方法是订阅型号通知。因此,您将必须实现两次该接口。 为简单起见,我将在一些基类中实现INotifyPropertyChanged并在您的 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));
    }
}

型号:

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}";
    }
}