【问题标题】:WPF MVVM: Update label depending on Textbox input (2nd Attempt)WPF MVVM:根据文本框输入更新标签(第二次尝试)
【发布时间】:2019-10-09 21:46:38
【问题描述】:

好的,我现在有一个更具体的问题。

我试图弄清楚当两个文本框不再为空时如何更改标签的值(布尔值)。我似乎无法弄清楚如何让它工作,即使它看起来很简单。

有人能指出我正确的方向吗?

请看下面我的代码。

模型(Person.cs)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PracticeUI.Model
{
    public class Person
    {
        private string _firstName;
        private string _lastName;

        public string FullName
        {
            get
            {
                return _firstName + " " + _lastName;
            }
            set { }
        }

        public string FirstName
        {
            get
            {
                return _firstName;
            }
            set
            {
                _firstName = value;
            }
        }

        public string LastName
        {
            get
            {
                return _lastName;
            }
            set
            {
                _lastName = value;
            }
        }
    }
}

ViewModel (PersonViewModel.cs)

using PracticeUI.Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace PracticeUI.ViewModel
{
    public class PersonViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private Person _newPerson = new Person();
        private ICommand _addPerson;
        public Person NewPerson
        {
            get
            {
                return _newPerson;
            }
            set
            {
                _newPerson = value;
                OnPropertyChanged("NewPerson");
            }
        }

        public PersonViewModel()
        {
            _PersonList.Add(new Person() { FirstName = "Tom", LastName = "Barratt" });
            _PersonList.Add(new Person() { FirstName = "Harriet", LastName = "Hammond" });
        }

        private ObservableCollection<Person> _PersonList = new ObservableCollection<Person>();

        public ObservableCollection<Person> PersonList
        {
            get
            {
                return _PersonList;
            }
            set
            {
                _PersonList = value;
                OnPropertyChanged("PersonList");
                OnPropertyChanged("AddPersonCanExecute");
            }
        }

        public void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public ICommand AddPersonCommand
        {
            get
            {
                if (_addPerson == null)
                {
                    _addPerson = new RelayCommand(p => this.AddPersonCanExecute, p => this.AddPerson());
                }
                return _addPerson;
            }
        }
        public bool AddPersonCanExecute
        {
            get
            {
                return _newPerson.FirstName != string.Empty || _newPerson.LastName != string.Empty;
            }
        }

        public void AddPerson()
        {
            _PersonList.Add(new Person() { FirstName = _newPerson.FirstName, LastName = _newPerson.LastName });
            OnPropertyChanged("PersonList");
        }
    }
}

查看 (MainWindow.xaml)

<Window x:Class="PracticeUI.View.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:ViewModel="clr-namespace:PracticeUI.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <ViewModel:PersonViewModel x:Key="ViewModel"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="20"/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="20"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <ListBox ItemsSource="{Binding Source={StaticResource ViewModel}, Path=PersonList}" Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="3" Height="200" Margin="0 0 0 20">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Label Content="{Binding FirstName}"/>
                        <Label Content="{Binding LastName}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Label Content="First Name:" Grid.Row="2" Grid.Column="1"/>
        <TextBox Text="{Binding Source={StaticResource ViewModel}, Path=NewPerson.FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"  Grid.Row="2" HorizontalAlignment="Left" Grid.Column="2" Height="40" Width="200" Margin="10 5"/>
        <Label Content="First Name:" Grid.Row="3" Grid.Column="1"/>
        <TextBox Text="{Binding Source={StaticResource ViewModel}, Path=NewPerson.LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Grid.Row="3" HorizontalAlignment="Left" Grid.Column="2" Height="40" Width="200" Margin="10 5"/>
        <Button Command="{Binding Source={StaticResource ViewModel}, Path=AddPersonCommand}" Content="Add Person" Width="120" Height="30" Grid.Row="4" Grid.Column="2"/>
        <Label Content="{Binding Source={StaticResource ViewModel}, Path=AddPersonCanExecute, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" Grid.Row="2" Grid.Column="4"/>
    </Grid>
</Window>

