WPF - 为什么 SetProperty() 在 XAML 中设置 {Binding BindableObject.Text} 时不触发 CanExecute?

WPF - Why SetProperty() not trigger CanExecute when set {Binding BindableObject.Text} in XAML?

我使用以下新的 BindableViewModel class(旧的我也在本主题的底部 post)使对象可观察(来自 MS 示例):

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value)) return false;

        storage = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

我想用它来创建一个 ViewModel 以绑定那些 TextBox 控件,例如:

public class TextBoxModel : BindableBase
{
    private string _text = string.Empty;
    // I'm going to bind this field to TextBox.Text property
    public string Text { get { return _text; } set { SetProperty(ref _text, value); } }

    private bool _isEnabled = true;
    // I'm going to bind this field to TextBox.IsEnabled property
    public bool IsEnabled { get { return _isEnabled; } set { SetProperty(ref _isEnabled, value); } }
}

然后,在我的 Page ViewModel 中,我使用 TextBoxModel 来定义字段:

public class Page1ViewModel : BindableBase
{
    private TextBoxModel _firstName = null;
    public TextBoxModel FirstName { get { return _firstName; } set { 
        if (SetProperty(ref _firstName, value))
            {
                SubmitCommand.RaiseCanExecuteChanged();
            } } }

    private TextBoxModel _surname = null;
    public TextBoxModel Surname { get { return _surname; } set { 
        if (SetProperty(ref _surname, value))
            {
                SubmitCommand.RaiseCanExecuteChanged();
            } } }

    private DelegateCommand _submitCommand = null;
    public DelegateCommand SubmitCommand { get { return _submitCommand??(_submitCommand=new DelegateCommand(SubmitExecute, SubmitCanExecute)); } }
    private void SubmitExecute()
    {
        MessageBox.Show($"{FirstName.Text} {Surname.Text}");
    }
    private bool SubmitCanExecute()
    {
        if(string.IsNullOrEmpty(FirstName.Text))
            return false;
        else if(string.IsNullOrEmpty(Surname.Text))
            return false;
        else
            return true;
    }
}

在我的 XAML 视图中,我像往常一样设置文本框绑定:

<TextBox Text="{Binding FirstName.Text, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding FirstName.IsEnabled}"/>
<TextBox Text="{Binding Surname.Text, UpdateSourceTrigger=PropertyChanged}" IsEnabled="{Binding Surname.IsEnabled}"/>
<Button Content="Submit" Command{Binding SubmitCommand} />

当我 运行 执行此操作时,我发现无法更改文本。它没有触发 setter 中的 if(SetProperty(ref _firstName, value)) 或 if(SetProperty(ref _surname, value))。如果我不组合 TextBoxModel 中的属性,则一切正常。如果我使用 OLD ViewModelBase,TextBoxModel 也能正常工作。

所以我想我一定是在使用新的 BindableBase 时遗漏了一些东西 class?寻求您的帮助,谢谢!

旧视图模型库:

[Obsolete("Please use BindableBase。")]
public abstract class ObservableModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public static string GetPropertyName<T>(System.Linq.Expressions.Expression<Func<T>> e)
    {
        var member = (System.Linq.Expressions.MemberExpression)e.Body;
        return member.Member.Name;
    }

    protected virtual void RaisePropertyChanged<T>
        (System.Linq.Expressions.Expression<Func<T>> propertyExpression)
    {
        RaisePropertyChanged(GetPropertyName(propertyExpression));
    }

    protected void RaisePropertyChanged(String propertyName)
    {
        System.ComponentModel.PropertyChangedEventHandler temp = PropertyChanged;
        if (temp != null)
        {
            temp(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
        }
    }
}

您的绑定永远不会设置 FirstNameLastName,而且您所显示的其他内容也不会设置。它们都是 null。绑定到 Text 只会设置该值,它不会创建新的 TextBoxModel 并设置它。

如果您希望它在以这种方式组织时工作,您需要执行如下操作:

public class Page1ViewModel : BindableBase
{
    private readonly TextBoxModel _firstName = new TextBoxModel();

    public Page1ViewModel()
    {
        _firstName.PropertyChanged += 
            (sender, args) => SubmitCommand.RaiseCanExecuteChanged();
    }

    public TextBoxModel FirstName 
    {
        get { return _firstName; }
    }
}

您尝试做的事情是有道理的,但是您是如何做的是不必要的复杂。你有一个 TextBoxModel 试图模仿你根本不需要做的 真实 TextBox,我认为你的方法是错误的。

首先,模型表示 数据,而不是 UI。与其创建代表 TextBoxTextBoxModel,不如创建代表数据结构的模型,例如 PersonModelUserModel。这是我的意思的一个例子:

public class PersonModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

注意:您可以将 INotifyPropertyChanged 内容放入模型中,但这并不是真正的 UI 问题,它只是数据。 属性 更改实现的最佳位置是在视图模型中,我将在下面进一步解释。

好的,现在数据层已经排序了,你只需要通过View Model暴露这个模型给View。首先,这里是一个简单的 属性 更改基础,我将在这个例子中使用:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyOfPropertyChange([CallerMemberName]string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

像这样公开模型属性:

public class PersonViewModel : PropertyChangedBase
{
    private PersonModel _Person;

    public string FirstName
    {
        get { return _Person.FirstName; }
        set
        {
            _Person.FirstName = value;
            NotifyOfPropertyChange();
        }
    }

    public string LastName
    {
        get { return _Person.LastName; }
        set
        {
            _Person.LastName = value;
            NotifyOfPropertyChange();
        }
    }

    //TODO: Your command goes here

    public PersonViewModel()
    {
        //TODO: Get your model from somewhere.
        _Person = new PersonModel();
    }
}

现在您可以简单地将 TextBox 绑定到 FirstNameLastName 视图模型 属性:

<TextBox Text="{Binding FirstName}" ... />

就这么简单。您 不需要 在您的数据层中重新创建一个 TextBox,事实上所有 UI 问题都应该与模型层完全分开。

使用此方法,您也无需担心引发 CanExecuteChanged 方法,因为它已由 INotifyPropertyChanged.

处理

记住,UI是UI,数据是数据