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

Sha*_*ims 6 c# wpf mvvm multibinding

我的viewmodel中有一组变量:

public ObservableCollection<ObservableVariable> Variables { get; }= new ObservableCollection<ObservableVariable>();
Run Code Online (Sandbox Code Playgroud)

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

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

带有'全选'的WPF清单

观察下面的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>
Run Code Online (Sandbox Code Playgroud)

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

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

注意,我的实现中的大多数事情都能正常工作 例如,如果单击某个复选框,则视图模型会正确更新.

问题更详细:

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

但是,当我单击"全选"复选框时,视图中会更新各个复选框,但不会在任何变量中调用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

Gin*_*nja 5

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

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

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

新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>
Run Code Online (Sandbox Code Playgroud)

我将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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

并且为了完整性,包括我使用的标准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);
    }
}
Run Code Online (Sandbox Code Playgroud)