我尝试使用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?
答案 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}";
}
}