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));
}
}
}
您的绑定永远不会设置 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; }
}
}
您尝试做的事情是有道理的,但是您是如何做的是不必要的复杂。你有一个 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,数据是数据。
我使用以下新的 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));
}
}
}
您的绑定永远不会设置 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; }
}
}
您尝试做的事情是有道理的,但是您是如何做的是不必要的复杂。你有一个 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,数据是数据。