【问题讨论】:

    标签: c# wpf mvvm


    【解决方案1】:

    首先,将PersonViewModel 重命名为MainViewModel。它不是代表一个人的视图模型,而是整个程序的主要视图模型。它有一个完整的集合Person;一个人怎么样?它不是。很好地命名你的类可以更容易地跟踪什么是什么。我们将把Person 重命名为PersonViewModel,因为它也需要是一个视图模型,而且它确实代表了一个人。

    您希望用户界面在NewPerson.FirstNameNewPerson.LastName 的值发生变化时查看AddPersonCanExecute 的值。

    什么会导致这些值发生变化?

    一种方法是NewPerson 可以更改。所以:

    public Person NewPerson
    {
        get
        {
            return _newPerson;
        }
        set
        {
            _newPerson = value;
            OnPropertyChanged(nameof(AddPersonCanExecute));
            OnPropertyChanged(nameof(NewPerson));
        }
    }
    

    另一种方法是用户可以在绑定到NewPersonFirstNameLastName 属性的文本框中键入新值。那么你和 UI 就不走运了,因为 Person 不是视图模型。当其属性发生变化时,它从不引发任何事件。所以让它成为一个视图模型。

    public class ViewModelBase : INotifyPropertyChanged
    {
        //  Copy your INotifyPropertyChanged implementation here from your main viewmodel
        //  Make your main viewmodel inherit from ViewModelBase
    }
    
    //  Formerly PersonViewModel
    public class MainViewModel : ViewModelBase
    {
        //  We need this to be the actual type because we'll need to be calling 
        //  RaiseCanExecuteChanged() on it. Or whatever equivalent. 
        private RelayCommand _addPerson;
    
        //  All the stuff PersonViewModel had.
        //  Stuff
        //  Stuff
        //  Stuff
    }
    
    //  Remember, your old PersonViewModel is now named MainViewModel. This is the class 
    //  that you used to call Person. 
    public class PersonViewModel : ViewModelBase
    {
        public string FullName
        {
            get
            {
                return _firstName + " " + _lastName;
            }
            //  No empty set, not ever. Somebody will try to set FullName and the compiler 
            //  will let him think it worked. But nothing will change. That's a bug. 
            //set { }
        }
    
        public string FirstName
        {
            get
            {
                return _firstName;
            }
            set
            {
                _firstName = value;
                //  Do the same for LastName. Careful you don't pass nameof(FirstName) 
                // over there. 
                OnPropertyChanged(nameof(FirstName));
                OnPropertyChanged(nameof(FullName));
            }
        }
    

    现在 UI 知道这些属性何时发生变化,但主视图模型仍然不知道。但是现在我们收到了来自Person 的通知,这是可以解决的。我们必须再次重写 NewPerson:

    private PersonViewModel _newPerson = null;
    public PersonViewModel NewPerson
    {
        get { return _newPerson; }
        set
        {
            if (value != _newPerson)
            {
                //  Take the handler off the old NewPerson, if any. 
                if (_newPerson != null)
                {
                    _newPerson.PropertyChanged -= NewPerson_PropertyChanged;
                }
                _newPerson = value;
                if (_newPerson != null)
                {
                    _newPerson.PropertyChanged += NewPerson_PropertyChanged;
                }
                OnPropertyChanged(nameof(NewPerson));
                OnPropertyChanged(nameof(AddPersonCanExecute));
    
                //  I don't know what your RelayCommand class looks like, but it should 
                //  provide some way to force it to raise its CanExecuteChanged event. 
                //  That's what the Button is waiting for to enable or disable itself. 
                _addPerson.RaiseCanExecuteChanged()
            }
        }
    }
    
    private void NewPerson_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case nameof(Person.FirstName):
            case nameof(Person.LastName):
                OnPropertyChanged(nameof(AddPersonCanExecute));
                AddPerson
                break;
        }
    }
    

    另一点:不要让您的视图模型成为资源。它不会破坏你的代码,但它没有任何作用,并为你创造了额外的工作。

    <Window.DataContext>
        <ViewModel:PersonViewModel />
    </Window.DataContext>
    <Window.Resources>
        <!-- remove it from here -->
    </Window.Resources>
    

    现在对于属于 Window 本身的所有控件,您的所有绑定都可以如下所示:

    <TextBox 
        Text="{Binding NewPerson.FirstName, UpdateSourceTrigger=PropertyChanged}"  
        Grid.Row="2" 
        HorizontalAlignment="Left" 
        Grid.Column="2" 
        Height="40" Width="200" Margin="10 5"
        />
    

    去掉 TextBox.Text 上的 Mode=TwoWay;默认情况下,该属性将导致其上的绑定为双向。仅在 TextBox.Text 上保留 UpdateSourceTrigger=PropertyChanged:这将导致文本框在每次击键时更新视图模型,而不是仅在文本框失去焦点时更新视图模型属性的默认行为。您不需要在命令绑定或 Label.Content 绑定上使用UpdateSourceTrigger=PropertyChanged,因为这些属性永远无法更新 viewmodel 属性。默认情况下,它们是 OneWay,而且它们的工作性质也是如此。

    【讨论】:

    • 非常感谢您的批评和评论!这确实帮助了我的 WPF 之旅。我在 Code-Behind 和 XAML 中都在玩数据上下文——这就是为什么我没有将数据上下文设置为 PersonViewModel。目的是在一个窗口中使用两个视图模型。 docs.microsoft.com/en-gb/dotnet/desktop-wpf/data/…
    • @Teebee15 一个窗口(或其他)有一个视图模型。 然而,一个视图模型可以有任意数量的返回其他视图模型的属性(并且它们可以有自己的“子”视图模型属性,无穷无尽)。我的日常工作是维护一个包含 150 多个视图模型的应用程序,我们就是这样做的。该资源事物隐藏该视图模型,使视图模型无法以正常方式进行通信。这是 WPF 新手实践的反模式的一个案例:他们认为应用程序是 视图 的树。它不在 WPF 中。这是一个视图模型树。
    • @Teebee15 除了 XAML 中的直接子视图外,视图不需要相互了解;他们所做的只是显示视图模型的状态。关键是每个视图模型创建和管理自己的子视图模型。当您通过在视图中创建视图模型来相互隐藏视图模型时,您总是不得不求助于荒谬的 Rube Goldberg 变通方法来做任何有用的事情。我感到震惊和震惊的是,MS 的任何人都在这么晚的时候发布了类似的例子。是渎职。好的,布道结束。
    猜你喜欢
    • 1970-01-01
    • 2021-12-20
    • 1970-01-01
    • 1970-01-01
    • 2020-06-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-14
    相关资源
    最近更新 更多