简体   繁体   English

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

[英]WPF MVVM - Correct way of OneWay data binding

I try to grasp the concept of MVVM using WPF, but I am still struggling. 我尝试使用WPF掌握MVVM的概念,但我仍在努力。 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. 我有一个带有一个Button和一个TextBlock对象的UI。 The TextBlock shows 0 at the beginning. TextBlock的开头显示为0。 Whenever the button is clicked, the value should increase by one. 每当单击按钮时,该值应增加一。

This is my UI (MainWindow.xaml): 这是我的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): 我将代码保持在尽可能干净的位置(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). 定义了一个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.
    }
}

I use a lightweight ICommand implementation (RelayCommand.cs): 我使用轻量级的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;
    }
}

And finally the ViewModel (CounterViewModel.cs): 最后是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. 逐步执行代码,我看到按下按钮时该命令已执行,并且Cval每次都增加一。 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. 第一种是将TextBlock直接绑定到Counter类的Cval值。 But this would violate all principles of MVVM. 但这会违反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. 另一个是实现INotifyPropertyChanged,将UpdateSourceTrigger绑定到CounterString的PropertyChanged事件,并实现Cval的更新事件,CounterString的设置者可以预订该事件。 But that is extremely cumbersome, and it also makes the OneWay binding in the XAML file moot. 但这非常麻烦,这也使XAML文件中的OneWay绑定变得毫无意义。

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? 更改Cval并遵循MVVM原理时,如何更新TextBlock?

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. 如果走这条路线,您只需要Counter即可实现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. 我看不出它违反了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. 为了使WPF绑定起作用,您应该向它们推送一些数据更改通知。 To change your code I'd highly recommend implemeting the INotifyPropertyChanged interface on your ViewModel . 要更改您的代码,我强烈建议在ViewModel上实现INotifyPropertyChanged接口。 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. 由于您的命令正在更新Model的某些属性,因此ViewModel知道Model已更新的事实的最简单方法是订阅Model通知。 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: 为简单起见,我将在一些基类中实现INotifyPropertyChanged并在您的ModelViewModel中继承它: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}";
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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