WPF Multibinding在预期时不更新源;带有“全选”的复选框

时间:2017-10-23 19:04:11

标签: c# wpf mvvm multibinding

我的viewmodel中有一组变量:

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

ObservableVariable类有两个属性:string Name和bool Selected;该类实现了INotifyPropertyChanged,

我的目标是将此集合绑定到WPF视图中的清单,并使用MultiBinding实现绑定到该列表的“全选”复选框。下图说明了所需的视图。

WPF checklist with 'Select All'

观察下面的XAML:

<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>

LogicalOrConverter可以使用任意数量的bool;如果有的话,返回true。

如上所示,每个复选框都绑定到viewmodel中的变量和'select all'复选框的状态。目前,一切都按预期工作除了以下内容:如果单击“全选”,复选框将在视图中更新,但更改不会传播回视图模型。

注意,我的实现中的大部分内容都能正常工作。例如,如果单击某个复选框,则视图模型会正确更新。

问题更详细:

当我单击一个单独的复选框时,OnPropertyChanged事件会在其框刚刚更改的变量中触发;转换器中的ConvertBack函数被触发;视图模型已更新,一切正常。

但是,当我单击“全选”复选框时,视图中会更新各个复选框,但不会在任何变量中调用OnPropertyChanged,并且不会调用转换器中的ConvertBack函数。

同样相关,如果我取消选中“全选”,个别支票会回到之前的状态。

更新viewmodel的唯一方法是单击各个复选框。但是,多绑定适用于视图。

我的问题是:

为什么未对viewmodel中的源集合传播复选框进行更改

转换器:

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定义:

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));
    }
}

1 个答案:

答案 0 :(得分:5)

多绑定的问题在于它将“触发”两个数据更改,但第一个绑定(Path="Selected")是将更新VM中数据的绑定,因为数据绑定的是至。第二个绑定仅触发SelectAll复选框并更改IsChecked属性。仅仅因为你有一个MultiBinding并不意味着其他Bindings会推动他们彼此的变化。

这就是为什么你会看到点击SelectAll的行为,复选框会改变而不是数据。您没有明确设置SelectAll复选框的机制来告诉ViewModel更改数据。

通过一些试验和错误,我确定没有明确而简单的方法通过单独的MultiBinding来做到这一点(如果有人有办法,我有兴趣学习)。我也试过DataTriggers,它变得很乱。我发现的最佳方法是将SelectAll逻辑卸载到Viewmodel并在SelectAll复选框上使用Command。这使您可以很好地控制逻辑并允许更强大的调试。

新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>

我将IsChecked作为参数包含在内,因此您可以控制选择和取消选择。

我的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;
        }
    }
}

显然,您希望在参数上获得更好的验证逻辑。这主要是为了简短的回答。

为了完整性,包括我使用的标准RelayCommand Code。

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);
    }
}