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,它需要监视对 ABC 的更改。如果 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();

以上代码执行以下操作:

  1. 确保 FoobarPropertyChanged 始终与 Foobar 的当前值关联。 (而不是任何旧值。)

  2. 每当 Foobar 的值改变时调用 UpdateFromFoobar

  3. 每当当前 Foobar 的任何 属性 发生变化时调用 UpdateFromFoobar

您可能想要将 else 添加到 if (Foobar != null) 并清空目标属性。如果有很多属性,基于 PropertyChangedEventArgs.PropertyName,可以通过只更新受影响的属性来提高效率;当前代码会在 any 属性 of Foobar 发生变化时更新所有目标属性。