kev
kev

Reputation: 48

DependencyProperty not updating on PropertyChanged

In a ViewModel I have a property which itself implements INotifyPropertyChanged. When binding to a DependencyProperty I do not get into the DependencyProperty changed callback when I only set properties on the current instance bound to the DependencyProperty even if I raise property changed. (For details, see the code below)

Why do this construction fail?

One way to achieve what I want is to set CurrentFoobar to new instance every time its properties are changed.

Do there exist prettier ways of achieving the wanted behavior?

Edit: I am aware that this particular example may seem irrelevant since one may simply bind proper to the TextBlocks Text property. However I am interested in doing something like this for a control that do not expose proper dependency properties unlike the TextBlock.


Code

I have a ViewModel with a property "CurrentFoobar" of type Foobar.

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 is a class with two properties string Foo and int Bar. Foobar implements 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();
        }
    }
}

In the CurrentFoobar setter I subscribe to PropertyChanged on the current Foobar instance, and raise OnPropertyChanged.

    private Foobar _curfoobar;
    public Foobar CurrentFoobar
    {
        get => _curfoobar;
        set
        {
            _curfoobar = value;
            _curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();
            OnPropertyChanged();
        }
    }

Furthermore I have a View with a CustomControl that expose a DependencyProperty "FoobarProperty". I bind this DependencyProperty to my 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); }
    }
}

If I set the CurrentFoobar to a new instance of Foobar the DependencyProperty updates as expected. However if I simply change the properties of the current instance of CurrentFoobar it does not. I would have expected it to update since I raise property changed of CurrentFoobar.

For good measure, here is the 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;
}

Upvotes: 0

Views: 2354

Answers (1)

Keith Stein
Keith Stein

Reputation: 6724

Why it doesn't work

The reason your code doesn't function as you'd expect is because DependencyPropertys only call their property changed callbacks when the value actually changes, not every time the property is set. This is different from INotifyPropertyChanged which can be programmed to send a change notification whenever the implementer feels like it.

In your code, when a property of MainViewModel.CurrentFoobar changes you raise PropertyChanged on CurrentFoobar itself. (This, by the way, is not generally how you're supposed to handle this, but we'll ignore that for now.) This tells the binding to update UserControl1.Foobar. The thing is, since the value of CurrentFoobar hasn't actually changed, UserControl1.FoobarProperty looks at the new value and say "oh, that's the same one we already have" and ignores it. It never calls the property changed callback because the property never actually changed.

How this should be done

First off, get rid of _curfoobar.PropertyChanged += (s, e) => OnPropertyChanged();. The binding system already knows that if it's binding to A.B.C, it needs to watch for changes to either A, B, or C. If B changes, it knows it needs to update the value for B and then also reevaluate C.

The problem is that you aren't using the binding system all the way through. In FoobarPropertyChangedCallback you are manually assigning values to UI elements. Instead, you should be doing this in XAML with more bindings. Something like this (in your UserControl1.xaml):

<TextBlock Name="TextBlock1" Text="{Binding Foobar.Foo}"/>
...
<TextBlock Name="TextBlock2" Text="{Binding Foobar.Bar}"/>

As a side note: for bindings to work inside a UserControl, you need to set the DataContext correctly. It needs to be set up slightly differently than on a Window. I won't go into that here, but this answer explains it perfectly. (Make sure you look at the specific answer I linked to, the accepted one at time of writing does it wrong).

Without binding in the UserControl

After reading your comment, here's a way you could make this work without data binding in the UserControl (since there's no dependency property to use as a target).

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();
    }
}

You would still remove _curfoobar.PropertyChanged += (s, e) => OnPropertyChanged(); from MainViewModel.

The above code does the following:

  1. Makes sure FoobarPropertyChanged is always associated with the current value of Foobar. (And not any of the old values.)

  2. Calls UpdateFromFoobar whenever the value of Foobar changes.

  3. Calls UpdateFromFoobar whenever any property of the current Foobar changes.

You might want to add an else to if (Foobar != null) and blank out the target properties. If there are a lot of properties, this could be made more efficient by only updating the ones that are effected, based on PropertyChangedEventArgs.PropertyName; the current code updates all target properties whenever any property of Foobar changes.

Upvotes: 2

Related Questions