2015-10-12 123 views
1

我有一个非常简单的解决方案,基于MVVM填充选项卡。如何设置以下两个命令,然后选择“添加”和“删除”。从我在线阅读的内容看来,我需要设置ICommand或其他东西。在演示中,我不清楚我是否能够正常工作。wpf绑定命令到窗口的快捷方式

  1. 添加命令将调用视图模型类的现有功能。卜它将由密钥命令被称为“按Ctrl + N”

  2. 删除当用户点击该“X”按钮,这将消除特定标签命令将被调用。否则,它可以通过'Ctrl + W'调用,这会关闭当前选中的选项卡。

命令的东西是我的新东西,所以如果有人可以帮助我,它将不胜感激。我希望能够在此基础上继续增加更多的工具。

Link to visual studio dropbox files。你会看到我已经把事情分解到课堂上,并以一种清晰的方式组织它。

enter image description here

低于工具的片段......

视图模型

using System; 
using System.Collections.ObjectModel; 
using System.Windows; 

namespace WpfApplication1 
{ 
    public class ViewModel : ObservableObject 
    { 
     private ObservableCollection<TabItem> tabItems; 

     public ObservableCollection<TabItem> TabItems 
     { 
      get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); } 
     } 

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

     public void AddContentItem() 
     { 
      TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() }); 
     } 
    } 



    public class TabItem 
    { 
     public string Header { get; set; } 
     public string Content { get; set; } 
    } 
} 

主窗口XAML

<Window x:Class="WpfApplication1.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:data="clr-namespace:WpfApplication1" 
     Title="MainWindow" Height="350" Width="250"> 

    <Window.DataContext> 
     <data:ViewModel/> 
    </Window.DataContext> 

    <Grid> 
     <Grid.RowDefinitions> 
      <RowDefinition Height="Auto"/> 
      <RowDefinition Height="*"/> 
     </Grid.RowDefinitions> 

     <!--<Button Content="Add" Command="{Binding AddCommand}" Grid.Row="0"></Button>--> 
     <TabControl 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"/> 
        </StackPanel> 
       </DataTemplate> 
      </TabControl.ItemTemplate> 

      <TabControl.ContentTemplate> 
       <DataTemplate> 
        <TextBlock 
        Text="{Binding Content}" /> 
       </DataTemplate> 
      </TabControl.ContentTemplate> 
     </TabControl> 

    </Grid> 
</Window> 

回答

1

你收到的其他两个答案了。不幸的是,既没有精确解决删除命令。此外,人们更喜欢主要关注代码隐藏实现而不是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); 
    } 
} 

注意两个加嵌套类,AddCommandObjectRemoveCommandObject。这些都是几乎最简单的ICommand实现的例子。它们可以始终执行,因此CanExecute()的返回值永不改变(所以不需要提高CanExecuteChanged事件)。他们确实需要提及您的ViewModel对象,以便他们可以分别调用适当的方法。

还有两个公共属性被添加来允许绑定这些命令。当然,RemoveContentItem()方法需要知道要删除的项目。这需要在XAML中设置,以便该值可以作为参数传递给命令处理程序,并从那里传输到实际的方法RemoveContentItem()

为了支持使用键盘的命令,一种方法是将输入的绑定添加到窗口。这是我在这里选择的。所述RemoveCommand结合另外需要被删除作为命令参数传递的项目,所以这被绑定到CommandParameterKeyBinding对象(正如在项目ButtonCommandParameter)。

产生的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实现。 AddCommandObjectRemoveCommandObject类已从类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中通常状态连接的一致性(即通过绑定)。幸运的是,如果真的需要的话,以一种可绑定的方式实现接口(即如上所述)就足够简单了。

+0

你钉了@Peter Duniho非常感谢你解释解决方案的故障。非常感谢。你是否愿意解释将ICommand分解成一个帮助类有什么好处。你可能可以展示一下如何用这个项目来做这件事吗? – JokerMartini

+0

对显示ICommand助手类感兴趣吗?我很喜欢看到这样的一个例子。 – JokerMartini

+0

