wpf绑定命令到窗口的快捷方式

时间:2015-10-12 17:47:55

标签: c# wpf mvvm

我在下面有一个非常简单的解决方案,用于基于MVVM填充标签。如何设置以下两个命令,以及“添加”和“删除”。从我在网上看到的内容看来,我需要设置ICommand或其他类似的东西。对我来说,让我开始工作对我来说还不够清楚。

  1. 添加命令将调用ViewModel类中已存在的函数。它会被一个键命令'Ctrl + N'

  2. 调用
  3. 当用户点击“X”按钮时,将调用删除命令,这将删除该特定标签。否则可以通过“Ctrl + W”调用它,这将关闭当前选择的任何一个选项卡。

  4. 命令对我来说是新的,所以如果有人可以帮助我,我将不胜感激。我希望对此进行扩展,并继续为该工具添加更多内容。

    链接到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; }
        }
    }
    

    MainWindow 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>
    

3 个答案:

答案 0 :(得分:1)

您已经收到了另外两个答案。不幸的是,它们都没有精确地解决添加删除命令。此外,人们更倾向于主要关注代码隐藏实现而不是XAML声明,并且在细节方面相当稀疏,而另一个更正确地关注XAML中的实现(如果适用),但不包括正确的工作代码,以及(稍微)通过引入RelayCommand类型的额外抽象来混淆答案。

所以,我会提出自己对这个问题的看法,希望这对你更有用。


虽然我同意将ICommand实现抽象为辅助类(例如RelayCommand)是有用的,甚至是可取的,但不幸的是,这往往隐藏了正在发生的事情的基本机制,并且需要更多在另一个答案中提供的精心实施。所以现在,让我们忽略它。

相反,只关注需要实现的内容:ICommand接口的两种不同实现。您的视图模型将这些公开为两个可绑定属性的值,表示要执行的命令。

以下是您的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绑定还需要删除要作为命令参数传递的项,因此它绑定到CommandParameter对象的KeyBinding(就像CommandParameter一样项目中的Button。)

生成的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或不处理参数,等等。

另一方面,每次写这样的课程,都有机会写一个新的bug。更糟糕的是,如果你引入了一个后来被复制/粘贴的bug,那么当你最终发现bug时,你可能会或者可能不会在它存在的任何地方修复它。

当然,一遍又一遍地编写这些课程会变得乏味且耗时。

同样,这些只是&#34;最佳实践&#34;的一般传统智慧的具体例子。抽象可重用逻辑。

所以,如果我们已经接受抽象在这里很有用(我当然有:)),那么问题就变成了,抽象是什么样的?有许多不同的方法可以解决这个问题。引用的答案就是一个例子。这是我写的一种略有不同的方法:

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类中删除RemoveCommandObjectViewModel类。
  • 代替他们使用DelegateCommand<T>类。
  • 请注意,在某些情况下,命令处理程序不需要传递给ICommand.Execute(object)方法的参数。在上面,通过接受lambda(匿名)委托中的参数,然后在调用无参数处理程序方法时忽略它来解决这个问题。处理此问题的其他方法是让处理程序方法接受参数,然后忽略它,或者使用非泛型类,其中处理程序委托本身可以是无参数的。恕我直言,没有&#34;正确的方式&#34;本身...只是根据个人喜好可能被认为或多或少的各种选择。
  • 另请注意,此实现与引用的答案在处理CanExecuteChanged事件时的实现不同。在我的实现中,客户端代码被赋予对该事件的细粒度控制,代价是要求客户端代码保留对所讨论的DelegateCommand<T>对象的引用并在其中调用其RaiseCanExecuteChanged()方法。适当的时候。在另一个实现中,它依赖于CommandManager.RequerySuggested事件。这是方便性与效率之间的权衡,在某些情况下是正确性。也就是说,客户端代码必须保留对可能改变可执行状态的命令的引用不太方便,但是如果一个人走另一条路径,至少可以更频繁地引发CanExecuteChanged事件。超过要求,在某些情况下,它甚至可能应该被提出(这比可能的低效率更糟糕)。

在最后一点,另一种方法是使ICommand实现成为依赖项对象,并提供用于控制命令可执行状态的依赖项属性。这要复杂得多,但整体可以被认为是优秀的解决方案,因为它允许对CanExecuteChanged事件的提升进行细粒度控制,同时提供一种良好的,惯用的方式来绑定可执行状态命令,例如在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事件。恕我直言,遗憾的是ICommand界面之间的差异在于它的设计方式和WPF的正常工作方式(即具有可绑定属性)。我们基本上有一个属性但通过一个名为CanExecute()的显式getter方法公开,这有点奇怪。

另一方面,这种差异确实有一些有用的目的,包括明确和方便地使用CommandParameter来执行命令和检查可执行性。这些都是值得的目标。我个人不确定我是否做出了相同的选择,以便在WPF中连接状态的一致方式(即通过绑定)保持平衡。幸运的是,如果真的需要,可以以可绑定的方式(如上所述)实现ICommand接口,这很简单。

答案 1 :(得分: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);
        }
    }
}

接下来,在ViewModel类型RelayCommand中添加“可绑定”删除属性。这是您的TabItem按钮将绑定到的内容,以及它将如何通知它被按下的ViewModelReplayCommand将有一个执行方法,只要按下按钮就会调用该方法。在这里,我们从整个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都需要绑定到父RemoveCommand中的ViewModel,并作为参数传递给自己。我们可以这样做:

                           

<!--<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>

答案 2 :(得分:0)

首先,您需要设置命令

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}"/>