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

Posted

技术标签:

【中文标题】如何在没有代码隐藏的情况下处理 ViewModel 中的 WPF 路由命令?【英文标题】:How can I handle WPF routed commands in my ViewModel without code-behind? 【发布时间】:2016-07-11 01:12:54 【问题描述】:

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

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

实际上,对于在我的 ViewModel 之外定义的路由命令,我在 View 的 Code behind 中处理这些命令并将调用转发到 ViewModel。但我觉得我不得不这样做很尴尬。它违背了推荐的 MVVM 良好实践。我认为应该有更优雅的方式来完成这项工作。

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

【问题讨论】:

如果没有一个好的minimal reproducible example 准确地显示您尝试过的内容、命令的实现以及该实现与您的各种对象(视图、模型等)的关系,就不可能提供一个你的问题的具体答案。很可能,您只想在您的一个 UI 对象(例如窗口本身)的 CommandBindings 集合中声明 CommandBinding 元素。但是有很多可能性。 与此同时,Stack Overflow 上有大量关于该主题的现有信息。参见例如***.com/questions/1281178/…、***.com/questions/601393/…,尤其是这个具体的答案:***.com/a/9239(恕我直言,接受的答案对您来说不会很有用,但所有其他链接都会)。 如果有这么多可能性..你为什么不去告诉一个?我很高兴看到您的任何回答。您给我的所有链接都没有回答我的问题或无助于找到答案。并不是因为一两个词可以匹配,答案就相关了。有时问题在于两个问题的组合,即:将操作绑定到视图模型(问题的根源)。直接在 ViewModel 中绑定到命令很容易。绑定到视图很容易。但是将命令操作绑定到 ViewModel,我不知道该怎么做? 我实际上正在做一个简短的示例。 只发布一个!只有一个工作正常! 【参考方案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
        
   

【讨论】:

谢天谢地,问题是命令被定义到 ViewModel 中。我的问题的问题主要在于没有将命令定义到 ViewModel 中......只有“CommandExecute”和/或“CommandCanEXecute”的处理程序应该在 ViewModel 中定义。 @EricOuellet:对不起,我没听懂。您的问题仍然存在还是已解决? 你很快... :-) !!!我仍在尝试理解 MackieChan 的答案,但乍一看,他似乎得到了正确的解决方案。我可能需要一些时间才能理解它。这并不像我想象的那么简单。但他听起来已经找到了一个很好的方法来做到这一点。非常感谢您的尝试!!! 我现在无法回答。据我了解 MackieChan 解决方案,他得到了一个非常好的答案,我可能会接受它。但我想在接受某些东西之前验证它是否正常工作。我现在无法验证,因为我在家,我的问题出在工作上。我会在星期二检查(星期一是加拿大许多人的假期)。 这个问题没有问如何绑定到按钮上的命令。它询问如何将 RoutedCommand 连接到视图模型上的 ICommand。【参考方案2】:

我将问题改写为:

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

对此,我会回答:好问题!

WPF 不提供执行此操作的内置方法,当您第一次启动 WPF 并且每个人都告诉您“代码隐藏是邪恶的”(确实如此)时,这尤其令人讨厌。所以你必须自己构建它。

自己建造

那么,如何自己创建这样的功能呢?好吧,首先我们需要一个等价的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) == true;
    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 与特定元素相关联的类。为此,我们将收集RoutedCommandHandlers 作为附加属性,如下所示:

/// <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) == true;
    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>

【讨论】:

非常感谢 MackieChan,看来您得到了正确答案。但我想在周二的工作中进行验证。我有一个问题,您在哪里定义 "local:RoutedCommandHandlers.Commands" 或 "i:Interaction.Behaviors" ?你把它放到资源部分吗?我的意思是我认为“CommandBindings”只接受“CommandBinding”类型的对象?我觉得自己很笨,但我真的不知道把它们放在哪里? 关于这个问题,我更愿意保持原样。那是因为我觉得它完全涵盖了我的意图。其他可能有相同问题的人不一定知道他们必须绑定到 RoutedCommand。我认为可能存在不需要 RoutedCommand 的其他潜在解决方案。 @EricOuellet RoutedCommandHandlers.Commandsi:Interaction.Behaviors 都是附加属性。我们实际上是在RoutedCommandHandlers 类中定义附加属性RoutedCommandHandlers.Commands(WPF 最终调用RoutedCommandHandlers.GetCommands)。如果您不知道附加属性是什么,我建议您在某个时候查找它们,但它们基本上是由外部类定义的依赖属性。 @EricOuellet 仅供参考,如果您不使用表达式行为,请完全忽略第二个示例,因为它是一个全新的概念,虽然有用,但目前超出了范围,可能会使情况复杂化你的理解。但我包含它是因为其他人可能会觉得它有用,并且我最终使用它而不是第一种方法,因为它需要维护的代码更少 :: ) 我知道(但不掌握)附加的 DP 及其 Blend 版本。我写了几个简单的。我曾经使用它们来控制窗口,但没有意识到 Window 也是一个依赖对象并且可以从中受益。如果我是对的,您将您的行为直接附加到窗口,这是合乎逻辑的。您的解决方案看起来非常棒!我期待在星期二对其进行测试。非常感谢!【参考方案3】:

接受的答案非常好,但似乎 OP 不太了解 RoutedCommands 的工作原理,这导致了一些混乱。引用问题:

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

这是模棱两可的,但无论哪种方式都不正确:

    要么 - 不能将 RoutedCommand 定义为 Relay/DelegateCommand,因为 RoutedCommand 是 ICommand 接口的不同实现。 或者 - 如果 VM 公开了实际的 RoutedCommand,则仍然会面临与那些在 VM 外部定义的 RoutedCommand 相同的问题(因为 RoutedCommand 的工作方式)。

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 - 那里没有什么重要的,没有逻辑(没有什么要测试的)。如果您不想隐藏代码,那么接受的答案有很好的解决方案(为您转发)。

【讨论】:

以上是关于如何在没有代码隐藏的情况下处理 ViewModel 中的 WPF 路由命令?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 View 和关联的代码隐藏文件中访问我的 ViewModel?

如何在没有代码的情况下隐藏 Android 布局 xml 中的部分可见视图?

MVVM 模式中代码隐藏的实用使用

在没有实时数据的情况下使用 ViewModel 可以吗

在不修改快捷方式的情况下使批处理文件隐藏/最小化

如何在不创建 ViewModel 对象的情况下指定 DataContext (ViewModel) 类型以在 XAML 编辑器中进行设计时绑定检查? [复制]