0

Playing with MultiBinding:

What I want: clicking either checkbox should toggle all others.

Problem: clicking A doesn't change B, clicking B doesn't change A. Result works.

Question: how would I fix it, while still using MultiBinding?

P.S.: this is an attempt to solve more complicated problem, please refer to it before offering to bind all checkboxes to a single property.


Below is a mcve.

xaml:

<StackPanel>
    <CheckBox Content="A" IsChecked="{Binding A}" />
    <CheckBox Content="B" IsChecked="{Binding B}" />
    <CheckBox Content="Result">
        <CheckBox.IsChecked>
            <MultiBinding Converter="{local:MultiBindingConverter}">
                <Binding Path="A" />
                <Binding Path="B" />
            </MultiBinding>
        </CheckBox.IsChecked>
    </CheckBox>
</StackPanel>

cs:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }
}

ViewModel:

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string property = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));

    bool _a;
    public bool A
    {
        get { return _a; }
        set { _a = value; OnPropertyChanged(); }
    }

    bool _b;
    public bool B
    {
        get { return _b; }
        set { _b = value; OnPropertyChanged(); }
    }
}

Converter:

public class MultiBindingConverter : MarkupExtension, IMultiValueConverter
{
    public MultiBindingConverter() { }

    public override object ProvideValue(IServiceProvider serviceProvider) => this;

    object[] _old;

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        // first time init
        if (_old == null)
            _old = values.ToArray();
        // find if any value is changed and return value
        for (int i = 0; i < values.Length; i++)
            if (values[i] != _old[i])
            {
                _old = values.ToArray();
                return values[i];
            }
        // if no changes return first value
        return values[0];
    }


    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
        Enumerable.Repeat(value, targetTypes.Length).ToArray();
}
Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • why not using a property result returning a || b and raising the property change when updating a or b ? – Boo Mar 17 '16 at 15:07
  • @Boo, because you haven't read `P.S.` ;) One of the properties (e.g. `A`) will not be available in ViewModel. – Sinatr Mar 17 '16 at 15:08
  • My two cents: you miss a ViewModel here. – Arnaud Weil Mar 17 '16 at 15:09
  • 1
    @ArnaudWeil, press `control`+`F`, type *`ViewModel`* and press `Enter`. – Sinatr Mar 17 '16 at 15:10
  • Funny. Seriously, what you call a ViewModel is not a ViewModel, because it doesn't expose a property for every input. Your multibinding is here to hide the fact that it's not a read ViewModel. – Arnaud Weil Mar 18 '16 at 15:27
  • @ArnaudWeil, it does. There are `A` and `B` for you to get combined result, but I think I understand what you mean. Changing their values is not the same as getting their values (getting is a common task used in conjunction with converters, e.g. to convert `int` or event expression result of multiple properties into `Color`). – Sinatr Mar 18 '16 at 15:36
  • The `ConvertBack()` method of the MultiBindingConverter will **not** get called when you change `A` or `B` respectively, only when you click `Result` the method is called. And it is the sane thing to do it that way, because otherwise you could rather carelessly create an infinte loop of calling property setters and getters through `MultiBinding` on the one hand. And on the other hand, `Result` doesn't really need to call an update of the property because it has just been read by the getter updating the view and thus hasn't really changed. – Adwaenyth Aug 08 '17 at 14:00

2 Answers2

0

I think your converter should look like this

public class MultiBindingConverter : MarkupExtension, IMultiValueConverter
    {
        public MultiBindingConverter() { }

        public override object ProvideValue(IServiceProvider serviceProvider) => this;

        object[] _old;

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return ((bool)values[0] /*A */) || ((bool)values[1]/* B */);
        }


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

and then

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string property = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));

    bool _a;
    public bool A
    {
        get { return _a || _b; }
        set {
              if (_a == value) return;
              _a = value; 
              OnPropertyChanged("A");
              OnPropertyChanged("B");
            }
    }

    bool _b;
    public bool B
    {
        get { return _b || _a; }
        set {
              if (_b == value) return;
              _b = value; 
              OnPropertyChanged("B");
              OnPropertyChanged("A");
            }
    }
}
Boo
  • 663
  • 5
  • 10
  • This converter has same problem: clicking `A` will not change `B` (only `A` and `Result` will be changed). – Sinatr Mar 17 '16 at 15:27
  • @Sinatr with the change to the vm added – Boo Mar 17 '16 at 15:42
  • This change to ViewModel makes `A` and `B` too dependent and renders `MultiBinding` useless. As I mention in comments, `A` will not be available in the ViewModel (it will be an attached property in the view), so this kind of solution is not useful to me, sorry about that. I was trying to make `mcve` as easy as possible, so didn't separate `A` and `B` enough (so that you can't manipulate with them in ViewModel). – Sinatr Mar 17 '16 at 15:54
  • well in that case just add a property result returning a|| b, i dont see the problem – Boo Mar 17 '16 at 15:58
0

The simple reason is, the ConvertBack() method will never be called when you click CheckBox A.

Consider the following reasoning:

Checkbox A gets checked.

<Binding /> calls Property-Setter A.

Property-Setter A gets called.

Property-Setter A calls OnPropertyChanged("A").

PropertyChanged-Event is recieved by the <MultiBinding /> of CheckBox Result.

Property-Getters A and B (which is still unchanged) are being called.

MultiBindingConverter.Convert() method gets called by the Binding.

<MultiBinding /> updates the CheckBox Result IsChecked state in the view.


Handling of change is done without ever touching CheckBox B and only calling the getter of property B.


If you have a MultiBinding on all CheckBoxes, then all appropriate setters will be called. You might need to implement a different Converter though, if the change behaviour should be different for each CheckBox.

That is also the reason, why changing stuff like that should - preferably - be done within the ViewModel if possible, because all those Bindings and Converters make it a little hard to track.

Adwaenyth
  • 2,020
  • 12
  • 24
  • Took me some time to remember stuff, thanks for showing interest. *"within the ViewModel if possible"* - unfortunately it's not possible, VM doesn't know about attached property, nor attached property about VM (see [this question](https://stackoverflow.com/q/36061719/1997232) for origin of the problem, where multibinding is an *attempted* solution). The explanation is clear, I accept the "no" as the answer. – Sinatr Aug 08 '17 at 14:35