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));
【问题讨论】:
我不明白这是怎么工作的。当文本更改时(或者,实际上,根本没有!),您没有设置FirstName
或 Surname
,因此不会调用 SetProperty
。您需要观察 within TextBoxModel
. 的属性变化
@CharlesMager 你是什么意思?我确实更改了 FirstName 对象的 Text 属性,但它没有触发 if(SetProperty(ref _firstName, value))。但是,它触发了 FirstName 对象内的 Text 属性。为什么 if(SetProperty(ref _firstName, value)) 甚至里面的 Text 属性都没有触发?
您的绑定是到Text
属性,即string
。尝试设置该值不会神奇地新建TextBoxModel
并设置FirstName
。它只会默默地失败,因为FirstName
是null
。
【参考方案1】:
您的绑定永远不会设置FirstName
或LastName
,您所显示的其他任何内容也不会设置。这些都是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。与其创建代表TextBox
的TextBoxModel
,不如创建一个代表您的数据结构的模型,例如PersonModel
或UserModel
。这是我的意思的一个例子:
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
绑定到 FirstName
或 LastName
视图模型属性:
<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?的主要内容,如果未能解决你的问题,请参考以下文章
在 XAML 中设置 WPF OxyPlot PlotViews 的样式
使用 MvvmCross 时如何在 WPF App.xaml 中设置资源? (MaterialDesignInXamlToolkit)