Reputation: 338
Let's say I have two VM classes ParentVM and ChildVM. ParentVM has ObservableCollection<ChildVM> Children
. Also ParentVM has two commands MoveUpCommand
and MoveDownCommand
which implemented by MoveUp(ChildVM child){..}
and MoveDown(ChildVM child){...}
In my view I have a ListBox
which ItemSource
binds to Children
collection. ItemTemplate
contains TextBlock and two buttons (Move Up and Move Down) for each child. I bind commands of this buttons to Parent commands and use ListBoxItem
's DataContext
i.e. ChildVM
as CommandParameter
.
So far so good it works)
Problem is there when I want to set proper CanExecute
method. Because I don't want to Button be active when Child cannot be moved up (i.e. already on top). And there is a catch, when I write implementation of my command I have my ChildVM
as parameter so I can check whether it on top or at the bottom and simply ignore MoveUp or MoveDown. But there is nothing like that for CanExecute
.
I read some question, someone advice to bind parameter to some property. But how? I think I can create int CollectionIndex
property (which I will update from Parent) in ChildVM and transfer commands to ChildVM, but it looks like something that I should not do.
Any common solution here?
UPDATE 1 Some code for demonstation. This code is simplified to make it shorter.
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 partial class ParentView : UserControl { public ParentView() { InitializeComponent(); this.DataContext = new ParentVM(); } }
I write it again here. 'Move Up' button appears FOR EACH child. In this case SelectedItem cannot be used because I can just press button without actually selecting item
Upvotes: 1
Views: 579
Reputation: 9475
There are two answers below. The first uses standard MVVM patterns and is very simple. However, it doesn't use Reactive UI at all. The second does use ReactiveCommand from Reactive UI, but frankly it's messy. I'm not an expert on Reactive UI, but I think this is one case where it's quite hard to write a decent answer using it. Maybe a Reactive UI expert will see this and correct me.
For the first solution I just took the RelayCommand class from Josh Smith's orginal MVVM paper. We can then rewrite your class very slightly and everything works. The XAML is exactly as in the question. You asked for what the 'common solution' would be and I expect this is it.
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); }
}
Of course the solution above uses an extra class, RelayCommand, and doesn't use the standard ReactiveCommand. The Reactive UI docs say '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.'. However this is quite tricky to do in this case.
The second solution below uses ReactiveCommand. It binds the commands into the child VMs, which call the parent VM when the command is executed. The child VMs know their index position in the list, so we can base canExecute on this. As you can see the parents and children end up tightly-coupled and this isn't pretty.
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>
Upvotes: 1