你收到的其他两个答案了。不幸的是,既没有精确解决加和删除命令。此外,人们更喜欢主要关注代码隐藏实现而不是XAML声明,而且对细节来说相当稀少,而另一方更正确地集中于适当的XAML实现,但不包括正确的工作代码和(稍微)通过引入RelayCommand
类型的额外抽象来混淆答案。
所以,我会提出自己的看法,希望这对你更有用。
虽然我同意抽象的ICommand
落实到辅助类如RelayCommand
是有用的,甚至是可取的,不幸的是这往往隐藏了什么事情的基本机制,并要求在提供了更详细的实施另一个答案。所以现在,让我们忽略它。
相反,只关注需要实现的内容:接口的两种不同实现。您的视图模型会将这些视图显示为代表要执行的命令的两个可绑定属性的值。
这是你ViewModel
类的新版本(与不相关的未拨备ObservableObject
类型删除):
class ViewModel
{
private class AddCommandObject : ICommand
{
private readonly ViewModel _target;
public AddCommandObject(ViewModel target)
{
_target = target;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_target.AddContentItem();
}
}
private class RemoveCommandObject : ICommand
{
private readonly ViewModel _target;
public RemoveCommandObject(ViewModel target)
{
_target = target;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_target.RemoveContentItem((TabItem)parameter);
}
}
private ObservableCollection<TabItem> tabItems;
public ObservableCollection<TabItem> TabItems
{
get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
}
public ICommand AddCommand { get { return _addCommand; } }
public ICommand RemoveCommand { get { return _removeCommand; } }
private readonly ICommand _addCommand;
private readonly ICommand _removeCommand;
public ViewModel()
{
TabItems.Add(new TabItem { Header = "One", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Two", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
_addCommand = new AddCommandObject(this);
_removeCommand = new RemoveCommandObject(this);
}
public void AddContentItem()
{
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
}
public void RemoveContentItem(TabItem item)
{
TabItems.Remove(item);
}
}
注意两个加嵌套类,AddCommandObject
和RemoveCommandObject
。这些都是几乎最简单的ICommand
实现的例子。它们可以始终执行,因此CanExecute()
的返回值永不改变(所以不需要提高CanExecuteChanged
事件)。他们确实需要提及您的ViewModel
对象,以便他们可以分别调用适当的方法。
还有两个公共属性被添加来允许绑定这些命令。当然,RemoveContentItem()
方法需要知道要删除的项目。这需要在XAML中设置,以便该值可以作为参数传递给命令处理程序,并从那里传输到实际的方法RemoveContentItem()
。
为了支持使用键盘的命令,一种方法是将输入的绑定添加到窗口。这是我在这里选择的。所述RemoveCommand
结合另外需要被删除作为命令参数传递的项目,所以这被绑定到CommandParameter
为KeyBinding
对象(正如在项目Button
的CommandParameter
)。
产生的XAML看起来是这样的:
<Window.DataContext>
<data:ViewModel/>
</Window.DataContext>
<Window.InputBindings>
<KeyBinding Command="{Binding AddCommand}">
<KeyBinding.Gesture>
<KeyGesture>Ctrl+N</KeyGesture>
</KeyBinding.Gesture>
</KeyBinding>
<KeyBinding Command="{Binding RemoveCommand}"
CommandParameter="{Binding SelectedItem, ElementName=tabControl1}">
<KeyBinding.Gesture>
<KeyGesture>Ctrl+W</KeyGesture>
</KeyBinding.Gesture>
</KeyBinding>
</Window.InputBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TabControl x:Name="tabControl1" ItemsSource="{Binding TabItems}" Grid.Row="1" Background="LightBlue">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding Header}" VerticalAlignment="Center"/>
<Button Content="x" Width="20" Height="20" Margin="5 0 0 0"
Command="{Binding DataContext.RemoveCommand, RelativeSource={RelativeSource AncestorType=TabControl}}"
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}">
</Button>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
编辑:
正如我上面提到的,其实也有利于抽象的ICommand
实施,使用辅助类而不是为每个要执行的命令声明一个新类。在Why RelayCommand提到的答案提到松耦合和单元测试是动机。虽然我同意这些是很好的目标,但我不能说这些目标实际上是由ICommand
实施的抽象本身服务的。
相反,我看到了好处,为使这种抽象的时候,主要发现了同样的:它允许代码重用,并在这样做提高了开发人员的生产率,且代码的可维护性和质量一起。
在我上面的例子中,你想要一个新的命令每一次,你必须写一个实现ICommand
一个新的类。一方面,这意味着你写的每一堂课都可以为特定目的量身定做。根据具体情况处理CanExecuteChanged
(视情况需要),是否传递参数等。
另一方面,每当您编写这样的类时,就有机会编写一个新的错误。更糟的是,如果你引入了一个后来被复制/粘贴的错误,那么当你最终发现错误时,你可能会或可能不会在它存在的任何地方修复它。
当然,反复编写这样的类会变得繁琐和耗时。
再次,这些可重复使用的抽象逻辑的“最佳实践”的一般传统智慧的只是具体的例子。
所以,如果我们接受了一个抽象这里很有用(我当然有:)),接下来的问题是,是什么抽象的样子?有很多不同的方法来处理这个问题。引用的答案就是一个例子。这里有一个稍微不同的方法,我已经写了:
class DelegateCommand<T> : ICommand
{
private readonly Func<T, bool> _canExecuteHandler;
private readonly Action<T> _executeHandler;
public DelegateCommand(Action<T> executeHandler)
: this(executeHandler, null) { }
public DelegateCommand(Action<T> executeHandler, Func<T, bool> canExecuteHandler)
{
_canExecuteHandler = canExecuteHandler;
_executeHandler = executeHandler;
}
public bool CanExecute(object parameter)
{
return _canExecuteHandler != null ? _canExecuteHandler((T)parameter) : true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_executeHandler((T)parameter);
}
public void RaiseCanExecuteChanged()
{
EventHandler handler = CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
在ViewModel
类,上面会像这样使用:
class ViewModel
{
private ObservableCollection<TabItem> tabItems;
public ObservableCollection<TabItem> TabItems
{
get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); }
}
public ICommand AddCommand { get { return _addCommand; } }
public ICommand RemoveCommand { get { return _removeCommand; } }
private readonly ICommand _addCommand;
private readonly ICommand _removeCommand;
public ViewModel()
{
TabItems.Add(new TabItem { Header = "One", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Two", Content = DateTime.Now.ToLongDateString() });
TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() });
// Use a lambda delegate to map the required Action<T> delegate
// to the parameterless method call for AddContentItem()
_addCommand = new DelegateCommand<object>(o => this.AddContentItem());
// In this case, the target method takes a parameter, so we can just
// use the method directly.
_removeCommand = new DelegateCommand<TabItem>(RemoveContentItem);
}
注:
- 当然,现在不再需要特定的
ICommand
实现。 AddCommandObject
和RemoveCommandObject
类已从类ViewModel
中删除。
- 代替使用
DelegateCommand<T>
类。
- 请注意,在某些情况下,命令处理程序不需要传递给
ICommand.Execute(object)
方法的参数。在上面,这是通过接受lambda(anonymous)委托中的参数,然后在调用无参数处理方法时忽略它来解决的。处理这个问题的其他方法是让处理程序方法接受参数,然后忽略它,或者有一个非泛型类,其中处理程序委托本身可以是无参数的。恕我直言,本身并没有“正确的方式”,根据个人喜好,可能会考虑更多或更少的各种选择。
- 还请注意,此实现与处理
CanExecuteChanged
事件时参考答案的实现不同。在我的实现中,客户代码对该事件进行了细粒度的控制,代价是客户代码保留对该问题的DelegateCommand<T>
对象的引用,并在适当的时候调用其RaiseCanExecuteChanged()
方法。在其他实现中,它取决于CommandManager.RequerySuggested
事件。这是一种方便性与效率之间的平衡,并且在某些情况下是正确的。也就是说,客户端代码不得不保留对可能改变可执行状态的命令的引用,但是如果其他路由转移到另一个路由上,至少可能导致比事件更多的事件,在某些情况下,甚至有可能它应该被提高(这比可能的低效率差得多),可能会导致而不是。
在最后一点上,另一种方法是将ICommand
实现作为依赖对象,并提供用于控制命令的可执行状态的依赖项属性。这要复杂得多,但总体上可以认为是优越的解决方案,因为它允许对事件的提升进行细粒度的控制,同时提供了绑定命令的可执行状态的良好惯用方式,例如,在XAML中对任何属性或属性实际确定所述可执行性。
这样的实现可能是这个样子:
class DelegateDependencyCommand<T> : DependencyObject, ICommand
{
public static readonly DependencyProperty IsExecutableProperty = DependencyProperty.Register(
"IsExecutable", typeof(bool), typeof(DelegateCommand<T>), new PropertyMetadata(true, OnIsExecutableChanged));
public bool IsExecutable
{
get { return (bool)GetValue(IsExecutableProperty); }
set { SetValue(IsExecutableProperty, value); }
}
private static void OnIsExecutableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DelegateDependencyCommand<T> command = (DelegateDependencyCommand<T>)d;
EventHandler handler = command.CanExecuteChanged;
if (handler != null)
{
handler(command, EventArgs.Empty);
}
}
private readonly Action<T> _executeHandler;
public DelegateDependencyCommand(Action<T> executeHandler)
{
_executeHandler = executeHandler;
}
public bool CanExecute(object parameter)
{
return IsExecutable;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_executeHandler((T)parameter);
}
}
在上面,在canExecuteHandler
参数的类被消除,代替IsExecutable
财产。当该属性更改时,将引发CanExecuteChanged
事件。恕我直言,不幸的是,在它的设计方式和WPF如何正常工作(即具有可绑定属性)之间的界面上存在这种差异。有点奇怪,我们基本上有一个属性,但通过名为CanExecute()
的显式获取方法公开。另一方面,这种差异具有一些有用的用途,包括明确和方便地使用CommandParameter
来执行命令和检查可执行性。这些都是有价值的目标。我只是不确定我是否做出了同样的选择来平衡它们与WPF中通常状态连接的一致性(即通过绑定)。幸运的是,如果真的需要的话,以一种可绑定的方式实现接口(即如上所述)就足够简单了。
你钉了@Peter Duniho非常感谢你解释解决方案的故障。非常感谢。你是否愿意解释将ICommand分解成一个帮助类有什么好处。你可能可以展示一下如何用这个项目来做这件事吗? – JokerMartini
对显示ICommand助手类感兴趣吗?我很喜欢看到这样的一个例子。 – JokerMartini
@JokerMartini:是的,我很高兴。我目前没有时间。同时,您可能需要查看回答者d.moncada引用的问答:[为什么使用RelayCommand](https://stackoverflow.com/a/22286816)。我没有发现那里的讨论尽可能有帮助,但它有一个完整的代码示例(即'RelayCommand'class)。你可以从头开始研究,看看它如何适合我替代上面提供的两个特殊用途的'ICommand'实现。 –