如何在没有代码隐藏的情况下处理我的 ViewModel 中的 WPF 路由命令?
How can I handle WPF routed commands in my ViewModel without code-behind?
根据我对 MVVM 的理解,直接在 ViewModel 中处理路由命令是一种很好的做法。
当路由命令在 ViewModel 中定义为 RelayCommand(或 DelegateCommand)时,很容易像这样直接绑定到命令:Command={Binding MyViewModelDefinedCommand}。
实际上,对于在我的 ViewModel 之外定义的路由命令,我在 View 的代码隐藏中处理这些命令并将调用转发到 ViewModel。但我觉得我不得不这样做很尴尬。它违背了推荐的 MVVM 良好实践。我认为应该有一种更优雅的方式来完成这项工作。
如何直接在 ViewModel 中处理 "System.Windows.Input.ApplicationCommands" 或在 Viewmodel 之外定义的任何路由命令。
换句话说,对于在 ViewModel 之外定义的命令,我如何直接将 CommandBinding 回调 "CommandExecute" and/or "CommandCanExecute" 处理到 ViewModel?
那可能吗?如果是如何?如果不是,为什么?
这里有一个将命令绑定到按钮的简单示例:
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
}
}
我将问题改写为:
How can I handle WPF routed commands in my ViewModel without code-behind?
对此,我会回答:好问题!
WPF 不提供执行此操作的内置方法,这在您第一次启动 WPF 并且每个人都告诉您 "Code-Behind is evil"(确实如此)时尤其烦人。所以你必须自己构建它。
我们自己构建
那么,如何自己创建这样的功能呢?好吧,首先我们需要一个 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;
}
}
然后我们需要一个 class,它将实际将 RoutedCommandHandler 与特定元素相关联。为此,我们将收集 RoutedCommandHandler
作为附加的 属性,如下所示:
/// <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);
}
}
然后,就像在我们的元素上使用 classes 一样简单:
<local:RoutedCommandHandlers.Commands>
<local:RoutedCommandHandler RoutedCommand="Help" Command="{Binding TheCommand}" />
</local:RoutedCommandHandlers.Commands>
Interaction.Behavior实施
了解了以上,你可能会问:
Wow, that's great, but that's a lot of code. I'm using Expression Behaviors already, so is there a way to simplify this a bit?
对此,我会回答:好问题!
如果您已经在使用 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>
接受的答案非常好,但似乎 OP 不太了解 RoutedCommands 的工作原理,这造成了一些混乱。引用问题:
When a routed command is defined in a ViewModel as a RelayCommand (or
DelegateCommand), it is easy to bind directly to the command like
this: Command={Binding MyViewModelDefinedCommand}.
这是模棱两可的,但无论如何都是不正确的:
- 要么 - 不能将 RoutedCommand 定义为 Relay/DelegateCommand,因为 RoutedCommand 是 ICommand 接口的不同实现。
- 或者 - 如果一个 VM 公开了一个实际的 RoutedCommand,人们仍然会面临与那些在 VM 外部定义的 RoutedCommands 相同的问题(因为 RoutedCommands 的工作方式)。
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 class 不继承自 DependencyObject(它的命令 属性 不能绑定到 VM 上公开的命令)。您可以使用附加到 CommandBinding 的事件处理程序将调用(在代码隐藏中)转发给 VM - 那里没有什么重要的,没有逻辑(没有要测试的)。如果你不想要代码隐藏,那么接受的答案有很好的解决方案(为你转发)。
根据我对 MVVM 的理解,直接在 ViewModel 中处理路由命令是一种很好的做法。
当路由命令在 ViewModel 中定义为 RelayCommand(或 DelegateCommand)时,很容易像这样直接绑定到命令:Command={Binding MyViewModelDefinedCommand}。
实际上,对于在我的 ViewModel 之外定义的路由命令,我在 View 的代码隐藏中处理这些命令并将调用转发到 ViewModel。但我觉得我不得不这样做很尴尬。它违背了推荐的 MVVM 良好实践。我认为应该有一种更优雅的方式来完成这项工作。
如何直接在 ViewModel 中处理 "System.Windows.Input.ApplicationCommands" 或在 Viewmodel 之外定义的任何路由命令。 换句话说,对于在 ViewModel 之外定义的命令,我如何直接将 CommandBinding 回调 "CommandExecute" and/or "CommandCanExecute" 处理到 ViewModel? 那可能吗?如果是如何?如果不是,为什么?
这里有一个将命令绑定到按钮的简单示例:
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
}
}
我将问题改写为:
How can I handle WPF routed commands in my ViewModel without code-behind?
对此,我会回答:好问题!
WPF 不提供执行此操作的内置方法,这在您第一次启动 WPF 并且每个人都告诉您 "Code-Behind is evil"(确实如此)时尤其烦人。所以你必须自己构建它。
我们自己构建
那么,如何自己创建这样的功能呢?好吧,首先我们需要一个 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;
}
}
然后我们需要一个 class,它将实际将 RoutedCommandHandler 与特定元素相关联。为此,我们将收集 RoutedCommandHandler
作为附加的 属性,如下所示:
/// <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);
}
}
然后,就像在我们的元素上使用 classes 一样简单:
<local:RoutedCommandHandlers.Commands>
<local:RoutedCommandHandler RoutedCommand="Help" Command="{Binding TheCommand}" />
</local:RoutedCommandHandlers.Commands>
Interaction.Behavior实施
了解了以上,你可能会问:
Wow, that's great, but that's a lot of code. I'm using Expression Behaviors already, so is there a way to simplify this a bit?
对此,我会回答:好问题!
如果您已经在使用 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>
接受的答案非常好,但似乎 OP 不太了解 RoutedCommands 的工作原理,这造成了一些混乱。引用问题:
When a routed command is defined in a ViewModel as a RelayCommand (or DelegateCommand), it is easy to bind directly to the command like this: Command={Binding MyViewModelDefinedCommand}.
这是模棱两可的,但无论如何都是不正确的:
- 要么 - 不能将 RoutedCommand 定义为 Relay/DelegateCommand,因为 RoutedCommand 是 ICommand 接口的不同实现。
- 或者 - 如果一个 VM 公开了一个实际的 RoutedCommand,人们仍然会面临与那些在 VM 外部定义的 RoutedCommands 相同的问题(因为 RoutedCommands 的工作方式)。
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 class 不继承自 DependencyObject(它的命令 属性 不能绑定到 VM 上公开的命令)。您可以使用附加到 CommandBinding 的事件处理程序将调用(在代码隐藏中)转发给 VM - 那里没有什么重要的,没有逻辑(没有要测试的)。如果你不想要代码隐藏,那么接受的答案有很好的解决方案(为你转发)。