WPF Multibinding not Updating Source when Expected; Checkboxes with 'Select All'
Asked Answered
F

1

7

I have a collection of variables in my viewmodel:

public ObservableCollection<ObservableVariable> Variables { get; }= new ObservableCollection<ObservableVariable>();

The ObservableVariable class has two properties: string Name, and bool Selected; the class implements INotifyPropertyChanged,

My goal is to have this collection bound to a checklist in a WPF view, and to have a 'select all' checkbox bound to that list implemented using MultiBinding. The following image illustrates the desired view.

WPF checklist with 'Select All'

Observe the XAML below:

<CheckBox Content="Select All" Name="SelectAllCheckbox"></CheckBox>
...
<ListBox ItemsSource="{Binding Variables}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Name}">
                <CheckBox.IsChecked>
                    <MultiBinding Converter="{StaticResource LogicalOrConverter}" Mode="TwoWay">
                        <Binding Path="Selected"></Binding>
                        <Binding ElementName="SelectAllCheckbox" Path="IsChecked"></Binding>
                    </MultiBinding>
                </CheckBox.IsChecked>
            </CheckBox>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

The LogicalOrConverter takes any number of bools; if any are true, return true.

As you can see above each checkbox is bound to a variable in the viewmodel and the state of the 'select all' checkbox. Currently, everything works as desired EXCEPT the following: If I click 'Select All,' the checkboxes update in the view, but the change does NOT propagate back to the viewmodel.

Note, most things in my implementation work correctly. For example, if I click an individual checkbox, the viewmodel is updated correctly.

The problem in more detail:

When I click an individual checkbox the OnPropertyChanged event is fired in the variable whose box was just changed; the ConvertBack function in the converter is fired; the viewmodel is updated and all is well.

However, when I click the "Select All" checkbox, the individual checkboxes are updated in the view, but OnPropertyChanged is not called in any variable, and the ConvertBack function in the converter is not called.

Also relevent, if I uncheck "Select All," the individual checks go back to what they were before.

The only way to update the viewmodel is to click in the individual checkboxes. However, the multibinding works for the purposes of the view.

My question is:

Why aren't changes to the checkbox propagated to the source collection in the viewmodel

The converter:

public class LogicalOrConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {

        foreach (object arg in values)
        {
            if ((arg is bool) && (bool)arg == true)
            {
                return true;
            }
        }

        return false;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        object[] values = new object[2] {false, false};

        if (value is bool && (bool) value == true)
            values[0] = true;

        return values;
    }
}

ObservableVariable definition:

public class ObservableVariable : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
        }
    }

    private bool _selected;
    public bool Selected
    {
        get { return _selected; }
        set
        {
            _selected = value;
            OnPropertyChanged(nameof(Selected));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
Finecut answered 23/10, 2017 at 19:4 Comment(6)
A question - which value have to have the checkboxes, if they were selected via "check all" and then deselected via "check all". With your MValConverter they will be never deselected, if the values will be propagated to the VM.Shanly
@Rekshino, sorry for the late response. I haven't worked out how deselecting will work. It might be necessary to add logic for that, depending on what I want to do. I intentionally left out that part of the problem because I consider the deselection logistics a separate issue.Finecut
If you necessarily want to use multibinding for updating of viewmodel you can get it via a workaround, but I doubt it will be useful in the praxis. I'll post later my answer if you need it.Shanly
do you need it?Shanly
@Shanly Thank you for offering, but no. I know several workarounds to solve my problem and how to implement them. I posted here to find out if I can implement this select-all checkbox in true MVVM. To me, that means no select-all logic in the view model, and, ideally, no code behind.Finecut
It is possible to do it in View only(!) in XAML + behaviors(behavior could be considered as code behind, but the plus is, that it can be reused). I desagree with you, that implementing the "select all" logic in a viewmodel isn't "true MVVM". What Ginger Ninja has answered is true MVVM and in my opinion preferred way!Shanly
S
5

The problem with your multibinding is that it will "trigger" on both data changes, but the first binding (Path="Selected") is the one that will update the data in your VM as that is what the data is bound to. The second binding will only trigger the SelectAll Checkbox and change the IsChecked properties. Just because you have a MultiBinding does not mean the other Bindings will propegate their changes to one another.

This is why you see the behaviour of a click on SelectAll and the checkboxes change but not the data. You havent explicitly setup a mechanism for the SelectAll checkbox to tell the ViewModel to change the data.

Through a little trial and error I determined that there is no clear and easy way to do this through a MultiBinding alone (if someone has a way to do it, i am interested to learn). I also tried DataTriggers, which was getting messy. The best method i found was to offload the SelectAll logic to the Viewmodel and use a Command on the SelectAll Checkbox. This allows you to control the logic nicely and allows for stronger debugging.

New XAML:

<CheckBox Content="Select All" x:Name="SelectAllCheckbox" 
          Command="{Binding SelectAllCommand}" 
          CommandParameter="{Binding IsChecked, RelativeSource={RelativeSource Self}}"/>


    <ListBox ItemsSource="{Binding Variables}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding Name}" 
                          IsChecked="{Binding Selected}">
                </CheckBox>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

I included the IsChecked as a parameter so you can control Select and Deselect.

My ViewModel:

public class ViewModel
{
    public ObservableCollection<ObservableVariable> Variables { get; set; }
    public ViewModel()
    {
        Variables = new ObservableCollection<ObservableVariable>();
        SelectAllCommand = new RelayCommand(SelectAll, ()=>true);
    }

    public RelayCommand SelectAllCommand { get; set; }

    public void SelectAll(object param)
    {
        foreach (var observableVariable in Variables)
        {
            observableVariable.Selected = (bool)param;
        }
    }
}

Obviously you want better validation logic on the parameter. This is mainly for a short answer.

And for completeness ill include the standard RelayCommand Code i used.

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    private Action<object> methodToExecute;
    private Func<bool> canExecuteEvaluator;
    public RelayCommand(Action<object> methodToExecute, Func<bool> canExecuteEvaluator)
    {
        this.methodToExecute = methodToExecute;
        this.canExecuteEvaluator = canExecuteEvaluator;
    }
    public RelayCommand(Action<object> methodToExecute)
        : this(methodToExecute, null)
    {
    }
    public bool CanExecute(object parameter)
    {
        if (this.canExecuteEvaluator == null)
        {
            return true;
        }
        else
        {
            bool result = this.canExecuteEvaluator.Invoke();
            return result;
        }
    }
    public void Execute(object parameter)
    {
        this.methodToExecute.Invoke(parameter);
    }
}
Shoffner answered 23/10, 2017 at 21:45 Comment(2)
Interesting. I was hoping to avoid putting the 'select all' logic in the view model. It seems to me that violates the MVVM paradigm, but alas. I often feel WPF doesn't include the tools to implement MVVM properly.Finecut
@ShaneSims it doesnt really violate MVVM. Your view model is there to handle any extra logic between the data and view. and that is what you are doing. You are looking to change the data (Selected Property) in a more complex manner. It is not purely UI based (which would be put in XAML or Code behind) as your data holds that status.Shoffner

© 2022 - 2024 — McMap. All rights reserved.