如何在没有代码隐藏的情况下处理ViewModel中的WPF路由命令?

时间:2016-03-23 16:16:20

标签: c# wpf mvvm command routed-commands

根据我对MVVM的理解,直接在ViewModel中处理路由命令是一种很好的做法。

当路由命令在ViewModel中定义为RelayCommand(或DelegateCommand)时,很容易直接绑定到命令,如下所示:Command = {Binding MyViewModelDefinedCommand}。

实际上,对于在我的ViewModel之外定义的路由命令,我在View后面的代码中处理这些命令并将调用转发到ViewModel。但我发现我必须这样做很尴尬。它违背了推荐的MVVM良好做法。我认为应该有一种更优雅的方式来实现这项工作。

我如何处理" System.Windows.Input.ApplicationCommands"或者直接在ViewModel中在Viewmodel外部定义的任何路由命令。 换句话说,对于在ViewModel之外定义的命令,我如何处理CommandBinding回调" CommandExecute"和/或" CommandCanExecute"直接到ViewModel? 这有可能吗?如果有,怎么样?如果不是,为什么?

3 个答案:

答案 0 :(得分:23)

我将这个问题改为:

  

如何在没有代码隐藏的情况下处理ViewModel中的WPF路由命令?

我会回答:很好的问题!

WPF没有提供内置的方法来执行此操作,当您首次启动WPF并且每个人都告诉您" Code-Behind是邪恶的"时,这尤其令人讨厌。 (确实如此)。所以你必须自己构建它。

自己建造

那么,如何自己创建这样的功能呢?好吧,首先我们需要等效的CommandBinding

/// <summary>
///  Allows associated a routed command with a non-routed command.  Used by
///  <see cref="RoutedCommandHandlers"/>.
/// </summary>
public class RoutedCommandHandler : Freezable
{
  public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
    "Command",
    typeof(ICommand),
    typeof(RoutedCommandHandler),
    new PropertyMetadata(default(ICommand)));

  /// <summary> The command that should be executed when the RoutedCommand fires. </summary>
  public ICommand Command
  {
    get { return (ICommand)GetValue(CommandProperty); }
    set { SetValue(CommandProperty, value); }
  }

  /// <summary> The command that triggers <see cref="ICommand"/>. </summary>
  public ICommand RoutedCommand { get; set; }

  /// <inheritdoc />
  protected override Freezable CreateInstanceCore()
  {
    return new RoutedCommandHandler();
  }

  /// <summary>
  ///  Register this handler to respond to the registered RoutedCommand for the
  ///  given element.
  /// </summary>
  /// <param name="owner"> The element for which we should register the command
  ///  binding for the current routed command. </param>
  internal void Register(FrameworkElement owner)
  {
    var binding = new CommandBinding(RoutedCommand, HandleExecuted, HandleCanExecute);
    owner.CommandBindings.Add(binding);
  }

  /// <summary> Proxy to the current Command.CanExecute(object). </summary>
  private void HandleCanExecute(object sender, CanExecuteRoutedEventArgs e)
  {
    e.CanExecute = Command?.CanExecute(e.Parameter) != null;
    e.Handled = true;
  }

  /// <summary> Proxy to the current Command.Execute(object). </summary>
  private void HandleExecuted(object sender, ExecutedRoutedEventArgs e)
  {
    Command?.Execute(e.Parameter);
    e.Handled = true;
  }
}

然后我们需要一个实际将RoutedCommandHandler与特定元素相关联的类。为此,我们将RoutedCommandHandler s的集合作为附加属性,如下所示:

/// <summary>
///  Holds a collection of <see cref="RoutedCommandHandler"/> that should be
///  turned into CommandBindings.
/// </summary>
public class RoutedCommandHandlers : FreezableCollection<RoutedCommandHandler>
{
  /// <summary>
  ///  Hide this from WPF so that it's forced to go through
  ///  <see cref="GetCommands"/> and we can auto-create the collection
  ///  if it doesn't already exist.  This isn't strictly necessary but it makes
  ///  the XAML much nicer.
  /// </summary>
  private static readonly DependencyProperty CommandsProperty = DependencyProperty.RegisterAttached(
    "CommandsPrivate",
    typeof(RoutedCommandHandlers),
    typeof(RoutedCommandHandlers),
    new PropertyMetadata(default(RoutedCommandHandlers)));

  /// <summary>
  ///  Gets the collection of RoutedCommandHandler for a given element, creating
  ///  it if it doesn't already exist.
  /// </summary>
  public static RoutedCommandHandlers GetCommands(FrameworkElement element)
  {
    RoutedCommandHandlers handlers = (RoutedCommandHandlers)element.GetValue(CommandsProperty);
    if (handlers == null)
    {
      handlers = new RoutedCommandHandlers(element);
      element.SetValue(CommandsProperty, handlers);
    }

    return handlers;
  }

  private readonly FrameworkElement _owner;

  /// <summary> Each collection is tied to a specific element. </summary>
  /// <param name="owner"> The element for which this collection is created. </param>
  public RoutedCommandHandlers(FrameworkElement owner)
  {
    _owner = owner;

    // because we auto-create the collection, we don't know when items will be
    // added.  So, we observe ourself for changes manually. 
    var self = (INotifyCollectionChanged)this;
    self.CollectionChanged += (sender, args) =>
                              {
                                // note this does not handle deletions, that's left as an exercise for the
                                // reader, but most of the time, that's not needed! 
                                ((RoutedCommandHandlers)sender).HandleAdditions(args.NewItems);
                              };
  }

  /// <summary> Invoked when new items are added to the collection. </summary>
  /// <param name="newItems"> The new items that were added. </param>
  private void HandleAdditions(IList newItems)
  {
    if (newItems == null)
      return;

    foreach (RoutedCommandHandler routedHandler in newItems)
    {
      routedHandler.Register(_owner);
    }
  }

