WPF: TwoWay binding is ALWAYS updated - OneWay binding is ONLY ONCE updated

1.7k views Asked by At

I know what you think: It is 2017, please don't come up with this again, but I really can not find any valueable explanation for this.

Please have a look at the ActiveNotes property in this XAML-Code.

I have this TwoWay binding in my XAML, which works perfectly. It is ALWAYS updated, if the PropertyChanged event for ScaleNotes is fired and if the binding is set to TwoWay.

<c:Keyboard 
            Grid.Row="2" 
            Grid.Column="0" 
            PlayCommand="{Binding PlayCommand}" 
            StopCommand="{Binding StopCommand}" 
            ActiveNotes="{Binding ScaleNotes, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

The ScaleNotes property in the ViewModel looks like this. Whenever it changes, the PropertyChanged event is guaranteed to be fired. I checked and double checked it. The business logic in the ViewModel works.

private ReadOnlyCollection<eNote> _ScaleNotes;
public ReadOnlyCollection<eNote> ScaleNotes
{
    get { return _ScaleNotes; }
    set { SetField(ref _ScaleNotes, value); }
}

[DebuggerStepThrough]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

[DebuggerStepThrough]
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(propertyName);
    return true;
}

Up to here everything is ok. Whenever the ScaleNotes property in the VM is changed, the target property ActiveNotes is updated.

Now the problem:

If I only change the binding in the XAML to OneWay and the business logic in the VM stays 100% the same, the ActivesNotes property in the target object is updated only once even if the PropertyChanged event is fired. I checked and double checked it. The PropertyChanged event for the ScaleNotes property is always fired.

<c:Keyboard 
            Grid.Row="2" 
            Grid.Column="0" 
            PlayCommand="{Binding PlayCommand}" 
            StopCommand="{Binding StopCommand}" 
            ActiveNotes="{Binding ScaleNotes, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>

Just to make this complete, here is DP in the target object.

public static DependencyProperty ActiveNotesProperty = DependencyProperty.Register(
        "ActiveNotes", 
        typeof(ReadOnlyCollection<eNote>), 
        typeof(Keyboard), 
        new PropertyMetadata(OnActiveNotesChanged));


public ReadOnlyCollection<eNote> ActiveNotes
{
    get
    {
        return (ReadOnlyCollection<eNote>)GetValue(ActiveNotesProperty);
    }
    set
    {
        SetValue(ActiveNotesProperty, value);
    }
}

private static void OnActiveNotesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Keyboard keyboard    = (Keyboard)d;
    keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;

    if ((keyboard.ActiveNotes != null) && (keyboard.ActiveNotes.Count > 0))
    {
        keyboard.AllKeys.ForEach(k => { if ( k.Note != eNote.Undefined) k.IsActiveKey = true; });
        keyboard.AllKeys.ForEach(k => { if ((k.Note != eNote.Undefined) && (!keyboard.ActiveNotes.Contains(k.Note))) k.IsActiveKey = false; });
    }
    else
    {
        keyboard.AllKeys.ForEach(k => { if (k.Note != eNote.Undefined) k.IsActiveKey = true; });
    }
}

I don't understand this. From my knowledge, OneWay and TwoWay only define in which direction the values are updated and not how often they can be updated.

I can not understand, that everything works fine with TwoWay, the business logic stays 100% the same and OneWay is a deal breaker.

If you ask yourself, why I want to know this: This binding was planned as a OneWay binding. It makes no sense to update the source in any way. I only changed it to TwoWay, because OneWay doesn't work as expected.

SOLUTION with the help of @MikeStrobel: (see comments)

The code needs be changed this way:

