简体   繁体   中英

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.

Yes, you effectively lost the binding.

If you set a dependencyproperty to a value then that value over rides any binding unless the binding is twoway.

When you click, you are setting the ischecked value on Test.

One way to avoid this is to move your logic into the viewmodel. You can then have fine control in the setter and getters of your various properties over exactly what you want to happen.

You may want to consider intermediate properties so you have a properties representing the view and input, you have other properties or just fields represent internal state.

Maybe you're trying to use the Test checkbox for two different purposes where two different controls might be more appropriate. Maybe it should be read only.

I think you need to change the back converter:

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM