Search code examples
c#wpfmultibinding

WPF Twoway MultiBinding => lost binding?


I have a checkbox with a MultiBinding where one binding is twoway (to a viewmodel) and the other is oneway (to it's own IsEnabled property). Everything seems to work fine until I touch the multibound checkbox. Then I suddenly loose a binding.

The following sample demonstrates this effect. In the real program, the IsEnabled property is also a multibinding, but that doesn't seem to make a difference.

<Window x:Class="TwowayMultiBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TwowayMultiBinding"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <StackPanel.Resources>
            <local:LogicalAndConverter x:Key="LogicalAndConverter"/>


        </StackPanel.Resources>
        <CheckBox Content="Enabled" Name="EnableCheck"/>
        <CheckBox Content="Ticked" Name="TickCheck"/>
        <CheckBox Content="Test" Name="TestCheck" IsEnabled="{Binding ElementName=EnableCheck, Path=IsChecked}">
            <CheckBox.IsChecked>
                <MultiBinding Converter="{StaticResource LogicalAndConverter}">
                    <Binding ElementName="TestCheck" Path="IsEnabled" Mode="OneWay"/>
                    <Binding ElementName="TickCheck" Path="IsChecked" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"/>
                </MultiBinding>
            </CheckBox.IsChecked>
        </CheckBox>

    </StackPanel>
</Window>

And I'm using the following converter:

public class LogicalAndConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (values == null || values.Length < 2)
        {
            return DependencyProperty.UnsetValue;
        }
        for (int i = 0; i < values.Length; i++)
        {
            bool result;
            if (values[i] is bool bValue)
            {
                result = bValue;
            }
            else if (values[i] is bool?)
            {
                result = ((bool?)values[i]) ?? false;
            }
            else
            {
                return DependencyProperty.UnsetValue;
            }
            if (!result)
            {
                return false;   // early exit.
            }
        }
        return true;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value is bool bValue)
        {
            return Enumerable.Repeat(value, targetTypes.Length).ToArray();
        }
        else if (value is bool?)
        {
            bool result = ((bool?)value) ?? false;
            return Enumerable.Repeat((object)result, targetTypes.Length).ToArray();
        }
        return null;
    }

When I run this example, the Test Check is disabled, because the Enabled checkbox is not checked (correct).

To reproduce the problem:

  • Click Enabled => Test check is enabled (correct).
  • Click Ticked => Test check is ticked (correct).
  • Click Ticked => Test check is unticked (correct).
  • Click Ticked => Test check is ticked (correct).
  • Click Enabled => Test check is disabled AND unticked (correct).
  • Click Enabled => Test check is enabled and ticked (since Tick Check is still checked). (correct)
  • Click Test => Test check is unchecked (correct).

But now, the Enabled check does not work anymore. Did I loose the binding?

Short route:

  • Start
  • Click Enabled => Test check is enabled (correct).
  • Click Test => Test check is unchecked (correct).

Once again, Enabled doesn't work anymore.

What I'm trying to do is:

  • Bind the check value to it's viewmodel (two way).
  • If Enabled is not checked, then the Test check should not be able to be checked in any way.

Solution

  • After Andy's answer that a binding is effectively lost unless it's a twoway binding, I decided to make it a twoway binding and let a 2nd converter avoid the checkbox actually being disabled when I click the Test Check. And this works:

    <Window x:Class="TwowayMultiBinding.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:TwowayMultiBinding"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
        <StackPanel>
            <StackPanel.Resources>
                <local:LogicalAndConverter x:Key="LogicalAndConverter"/>
                <local:OneWayBooleanConverter x:Key="OneWayBooleanConverter"/>
    
    
            </StackPanel.Resources>
            <CheckBox Content="Enabled" Name="EnableCheck"/>
            <CheckBox Content="Ticked" Name="TickCheck"/>
            <CheckBox Content="Test" Name="TestCheck" IsEnabled="{Binding ElementName=EnableCheck, Path=IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                <CheckBox.IsChecked>
                    <MultiBinding Converter="{StaticResource LogicalAndConverter}">                    
                        <Binding ElementName="TickCheck" Path="IsChecked" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding ElementName="TestCheck" Path="IsEnabled" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" Converter="{StaticResource OneWayBooleanConverter}"/>
                    </MultiBinding>
                </CheckBox.IsChecked>
            </CheckBox>
    
        </StackPanel>
    </Window>
    
    public class OneWayBooleanConverter : IValueConverter
        {
            private bool lastValue = false;
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                if (value is bool bValue)
                {
                    lastValue = bValue;
                    return bValue;
                }
                return DependencyProperty.UnsetValue;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                if (value is bool bValue)
                {
                    return lastValue;
                }
                return DependencyProperty.UnsetValue;
            }
        }
    

    The LogicalAndConverter class has been unchanged. Not sure if this solution is aesthetically correct, but atleast it works.

    Unfortunately, this doesn't work for the real-world program, because the checkbox being disabled is part of a item control. It's impossible to know which checkbox (and with that which cached value) is used from within the IValueConverter.ConvertBack function.

    Thankfully, the requirement for which I needed this has been dropped, so I don't have to worry about it any longer.