private static void OnActiveNotesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Keyboard keyboard    = (Keyboard)d;

    //THIS LINE BREAKED THE CODE, WHEN USING OneWay binding BUT NOT WITH TwoWay binding
    //keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;

    if ((keyboard.ActiveNotes != null) && (keyboard.ActiveNotes.Count > 0))
    {
        keyboard.AllKeys.ForEach(k => { if ( k.Note != eNote.Undefined) k.IsActiveKey = true; });
        keyboard.AllKeys.ForEach(k => { if ((k.Note != eNote.Undefined) && (!keyboard.ActiveNotes.Contains(k.Note))) k.IsActiveKey = false; });
    }
    else
    {
        keyboard.AllKeys.ForEach(k => { if (k.Note != eNote.Undefined) k.IsActiveKey = true; });
    }
}

Using a OneWay binding, the assignment in the OnActiveNotesChanged event handler method deletes or clears out the binding. Mike is correct saying, that the assingment is completely unnecessary, because at this point of time the value is already set before in the property. So it makes no sense at all, no matter if I use OneWay or TwoWay binding.

2

There are 2 answers

3
Mike Strobel On BEST ANSWER

Dependency properties have a complex system of precedence. The value of a dependency property at any given time may come from various sources: bindings, style setters, trigger setters, etc. Local values have the highest priority, and when you set a local value, you suppress values coming from other sources.

In the case of a binding, setting a local value will cause a source-to-target binding (OneWay or OneTime) to be *removed*. However, when you set a local value on a property with a target-to-source binding (TwoWay or OneWayToSource), the binding will be maintained, and the local value you assigned will get propagated back to the source.

In your case, the issue is here:

keyboard.ActiveNotes = (ReadOnlyCollection<eNote>)e.NewValue;

In your OnActiveNotesChanged handler, you're assigning a new local value to ActiveNotes, which is causing your OneWay binding to be removed. Fortunately, the solution is simple: you can remove this line entirely, as it is redundant. In most cases it is unnecessary to assign a dependency property in its own change handler—the new value has already been applied. (And if you want an opportunity to replace a proposed value before it gets applied, the place to do that would be a CoerceValueCallback, which you can also specify in you PropertyMetadata.)

0
John440 On

Let me share my experience after spending sleepless nights on this issue!

I stumbled into this issue on a slightly different use case for which I could not find a solution, but thanks to the advice above I could finally solve it.

To put it short, I faced the same Binding dissociation issue but without having code behind accessing the incriminated property at all! The issue occurred on a CustomControl!

In fact, you must also be aware that if a CustomControl declares a DependencyProperty which is used in a Binding both by the consumer of the CustomControl and inside the Template of the CustomControl, then you MUST ensure that our Binding in the Template of the CustomControl is eighter of type TemplateBinding or a regular binding using OneWay or OneWayToSource.

public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register("IsChecked", typeof(bool), typeof(QAToggle), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.None));

public bool IsChecked
{
    get => (bool)this.GetValue(IsCheckedProperty);
    set => this.SetValue(IsCheckedProperty, value);
}

If not, the Binding used in the UserControl's template when updating will also set a new value to the CustomControls Property thus removing the external binding set up by the consumer of the CustomControl.

<Style TargetType="{x:Type ccont:QAToggle}">
    
    <Setter Property="SnapsToDevicePixels"  Value="true" />
    <Setter Property="MinHeight"            Value="23" />
    <Setter Property="MinWidth"             Value="75" />
    
    <Setter Property="Template">
        <Setter.Value>

            <ControlTemplate TargetType="{x:Type ccont:QAToggle}">

                <ToggleButton x:Name            = "QAToggle"
                              Command           = "{TemplateBinding Command}" 
                              CommandParameter  = "{TemplateBinding CommandParameter}" 
                              FontSize          = "{TemplateBinding FontSize}"
                              FontWeight        = "{TemplateBinding FontWeight}"
                              FontFamily        = "{TemplateBinding FontFamily}"
                              IsThreeState      = "False"
                              IsChecked         = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}}">

Here the line IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}}" will break the external binding. To avoid this change it to:

IsChecked = "{TemplateBinding IsChecked}"

or

IsChecked = "{Binding IsChecked, RelativeSource={RelativeSource AncestorType=ccont:QAToggle}, Mode=OneWay}"