Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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));
    }
}
like image 853
Shane Sims Avatar asked Oct 23 '17 19:10

Shane Sims


1 Answers

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);
    }
}
like image 91
Ginger Ninja Avatar answered Nov 16 '22 05:11

Ginger Ninja