DependencyProperty 未在 PropertyChanged 上更新
DependencyProperty not updating on PropertyChanged
在 ViewModel 中,我有一个 属性,它本身实现了 INotifyPropertyChanged。绑定到 DependencyProperty 时,如果我只在绑定到 DependencyProperty 的当前实例上设置属性,即使我引发 属性 更改,我也不会进入 DependencyProperty 更改回调。 (详情见下方代码)
为什么这个构造会失败?
实现我想要的方法的一种方法是每次更改其属性时将 CurrentFoobar 设置为新实例。
是否存在更漂亮的方法来实现所需的行为?
编辑: 我知道这个特定的例子可能看起来无关紧要,因为一个人可能只是简单地绑定到 TextBlocks 文本 属性。但是,我有兴趣为控件做一些事情 像这样 ,与 TextBlock 不同,控件不公开适当的依赖属性。
代码
我有一个带有 属性 Foobar 类型“CurrentFoobar”的 ViewModel。
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
var foobar = new Foobar() {Foo = "Hello", Bar = 42};
CurrentFoobar = foobar;
updateCommand = new UpdateCommand(this);
}
private UpdateCommand updateCommand;
public ICommand UpdateCommand
{
get => updateCommand;
}
private Foobar _curfoobar;
public Foobar CurrentFoobar
{
get => _curfoobar;
set
{
_curfoobar = value;
_curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Foobar 是一个 class 具有两个属性 string Foo 和 int Bar。
Foobar 实现了 INotifyPropertyChanged。
public class Foobar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _foo;
public string Foo
{
get => _foo;
set
{
_foo = value;
OnPropertyChanged();
}
}
private int _bar;
public int Bar
{
get => _bar;
set
{
_bar = value;
OnPropertyChanged();
}
}
}
在 CurrentFoobar setter 中,我订阅了当前 Foobar 实例上的 PropertyChanged,并引发了 OnPropertyChanged。
private Foobar _curfoobar;
public Foobar CurrentFoobar
{
get => _curfoobar;
set
{
_curfoobar = value;
_curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
OnPropertyChanged();
}
}
此外,我有一个带有 CustomControl 的视图,它公开了一个 DependencyProperty“FoobarProperty”。
我将此 DependencyProperty 绑定到我的 ViewModels CurrentFoobar。
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public static readonly DependencyProperty FoobarProperty = DependencyProperty.Register(
"Foobar", typeof(Foobar), typeof(UserControl1), new PropertyMetadata(default(Foobar), FoobarPropertyChangedCallback));
private static void FoobarPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uc1 = d as UserControl1;
var newfb = e.NewValue as Foobar;
uc1.TextBlock1.Text = newfb.Foo;
uc1.TextBlock2.Text = newfb.Bar.ToString();
}
public Foobar Foobar
{
get { return (Foobar) GetValue(FoobarProperty); }
set { SetValue(FoobarProperty, value); }
}
}
如果我将 CurrentFoobar 设置为 Foobar 的新实例,则 DependencyProperty 会按预期更新。但是,如果我只是更改 CurrentFoobar 的当前实例的属性,它不会。自从我提出 属性 changed of CurrentFoobar.
以来,我本以为它会更新
为了更好的衡量,这里是 UpdateCommand:
public class UpdateCommand : ICommand
{
private MainViewModel _vm;
public UpdateCommand(MainViewModel vm)
{
_vm = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_vm.CurrentFoobar.Foo = "World";
_vm.CurrentFoobar.Bar = 84;
}
public event EventHandler CanExecuteChanged;
}
为什么它不起作用
您的代码无法按预期运行的原因是因为 DependencyProperty
仅在 值实际更改时调用其 属性 更改的回调,不是每次设置 属性 时。这与 INotifyPropertyChanged
不同,后者可以编程为在实施者愿意时发送更改通知。
在您的代码中,当 MainViewModel.CurrentFoobar
的 属性 更改时,您会在 CurrentFoobar
本身上引发 PropertyChanged
。 (顺便说一句,这通常不是您应该如何处理的,但我们现在将忽略它。)这告诉绑定更新 UserControl1.Foobar
。问题是,由于 CurrentFoobar
的值实际上并没有改变,UserControl1.FoobarProperty
查看新值并说“哦,那是我们已有的值”并忽略它。它从不调用 属性 已更改的回调,因为 属性 从未实际更改过。
应该如何完成
首先,去掉 _curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
。绑定系统已经知道如果它绑定到 A.B.C
,它需要监视对 A
、B
或 C
的更改。如果 B
发生变化,它知道它需要更新 B
的值,然后还重新评估 C
.
问题是您没有一直使用绑定系统。在 FoobarPropertyChangedCallback
中,您手动为 UI 元素赋值。相反,您应该在 XAML 中使用更多绑定来执行此操作。像这样的东西(在你的 UserControl1.xaml
中):
<TextBlock Name="TextBlock1" Text="{Binding Foobar.Foo}"/>
...
<TextBlock Name="TextBlock2" Text="{Binding Foobar.Bar}"/>
附带说明:要使绑定在 UserControl
内工作,您需要正确设置 DataContext
。它的设置需要与 Window
略有不同。我不会在这里深入,但 this answer 完美地解释了它。 (确保您查看我链接到的具体答案,在撰写本文时接受的答案是错误的)。
在 UserControl
中没有绑定
阅读您的评论后,有一种方法可以在 UserControl
中无需数据绑定(因为没有依赖性 属性 用作目标)。
private static void FoobarPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = d as UserControl1;
if (e.OldValue != null) { (e.OldValue as Foobar).PropertyChanged -= self.FoobarPropertyChanged; }
if (e.NewValue != null) { (e.NewValue as Foobar).PropertyChanged += self.FoobarPropertyChanged; }
self.UpdateFromFoobar();
}
private void FoobarPropertyChanged(object sender, PropertyChangedEventArgs e)
{
UpdateFromFoobar();
}
private void UpdateFromFoobar()
{
//Update all targets with values from current Foobar
if (Foobar != null)
{
TextBlock1.Text = Foobar.Foo;
TextBlock2.Text = Foobar.Bar.ToString();
}
}
您仍然会从 MainViewModel
中删除 _curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
。
以上代码执行以下操作:
确保 FoobarPropertyChanged
始终与 Foobar
的当前值关联。 (而不是任何旧值。)
每当 Foobar
的值改变时调用 UpdateFromFoobar
。
每当当前 Foobar
的任何 属性 发生变化时调用 UpdateFromFoobar
。
您可能想要将 else
添加到 if (Foobar != null)
并清空目标属性。如果有很多属性,基于 PropertyChangedEventArgs.PropertyName
,可以通过只更新受影响的属性来提高效率;当前代码会在 any 属性 of Foobar
发生变化时更新所有目标属性。
在 ViewModel 中,我有一个 属性,它本身实现了 INotifyPropertyChanged。绑定到 DependencyProperty 时,如果我只在绑定到 DependencyProperty 的当前实例上设置属性,即使我引发 属性 更改,我也不会进入 DependencyProperty 更改回调。 (详情见下方代码)
为什么这个构造会失败?
实现我想要的方法的一种方法是每次更改其属性时将 CurrentFoobar 设置为新实例。
是否存在更漂亮的方法来实现所需的行为?
编辑: 我知道这个特定的例子可能看起来无关紧要,因为一个人可能只是简单地绑定到 TextBlocks 文本 属性。但是,我有兴趣为控件做一些事情 像这样 ,与 TextBlock 不同,控件不公开适当的依赖属性。
代码
我有一个带有 属性 Foobar 类型“CurrentFoobar”的 ViewModel。
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
var foobar = new Foobar() {Foo = "Hello", Bar = 42};
CurrentFoobar = foobar;
updateCommand = new UpdateCommand(this);
}
private UpdateCommand updateCommand;
public ICommand UpdateCommand
{
get => updateCommand;
}
private Foobar _curfoobar;
public Foobar CurrentFoobar
{
get => _curfoobar;
set
{
_curfoobar = value;
_curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Foobar 是一个 class 具有两个属性 string Foo 和 int Bar。 Foobar 实现了 INotifyPropertyChanged。
public class Foobar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _foo;
public string Foo
{
get => _foo;
set
{
_foo = value;
OnPropertyChanged();
}
}
private int _bar;
public int Bar
{
get => _bar;
set
{
_bar = value;
OnPropertyChanged();
}
}
}
在 CurrentFoobar setter 中,我订阅了当前 Foobar 实例上的 PropertyChanged,并引发了 OnPropertyChanged。
private Foobar _curfoobar;
public Foobar CurrentFoobar
{
get => _curfoobar;
set
{
_curfoobar = value;
_curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
OnPropertyChanged();
}
}
此外,我有一个带有 CustomControl 的视图,它公开了一个 DependencyProperty“FoobarProperty”。 我将此 DependencyProperty 绑定到我的 ViewModels CurrentFoobar。
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public static readonly DependencyProperty FoobarProperty = DependencyProperty.Register(
"Foobar", typeof(Foobar), typeof(UserControl1), new PropertyMetadata(default(Foobar), FoobarPropertyChangedCallback));
private static void FoobarPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uc1 = d as UserControl1;
var newfb = e.NewValue as Foobar;
uc1.TextBlock1.Text = newfb.Foo;
uc1.TextBlock2.Text = newfb.Bar.ToString();
}
public Foobar Foobar
{
get { return (Foobar) GetValue(FoobarProperty); }
set { SetValue(FoobarProperty, value); }
}
}
如果我将 CurrentFoobar 设置为 Foobar 的新实例,则 DependencyProperty 会按预期更新。但是,如果我只是更改 CurrentFoobar 的当前实例的属性,它不会。自从我提出 属性 changed of CurrentFoobar.
以来,我本以为它会更新为了更好的衡量,这里是 UpdateCommand:
public class UpdateCommand : ICommand
{
private MainViewModel _vm;
public UpdateCommand(MainViewModel vm)
{
_vm = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_vm.CurrentFoobar.Foo = "World";
_vm.CurrentFoobar.Bar = 84;
}
public event EventHandler CanExecuteChanged;
}
为什么它不起作用
您的代码无法按预期运行的原因是因为 DependencyProperty
仅在 值实际更改时调用其 属性 更改的回调,不是每次设置 属性 时。这与 INotifyPropertyChanged
不同,后者可以编程为在实施者愿意时发送更改通知。
在您的代码中,当 MainViewModel.CurrentFoobar
的 属性 更改时,您会在 CurrentFoobar
本身上引发 PropertyChanged
。 (顺便说一句,这通常不是您应该如何处理的,但我们现在将忽略它。)这告诉绑定更新 UserControl1.Foobar
。问题是,由于 CurrentFoobar
的值实际上并没有改变,UserControl1.FoobarProperty
查看新值并说“哦,那是我们已有的值”并忽略它。它从不调用 属性 已更改的回调,因为 属性 从未实际更改过。
应该如何完成
首先,去掉 _curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
。绑定系统已经知道如果它绑定到 A.B.C
,它需要监视对 A
、B
或 C
的更改。如果 B
发生变化,它知道它需要更新 B
的值,然后还重新评估 C
.
问题是您没有一直使用绑定系统。在 FoobarPropertyChangedCallback
中,您手动为 UI 元素赋值。相反,您应该在 XAML 中使用更多绑定来执行此操作。像这样的东西(在你的 UserControl1.xaml
中):
<TextBlock Name="TextBlock1" Text="{Binding Foobar.Foo}"/>
...
<TextBlock Name="TextBlock2" Text="{Binding Foobar.Bar}"/>
附带说明:要使绑定在 UserControl
内工作,您需要正确设置 DataContext
。它的设置需要与 Window
略有不同。我不会在这里深入,但 this answer 完美地解释了它。 (确保您查看我链接到的具体答案,在撰写本文时接受的答案是错误的)。
在 UserControl
中没有绑定
阅读您的评论后,有一种方法可以在 UserControl
中无需数据绑定(因为没有依赖性 属性 用作目标)。
private static void FoobarPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = d as UserControl1;
if (e.OldValue != null) { (e.OldValue as Foobar).PropertyChanged -= self.FoobarPropertyChanged; }
if (e.NewValue != null) { (e.NewValue as Foobar).PropertyChanged += self.FoobarPropertyChanged; }
self.UpdateFromFoobar();
}
private void FoobarPropertyChanged(object sender, PropertyChangedEventArgs e)
{
UpdateFromFoobar();
}
private void UpdateFromFoobar()
{
//Update all targets with values from current Foobar
if (Foobar != null)
{
TextBlock1.Text = Foobar.Foo;
TextBlock2.Text = Foobar.Bar.ToString();
}
}
您仍然会从 MainViewModel
中删除 _curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
。
以上代码执行以下操作:
确保
FoobarPropertyChanged
始终与Foobar
的当前值关联。 (而不是任何旧值。)每当
Foobar
的值改变时调用UpdateFromFoobar
。每当当前
Foobar
的任何 属性 发生变化时调用UpdateFromFoobar
。
您可能想要将 else
添加到 if (Foobar != null)
并清空目标属性。如果有很多属性,基于 PropertyChangedEventArgs.PropertyName
,可以通过只更新受影响的属性来提高效率;当前代码会在 any 属性 of Foobar
发生变化时更新所有目标属性。