Reactive UI 替代 CanExecute 参数 (WPF)

Reactive UI alternative to Parameter for CanExecute (WPF)

假设我有两个 VM classes ParentVM 和 ChildVM。 ParentVM 有 ObservableCollection<ChildVM> Children。另外 ParentVM 有两个命令 MoveUpCommandMoveDownCommandMoveUp(ChildVM child){..}MoveDown(ChildVM child){...}

实现

在我看来,我有一个 ListBox,它 ItemSource 绑定到 Children 集合。 ItemTemplate 包含 TextBlock 和每个 child 的两个按钮(上移和下移)。我将此按钮的命令绑定到 Parent 命令并使用 ListBoxItemDataContextChildVM 作为 CommandParameter.

到目前为止效果很好)

当我想设置正确的 CanExecute 方法时出现问题。因为当 Child 无法向上移动(即已经在顶部)时,我不想让 Button 处于活动状态。有一个问题,当我编写我的命令的实现时,我将 ChildVM 作为参数,这样我就可以检查它是在顶部还是底部,并简单地忽略 MoveUp 或 MoveDown。但是 CanExecute.

就没有这样的东西了

我读了一些问题,有人建议将参数绑定到某些 属性。但是怎么办?我想我可以在 ChildVM 中创建 int CollectionIndex 属性(我将从 Parent 更新)并将命令传输到 ChildVM,但它看起来有点像我不应该做的。

这里有什么通用的解决方案吗?

更新 1 一些演示代码。此代码经过简化以使其更短。

public class ParentVM
{
    public ObservableCollection<ChildVM> Children { get; }

    public ICommand MoveUpCommand { get; }

    public ParentVM()
    {
        Children = new ObservableCollection<ChildVM>();
        Children.Add(new ChildVM { Name = "Child1" });
        Children.Add(new ChildVM { Name = "Child2" });
        Children.Add(new ChildVM { Name = "Child3" });

        MoveUpCommand = ReactiveCommand.Create<ChildVM>(MoveUp);
    }

    public void MoveUp(ChildVM child)
    {
        var index = Children.IndexOf(child);
        if (index > 0) Children.Move(index, index - 1);
    }
}


public class ChildVM
{
    public string Name { get; set; }
}

ParentView.xaml

<UserControl ...>
    <Grid>
        <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Children}" CanUserAddRows="False" CanUserDeleteRows="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" Width="*"/>
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Button Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=DataContext.MoveUpCommand}"
                                    CommandParameter="{Binding}">Move Up</Button>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid> 
    </Grid>
</UserControl>

ParentView.xaml.cs public 部分 class Parent 视图:用户控件 { public Parent视图() { 初始化组件(); this.DataContext = 新 ParentVM(); } }

我这里再写一遍。 'Move Up' 按钮出现 FOR EACH child。在这种情况下 SelectedItem 不能使用,因为我可以只按下按钮而不实际选择项目

下面有两个答案。第一个使用标准的 MVVM 模式并且非常简单。但是,它根本不使用 Reactive UI。第二个确实使用了来自 Reactive UI 的 ReactiveCommand,但坦率地说它很混乱。我不是 Reactive UI 方面的专家,但我认为在这种情况下很难使用它写出像样的答案。也许 Reactive UI 专家会看到并纠正我。

对于第一个解决方案,我只是从 Josh Smith's orginal MVVM paper 中获取 RelayCommand class。然后我们可以稍微重写你的 class 并且一切正常。 XAML 与问题中的完全一样。你问的 'common solution' 是什么,我想就是这个了。

public class ParentVM
{
    public ObservableCollection<ChildVM> Children { get; }

    public ICommand MoveUpCommand { get; }

    public ParentVM()
    {
        Children = new ObservableCollection<ChildVM>();
        Children.Add(new ChildVM { Name = "Child1" });
        Children.Add(new ChildVM { Name = "Child2" });
        Children.Add(new ChildVM { Name = "Child3" });

        MoveUpCommand = new RelayCommand(o => MoveUp((ChildVM)o), o => CanExecuteMoveUp((ChildVM)o));
    }

    public void MoveUp(ChildVM child)
    {
        var index = Children.IndexOf(child);
        if (index > 0) Children.Move(index, index - 1);
    }

    public bool CanExecuteMoveUp(ChildVM child)
    {
        return Children.IndexOf(child) > 0;
    }
}

public class ChildVM
{
    public string Name { get; set; }
}

public class RelayCommand : ICommand
{
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;
    public RelayCommand(Action<object> execute) : this(execute, null) { }
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");
        _execute = execute; _canExecute = canExecute;
    }
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
    public void Execute(object parameter) { _execute(parameter); }
}

当然上面的解决方案使用了一个额外的class,RelayCommand,并没有使用标准的ReactiveCommand。 Reactive UI docs 说 'Parameters, unlike in other frameworks, are typically not used in the canExecute conditions, instead, binding View properties to ViewModel properties and then using the WhenAnyValue() is far more common.'。然而,在这种情况下,这非常棘手。

下面的第二个解决方案使用 ReactiveCommand。它将命令绑定到 child 虚拟机,在执行命令时调用 parent 虚拟机。 child 个 VM 知道它们在列表中的索引位置,因此我们可以基于此执行 canExecute。如您所见,parents 和 children 最终变成了 tightly-coupled,这并不漂亮。

public class ParentVM
{
    public ObservableCollection<ChildVM> Children { get; }

    public ParentVM()
    {
        Children = new ObservableCollection<ChildVM>();
        Children.Add(new ChildVM { Name = "Child1", Parent=this, Index = 0 });
        Children.Add(new ChildVM { Name = "Child2", Parent = this, Index = 1 });
        Children.Add(new ChildVM { Name = "Child3", Parent = this, Index = 2 });
    }

    public void MoveUp(ChildVM child)
    {
        var index = Children.IndexOf(child);
        if (index > 0) 
        { 
            Children.Move(index, index - 1);
            Children[index].Index = index;
            Children[index - 1].Index = index - 1;
        }
    }
}

public class ChildVM: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string Name { get; set; }
    public ParentVM Parent { get; set; }
    public ICommand MoveUpCommand { get; }

    private int _index;
    public int Index
    {
        get { return _index; }
        set { _index = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Index))); }
    }

    public ChildVM()
    {
        IObservable<bool> canExecute = this.WhenAnyValue(x => x.Index, index => index > 0);
        MoveUpCommand = ReactiveCommand.Create(() => Parent.MoveUp(this), canExecute);
    }
}

XAML:

<UserControl>
    <Grid>
        <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Children}" CanUserAddRows="False" CanUserDeleteRows="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" Width="*"/>
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Button Command="{Binding MoveUpCommand}">Move Up</Button>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</UserControl>