WPF多绑定未按预期更新源;带有“全选”复选框

7

我在我的视图模型中有一组变量:

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

ObservableVariable类有两个属性:string Name和bool Selected;该类实现了INotifyPropertyChanged接口。
我的目标是将此集合绑定到WPF视图中的复选框,并使用MultiBinding实现将“全选”复选框绑定到该列表。下面的图像说明了所需的视图。
请注意下面的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,则返回 true。
如上所示,每个复选框都绑定到视图模型中的一个变量和“全选”复选框的状态。目前,除以下情况外,一切都按预期工作:如果我单击“全选”,则复选框在视图中更新,但更改不会传播回视图模型。
请注意,我的实现中大多数东西都可以正确工作。例如,如果我单击单个复选框,则视图模型会正确更新。
问题更详细地描述如下:
当我单击单个复选框时,在刚刚更改了该框的变量中触发 OnPropertyChanged 事件;调用转换器中的 ConvertBack 函数;更新视图模型,一切都很好。
然而,当我单击“全选”复选框时,视图中的单个复选框会更新,但不会在任何变量中调用 OnPropertyChanged,并且不会调用转换器中的 ConvertBack 函数。
还要注意的是,如果我取消选择“全选”,则各个检查框会恢复到之前的状态。
唯一更新视图模型的方法是单击单个复选框。但是,多绑定对于视图的目的是有效的。
我的问题是:
为什么不将复选框的更改传播到视图模型中的源集合?
该转换器:
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));
    }
}

{btsdaf} - Rekshino
@Rekshino,抱歉回复晚了。我还没有想清楚如何取消选择的操作会是怎样的。这可能需要根据我的意图添加逻辑。我故意省略了这个问题的部分,因为我认为取消选择的逻辑是一个单独的问题。 - Shane Sims
如果你一定要使用Multibinding来更新ViewModel,你可以通过一个变通方法实现,但我怀疑它在实践中是否有用。如果需要,我稍后会回复我的答案。 - Rekshino
你需要吗? - Rekshino
@Rekshino 谢谢你的提供,但不用了。我知道几种解决我的问题的方法以及如何实现它们。我在这里发布是为了找出是否可以在真正的 MVVM 中实现此选择所有复选框。对我来说,这意味着视图模型中没有选择所有逻辑,并且理想情况下没有代码后台。 - Shane Sims
在XAML + behaviors中,可以仅在View中完成它(行为可以被视为代码后台,但是它的优点是可以重复使用)。我不同意您的观点,即在ViewModel中实现“全选”逻辑不是“真正的MVVM”。Ginger Ninja所回答的才是真正的MVVM,而且在我看来是首选的方式! - Rekshino
1个回答

5
你的多绑定存在问题,因为它会在两个数据变化时都"触发",但第一个绑定 (Path="Selected") 是将数据绑定到你的 VM 并更新数据的绑定。第二个绑定只会触发全选复选框并更改 IsChecked 属性。仅仅因为你有一个 MultiBinding,并不意味着其他绑定会互相传播它们的更改。
这就是为什么你看到当点击全选时,选择框发生变化但数据没有变化的行为。你没有明确地设置一个机制让全选复选框告诉 ViewModel 更改数据。
通过一些试错,我确定单独使用 MultiBinding 没有清晰易懂的方法来完成这一点(如果有人有做到的方法,我很想学习)。我也尝试了 DataTriggers,但越来越麻烦。我发现最好的方法是将全选逻辑卸载到 ViewModel 并在全选复选框上使用一个 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代码。

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

有趣。我希望避免在视图模型中放置“全选”逻辑。在我看来,这似乎违反了MVVM范例,但是不幸的是。我经常觉得WPF没有包含实现MVVM所需工具的工具。 - Shane Sims
@ShaneSims,这并不真正违反MVVM。您的视图模型存在的目的是处理数据和视图之间的任何额外逻辑。而这就是您正在做的事情。您希望以更复杂的方式更改数据(Selected Property)。它不纯粹基于UI(这将放在XAML或Code behind中),因为您的数据保持该状态。 - Ginger Ninja

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接