  /// <inheritdoc />
  protected override Freezable CreateInstanceCore()
  {
    return new RoutedCommandHandlers(_owner);
  }
}

然后,它就像在元素上使用类一样简单:

<local:RoutedCommandHandlers.Commands>
  <local:RoutedCommandHandler RoutedCommand="Help" Command="{Binding TheCommand}" />
</local:RoutedCommandHandlers.Commands>

Interaction.Behavior实现

了解上述内容后,您可能会问:

  哇,这很棒,但那是很多代码。我已经使用了表达行为,所以有没有办法简化这一点?

我会回答:很好的问题!

如果您已经在使用Interaction.Behaviors,那么您可以使用以下实现:

/// <summary>
///  Allows associated a routed command with a non-ordinary command. 
/// </summary>
public class RoutedCommandBinding : Behavior<FrameworkElement>
{
  public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
    "Command",
    typeof(ICommand),
    typeof(RoutedCommandBinding),
    new PropertyMetadata(default(ICommand)));

  /// <summary> The command that should be executed when the RoutedCommand fires. </summary>
  public ICommand Command
  {
    get { return (ICommand)GetValue(CommandProperty); }
    set { SetValue(CommandProperty, value); }
  }

  /// <summary> The command that triggers <see cref="ICommand"/>. </summary>
  public ICommand RoutedCommand { get; set; }

  protected override void OnAttached()
  {
    base.OnAttached();

    var binding = new CommandBinding(RoutedCommand, HandleExecuted, HandleCanExecute);
    AssociatedObject.CommandBindings.Add(binding);
  }

  /// <summary> Proxy to the current Command.CanExecute(object). </summary>
  private void HandleCanExecute(object sender, CanExecuteRoutedEventArgs e)
  {
    e.CanExecute = Command?.CanExecute(e.Parameter) != null;
    e.Handled = true;
  }

  /// <summary> Proxy to the current Command.Execute(object). </summary>
  private void HandleExecuted(object sender, ExecutedRoutedEventArgs e)
  {
    Command?.Execute(e.Parameter);
    e.Handled = true;
  }
}

使用相应的XAML:

<i:Interaction.Behaviors>
  <local:RoutedCommandBinding RoutedCommand="Help" Command="{Binding TheCommand}" />
</i:Interaction.Behaviors>

答案 1 :(得分:1)

这里有一个将命令绑定到按钮的简单示例:

MainWindow.xaml

<Window x:Class="csWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow">
    <Canvas>
        <Button Name="btnCommandBounded" Command="{Binding cmdExecuteSubmit}" Height="29" Width="68" Content="Submit"></Button>
    </Canvas>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel();
        }
    }

MainWindowViewModel.cs

class MainWindowViewModel
    {
        public ICommand cmdExecuteSubmit { get; set; }
        public MainWindowViewModel()
        {
            cmdExecuteSubmit = new RelayCommand(doSubmitStuff);
        }
        public void doSubmitStuff(object sender)
        {
            //Do your action here
        }
   }

答案 2 :(得分:0)

接受的答案非常好,但似乎OP并不完全理解RoutedCommands是如何工作的,这引起了一些混乱。引用问题:

  

当路由命令在ViewModel中定义为RelayCommand(或   DelegateCommand),很容易直接绑定到命令之类的   this:Command = {Binding MyViewModelDefinedCommand}。

这是不明确的,但无论哪种方式都不正确:

  1. 任何一个 - 无法将RoutedCommand定义为Relay / DelegateCommand,因为RoutedCommand是ICommand接口的不同实现。
  2. 或者 - 如果VM公开实际的RoutedCommand,那么仍然会遇到与在VM外部定义的RoutedCommands相同的问题(因为RoutedCommands的工作方式)。
  3. RoutedCommand是ICommand

    的特定实现

    RoutedCommand的Execute / CanExecute方法不包含我们的应用程序逻辑(当您实例化RoutedCommand时,不传递Execute / CanExecute委托)。他们引发路由事件,与其他路由事件一样,遍历元素树。这些事件(PreviewCanExecute,CanExecute,PreviewExecuted,Executed)正在寻找具有该RoutedCommand的CommandBinding 的元素。 CommandBinding对象具有这些事件的事件处理程序,这就是我们的应用程序逻辑所在的位置(现在很清楚为什么从VM中暴露RoutedCommand无法解决问题)。

    // The command could be declared as a resource in xaml, or it could be one 
    // of predefined ApplicationCommands
    public static class MyCommands {
        public static readonly RoutedCommand FooTheBar = new RoutedCommand();
    }
    

    XAML:

    <Window x:Class...
            xmlns:cmd="clr-namespace:MyCommands.Namespace">
        <Window.CommandBindings>
            <CommandBinding Command="{x:Static cmd:MyCommands.FooTheBar}"
                            Executed="BarFooing_Executed"/>
        </Window.CommandBindings>
    
    <Grid>
    ...
    // When command is executed, event goes up the element tree, and when
    // it finds CommandBinding on the Window, attached handler is executed
    <Button Command="{x:Static cmd:MyCommands.FooTheBar}"
            Content="MyButton"/>
    ...
    </Grid>
    </Window>
    

    CommandBinding对象

    CommandBinding类不从DependencyObject继承(它的Command属性不能绑定到VM上公开的命令)。您可以使用附加到CommandBinding的事件处理程序将调用(在代码隐藏中)转发到VM - 没有什么重要的,没有逻辑(没有测试)。如果你不需要代码隐藏,那么接受的答案有很好的解决方案(为你转发)。