【问题标题】:WPF - Why SetProperty() not trigger CanExecute when set {Binding BindableObject.Text} in XAML?WPF - 为什么在 XAML 中设置 {Binding BindableObject.Text} 时 SetProperty() 不触发 CanExecute?
【发布时间】:2016-09-12 00:45:27
【问题描述】:

我使用以下新的 BindableViewModel 类(旧的我也在本主题的底部发布)使对象可观察(来自 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 类时我一定错过了什么?寻求您的帮助,谢谢!

旧视图模型库:

[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));
        }
    }
}

【问题讨论】:

  • 我不明白这是怎么工作的。当文本更改时(或者,实际上,根本没有!),您没有设置 FirstNameSurname,因此不会调用 SetProperty。您需要观察 within TextBoxModel. 的属性变化
  • @CharlesMager 你是什么意思?我确实更改了 FirstName 对象的 Text 属性,但它没有触发 if(SetProperty(ref _firstName, value))。但是,它触发了 FirstName 对象内的 Text 属性。为什么 if(SetProperty(ref _firstName, value)) 甚至里面的 Text 属性都没有触发?
  • 您的绑定是到Text 属性,即string。尝试设置该值不会神奇地新建TextBoxModel 并设置FirstName。它只会默默地失败,因为FirstNamenull

标签: c# wpf xaml mvvm command


【解决方案1】:

您的绑定永远不会设置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; }
    }
}

【讨论】:

  • 嗨@CharlesMager,您的代码有效,非常感谢。在我接受你的回答之前,我想了解一些事情:“_firstName.PropertyChanged += (sender, args) => SubmitCommand.RaiseCanExecuteChanged();”这意味着当 _firstName 对象更改时,而不是调用 SubmitCommand.RaiseCanExecuteChanged(),我明白了。但是,我确实在 setter 中添加了相同的代码,也意味着当 _firstName 对象更改时,而不是调用 SubmitCommand.RaiseCanExecuteChanged()。为什么我的不工作? (我将代码更改为 _firstName = new TextBoxModel(); 以避免它在开始时为空)
  • @OhMyDog 这意味着当_firstName property 发生更改时,它将调用该方法。放入setter意味着它只会在对象被替换时被调用。它永远不会被替换(您可以看到完全移除 setter 不会导致任何问题)。
【解决方案2】:

您尝试做的事情是有道理的,但是如何您这样做是不必要的复杂。你有一个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,Data 就是 Data

【讨论】:

  • 嗨@MikeEason 谢谢你的回答,但它并没有解决我的问题(我认为)。我有一个表单包含大约 80 个需要在 UI 上部署的字段,每个字段都需要 IsEnabled 和 IsFocused 属性,我需要在 ViewModel 中控制它们,使用 Value 属性本身,在我的 ViewModel 中至少有 80x3 = 240 个属性。因此,我决定通过相同的 Control 组合这些属性,以便于编写代码。
猜你喜欢
  • 2023-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-31
  • 1970-01-01
  • 2023-03-24
  • 2012-02-16
  • 2011-08-26
相关资源
最近更新 更多