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

Posted

技术标签:

【中文标题】WPF - 为啥在 XAML 中设置 Binding BindableObject.Text 时 SetProperty() 不触发 CanExecute?【英文标题】: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" CommandBinding 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 【参考方案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 组合这些属性,以便于编写代码。

以上是关于WPF - 为啥在 XAML 中设置 Binding BindableObject.Text 时 SetProperty() 不触发 CanExecute?的主要内容,如果未能解决你的问题,请参考以下文章

在 WPF/xaml 中设置静态日期时间

在 WPF/xaml 中设置静态日期时间

在 XAML 中设置 WPF OxyPlot PlotViews 的样式

使用 MvvmCross 时如何在 WPF App.xaml 中设置资源? (MaterialDesignInXamlToolkit)

在组合框中设置 SelectedItem

WPF:一旦我在代码中设置了一个属性,它就会永远忽略 XAML 绑定......我该如何防止这种情况发生?