Multibinding with convertback does not work when one converted value in UnsetValue

6.2k views Asked by At

I have the following Multibinding:

 <Grid.Visibility>
    <MultiBinding Converter="{StaticResource MyMultiValueConverter}" Mode="TwoWay">
      <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MyVisibilityDependencyProperty" Mode="TwoWay"/>
      <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MyBoolProperty" Mode="TwoWay"/>
   </MultiBinding>
</Grid.Visibility>

MyVisibilityDependencyProperty is a dependency property. MyBoolProperty is a normal property. The implementation of MyMultiValueConverter is the important thing:

public class MyMultiValueConverter: IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
       //Not interesting
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new[] { value, Binding.DoNothing};
    }
}

Now the scenario: I do smth. that triggers a call of the ConvertBack-Method, which means I hit a break point there. Afterwards I hit a break point in the OnPropertyChangedCallback of MyVisibilityDependencyProperty. There I can see that the new value of MyVisibilityDependencyProperty is the value that was set in the ConvertBack-Method.

Now the issue that I do not understand. I change the implementation of the ConvertBack-Method to:

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new[] { value, DependencyProperty.UnsetValue};
    }

Now I follow the exact same scenario. I do smth. that triggers a call of the ConvertBack-Method, which means I hit a break point there. After that nothing happens. The OnPropertyChangedCallback is not called and MyVisibilityDependencyProperty is not updated. Why?

It seems like that if one of the values in the array is DependencyProperty.UnsetValue, propagation of all values is stopped. Not only for that value but all values in the array. This is supported by the following behavior:

return new[] { Binding.DoNothing, false };

This results in a call of the setter of MyBoolProperty.

return new[] { DependencyProperty.UnsetValue, false };

This does not call the setter of MyBoolProperty.

I could not find any hints of explanation in documentation and it does not make sense in my opinion.

4

There are 4 answers

2
Mike Strobel On BEST ANSWER

I could not find any hints of explanation in documentation and it does not make sense in my opinion.

I don't recall ever seeing it in the documentation, but your observations are correct:

If any element in the result of IMultiValueConverter.ConvertBack is UnsetValue, the entire set of proposed values is rejected, i.e., the conversion fails, and the none of the child bindings have their source values updated.

The relevant code can be found in the MultiBindingExpression class. Below is an abbreviated excerpt.

internal override object ConvertProposedValue(object value)
{
    object result;
    bool success = ConvertProposedValueImpl(value, out result);
    {
        result = DependencyProperty.UnsetValue;
        ...
    }
    return result;
}

private bool ConvertProposedValueImpl(object value, out object result)
{
    result = GetValuesForChildBindings(value);

    object[] values = (object[])result;
    int count = MutableBindingExpressions.Count;
    bool success = true;

    // use the smaller count
    if (values.Length < count)
        count = values.Length;

    for (int i = 0; i < count; ++i)
    {
        value = values[i];
        ...
        if (value == DependencyProperty.UnsetValue)
            success = false; // if any element is UnsetValue, conversion fails
        values[i] = value;
    }

    result = values;
    return success;
}

As to whether it makes sense, I think that it does. A value of DoNothing in the result array indicates that the corresponding child binding should be skipped, i.e., its source value should not be updated. This, in effect, provides a mechanism for partial updates. If you think about it, the only scenarios we care about are:

  1. All sources updated successfully
  2. Some sources updated successfully
  3. Total failure: no sources updated

The normal behavior provides (1), and the use of DoNothing can satisfy (2). It arguably makes sense to use UnsetValue to indicate a total failure. That is also consistent with its meaning for single-value converters: UnsetValue means the conversion failed. The only difference is that ConvertBack returns object[], so you cannot return UnsetValue directly. You can, however, return an array containing only UnsetValue: since its presence means the entire result gets thrown out, the array length doesn't actually have to match the number of child bindings.

2
Bizhan On

According to MSDN:

UnsetValue is a sentinel value that is used for scenarios where the WPF property system is unable to determine a requested DependencyProperty value. UnsetValue is used rather than null, because null could be a valid property value, as well as a valid (and frequently used) DefaultValue.

In fact you can't set a DependencyProperty to UnsetValue, you can just compare with it. Setting to UnsetValue does not have an effect, you can try that yourself.

2
Benj On

I had a similiar problem some time ago [ Demultiplexing using IMultiValueConverter ]

What worked for me was to 'delay' the assignment of DependencyProperty.UnsetValue: Since a MultiBinding is somewhat a 'collection' of Bindings, you can assign an IValueConverter to each involved 1-1 bindings as follows:

(1) XAML-Markup: Introduce converters for the involved 1:1-bindings

<Grid.Visibility>
    <MultiBinding Converter="{StaticResource MyMultiValueConverter}" Mode="TwoWay">
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MyVisibilityDependencyProperty" Mode="TwoWay" />
        <Binding Converter="UnsetValueConverter" RelativeSource="{RelativeSource TemplatedParent}" Path="MyBoolProperty" Mode="TwoWay"/>
    </MultiBinding>
</Grid.Visibility>

(Note the introduced Converter="UnsetValueConverter" in the second binding)

(2) Implement MyMultiValueConverter: Create copies of the supplied element

public class MyMultiValueConverter: IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
       //Not interesting
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new[] { value, value };
    }
}

(3) Implement the in (1) introduced UnsetValueConverter

public class UnsetValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return DependencyProperty.UnsetValue;
    }
}

This way the disposal is delayed until right before the updated value is delivered

And yes, this isn't an answer to your question, but Mike S. did a good job at explaining it and this attempts to give a generic solution approach

1
Asha On

try this:

Converts source values to a value for the binding target. The data binding engine calls this method when it propagates the values from source bindings to the binding target.

    ///param name="values"> The array of values taht the source bindings in the<see cref="T:System.Windows.Data.MultiBinding"/>produces.The value <see cref="F:System.Windows.DependencyProperty.UnsetValue"/> indicates that the source binding has no value to provide for conversion.</param>
    /// <param name="targetType">The type of the binding target property.</param>
    /// <param name="parameter">The converter parameter to use.</param>
    /// <param name="culture">The culture to use in the converter.</param>
    /// <returns>
    /// A converted value.
    /// If the method returns null, the valid null value is used.
    /// A return value of <see cref="T:System.Windows.DependencyProperty"/>.<see cref="F:System.Windows.DependencyProperty.UnsetValue"/> indicates that the converter did not produce a value, and that the binding will use the <see cref="P:System.Windows.Data.BindingBase.FallbackValue"/> if it is available, or else will use the default value.
    /// A return value of <see cref="T:System.Windows.Data.Binding"/>.<see cref="F:System.Windows.Data.Binding.DoNothing"/> indicates that the binding does not transfer the value or use the <see cref="P:System.Windows.Data.BindingBase.FallbackValue"/> or the default value.
    /// </returns>

public object ConvertBack( object[] values, Type targetType, object parameter, System.Globalization.CultureInfo clture )
    {
        if( parameter == null )
           { return null;}


        return String.Format( parameter.ToString(), values );

    }