@JokerMartini:是的,我很高兴。我目前没有时间。同时,您可能需要查看回答者d.moncada引用的问答:[为什么使用RelayCommand](https://stackoverflow.com/a/22286816)。我没有发现那里的讨论尽可能有帮助,但它有一个完整的代码示例(即'RelayCommand 'class)。你可以从头开始研究,看看它如何适合我替代上面提供的两个特殊用途的'ICommand'实现。 –

0

你可以从删除开始。

首先,您需要创建一个RelayCommand类。有关RelayCommand更多信息,看到这个帖子:Why RelayCommand

public class RelayCommand : ICommand 
{ 
    #region Private members 
    /// <summary> 
    /// Creates a new command that can always execute. 
    /// </summary> 
    private readonly Action execute; 

    /// <summary> 
    /// True if command is executing, false otherwise 
    /// </summary> 
    private readonly Func<bool> canExecute; 
    #endregion 

    /// <summary> 
    /// Initializes a new instance of <see cref="RelayCommand"/> that can always execute. 
    /// </summary> 
    /// <param name="execute">The execution logic.</param> 
    public RelayCommand(Action execute): this(execute, canExecute: null) 
    { 
    } 

    /// <summary> 
    /// Initializes a new instance of <see cref="RelayCommand"/>. 
    /// </summary> 
    /// <param name="execute">The execution logic.</param> 
    /// <param name="canExecute">The execution status logic.</param> 
    public RelayCommand(Action execute, Func<bool> canExecute) 
    { 
     if (execute == null) 
     { 
      throw new ArgumentNullException("execute"); 
     } 
     this.execute = execute; 
     this.canExecute = canExecute; 
    } 

    /// <summary> 
    /// Raised when RaiseCanExecuteChanged is called. 
    /// </summary> 
    public event EventHandler CanExecuteChanged; 

    /// <summary> 
    /// Determines whether this <see cref="RelayCommand"/> can execute in its current state. 
    /// </summary> 
    /// <param name="parameter"> 
    /// Data used by the command. If the command does not require data to be passed, this object can be set to null. 
    /// </param> 
    /// <returns>True if this command can be executed; otherwise, false.</returns> 
    public bool CanExecute(object parameter) 
    { 
     return this.canExecute == null ? true : this.canExecute(); 
    } 

    /// <summary> 
    /// Executes the <see cref="RelayCommand"/> on the current command target. 
    /// </summary> 
    /// <param name="parameter"> 
    /// Data used by the command. If the command does not require data to be passed, this object can be set to null. 
    /// </param> 
    public void Execute(object parameter) 
    { 
     this.execute(); 
    } 

    /// <summary> 
    /// Method used to raise the <see cref="CanExecuteChanged"/> event 
    /// to indicate that the return value of the <see cref="CanExecute"/> 
    /// method has changed. 
    /// </summary> 
    public void RaiseCanExecuteChanged() 
    { 
     var handler = this.CanExecuteChanged; 
     if (handler != null) 
     { 
      handler(this, EventArgs.Empty); 
     } 
    } 
} 

接下来,在你的ViewModelRelayCommand类型添加一个“绑定”删除属性。这就是你的按钮将绑定到什么,以及它将如何通知ViewModel它被按下。 ReplayCommand将有一个执行方法,只要发生按键就会被调用。在这里,我们从整个TabItem列表中删除我们自己。

public class ViewModel : ObservableObject 
{ 
    private ObservableCollection<TabItem> tabItems; 
    private RelayCommand<object> RemoveCommand; 

    public ObservableCollection<TabItem> TabItems 
    { 
     get { return tabItems ?? (tabItems = new ObservableCollection<TabItem>()); } 
    } 

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

     RemoveCommand = new RelayCommand<object>(RemoveItemExecute); 
    } 

    public void AddContentItem() 
    { 
     TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString() }); 
    } 

    private void RemoveItemExecute(object param) 
    { 
     var tabItem = param as TabItem; 
     if (tabItem != null) 
     { 
      TabItems.Remove(tabItem); 
     } 
    } 
} 

现在,更新您的XAML。每个TabItem需要绑定到ViewModelRemoveCommand,并将其作为参数传入。我们可以做到这一点,如:

<!--<Button Content="Add" Command="{Binding AddCommand}" Grid.Row="0"></Button>--> 
<TabControl x:Name="TabItems" ItemsSource="{Binding TabItems}" Grid.Row="1" Background="LightBlue"> 
    <TabControl.ItemTemplate> 
     <DataTemplate> 
      <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> 
       <TextBlock Text="{Binding Header}" VerticalAlignment="Center"/> 
       <Button Command="{Binding ElementName=TabItems, Path=DataContext.RemoveCommand}" 
         CommandParameter="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}" 
         Content="x" 
         Width="20" 
         Height="20" 
         Margin="5 0 0 0"/> 
      </StackPanel> 
     </DataTemplate> 
    </TabControl.ItemTemplate> 

    <TabControl.ContentTemplate> 
     <DataTemplate> 
      <TextBlock 
      Text="{Binding Content}" /> 
     </DataTemplate> 
    </TabControl.ContentTemplate> 
</TabControl> 

0

起初u需要设置你的命令

public static class Commands 
{ 
    private static RoutedUICommand add; 
    private static RoutedUICommand remove; 

    static Commands() 
    { 
     searchValue = new RoutedUICommand("Add", "Add", typeof(Commands)); 
     showCSCode = new RoutedUICommand("Remove", "Remove", typeof(Commands)); 
     add.InputGestures.Add(new KeyGesture(Key.N, ModifierKeys.Control)); 
     remove.InputGestures.Add(new KeyGesture(Key.X)); 
    } 

    public static RoutedUICommand Add { get { return add; } } 
    public static RoutedUICommand Remove { get { return remove; } } 
} 

在窗口中加载事件,你必须绑定命令方法

<window ... Loaded="window_loaded"> 

的CS文件

CommandBindings.Add(new CommandBinding(Commands.Remove, HandleRemoveExecuted, HandleCanRemoveExecuted)); 

是否启用命令:

private void HandleCanAddExecute(object sender, CanExecuteRoutedEventArgs e) 
    { 
     e.CanExecute = true; 
    } 

应该采取什么命令做:

private void HandleAddExecute(object sender, ExecutedRoutedEventArgs e) 
    { 
     AddContentItem(); 
    } 

最后你只需要编辑使用现有的文件

TabItems.Add(new TabItem { Header = "Three", Content = DateTime.Now.ToLongDateString(), 
          CommandBindings.Add(new CommandBinding(Commands.Add, HandleAddExecuted, HandleCanAddExecuted)); }); 

XAML:

<Window ... 
     xmlns:commands="clr-namespace:<NAMESPACE>"> 
<Button Content="x" Width="20" 
     Height="20" Margin="5 0 0 0" 
     Command="{x:Static commands:Commands.Remove}"/>