将路由命令(RoutedCommands)“沉没”到 WPF 中的 ViewModel 命令





5.00/5 (3投票s)
这是“在 WPF 中使用 ViewModel 的 RoutedCommands”的另一种方案。
引言
我一直在寻找一种将 RoutedCommand 路由到 ViewModel 命令的解决方案。我找到了 Josh Smith 的 解决方案,但我发现当 Xaml 完成时很难阅读。我想要一个更易读的方案。
为什么我不直接使用 ViewModel 命令呢?好吧,有时我无法避免。当我的产品使用第三方控件时,它们通常会实现标准的 RoutedUICommands,例如 System.Windows.Input
命名空间中的那些,例如 ApplicationsCommands
、MediaCommands
和 NavigationCommands
。我想使用我购买的控件,但我也想坚持我的 MVVM 设计。
解决方案
我的解决方案包括添加一个不可见的 UserControl,名为 CommandSinkControl
。该控件将 RoutedCommands 与 view model ICommands 关联起来。然后在你的正常的 CommandBindings 中,将事件调用转发到 CommandSinkControl 的公共方法 DoExecuted
和 DoCanExecute
来处理该事件。
代码
你只需要将一个控件添加到你的代码库中,那就是 CommandSinkControl
。
CommandSinkControl.xaml
<UserControl x:Class="CommandSink.CommandSinkControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Visibility="Collapsed">
</UserControl>
CommandSinkControl.xaml.cs
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;
namespace CommandSink {
[ContentProperty("CommandSinkBindings")]
public partial class CommandSinkControl : UserControl {
private static readonly DependencyPropertyKey SinksPropertyKey =
DependencyProperty.RegisterReadOnly("CommandSinkBindings",
typeof(CommandSinkBindingCollection),
typeof(CommandSinkControl),
null);
public static readonly DependencyProperty SinksProperty =
SinksPropertyKey.DependencyProperty;
public CommandSinkBindingCollection CommandSinkBindings {
get { return (CommandSinkBindingCollection)this.GetValue(SinksProperty); }
private set { this.SetValue(SinksPropertyKey, value); }
}
public CommandSinkControl() {
this.InitializeComponent();
this.CommandSinkBindings = new CommandSinkBindingCollection();
this.CommandSinkBindings.CollectionChanged += this.Sinks_OnCollectionChanged;
}
protected override IEnumerator LogicalChildren {
get {
if (this.CommandSinkBindings == null) {
yield break;
}
foreach (var sink in this.CommandSinkBindings) {
yield return sink;
}
}
}
private void Sinks_OnCollectionChanged(object sender,
NotifyCollectionChangedEventArgs e) {
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
foreach (var sink in e.NewItems) {
this.AddLogicalChild(sink);
}
break;
}
}
public void DoCanExecute(object sender, CanExecuteRoutedEventArgs e) {
var commandSinkBinding = this.CommandSinkBindings
.FirstOrDefault(csb => csb.Faucet == e.Command);
if (commandSinkBinding != null && commandSinkBinding.Drain != null) {
e.Handled = true;
e.CanExecute = commandSinkBinding.Drain.CanExecute(e.Parameter);
}
}
public void DoExecuted(object sender, ExecutedRoutedEventArgs e) {
var commandSinkBinding = this.CommandSinkBindings
.FirstOrDefault(csb => csb.Faucet == e.Command);
if (commandSinkBinding != null && commandSinkBinding.Drain != null) {
e.Handled = true;
commandSinkBinding.Drain.Execute(e.Parameter);
}
}
}
public sealed class CommandSinkBindingCollection
: ObservableCollection<CommandSinkBinding> {
}
public class CommandSinkBinding : FrameworkElement {
public static readonly DependencyProperty FaucetProperty =
DependencyProperty.RegisterAttached("Faucet",
typeof(RoutedCommand),
typeof(CommandSinkBinding));
public RoutedCommand Faucet {
get { return (RoutedCommand) this.GetValue(FaucetProperty); }
set { this.SetValue(FaucetProperty, value); }
}
public static readonly DependencyProperty DrainProperty =
DependencyProperty.RegisterAttached("Drain",
typeof(ICommand),
typeof(CommandSinkBinding));
public ICommand Drain {
get { return (ICommand) this.GetValue(DrainProperty); }
set { this.SetValue(DrainProperty, value); }
}
}
}
实施
在下面的示例中,我已将 CommandSinkControl
连接起来,使其将 ApplicationCommands.Open
RoutedCommand 路由到我在 ViewModel 中定义的 OpenCommand
。
MainWindow.xaml
<Window x:Class="CommandSink.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CommandSink"
Title="CommandSink" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
<!-- Normal Command Bindings to RoutedCommands -->
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open"
Executed="CommandBinding_OnExecuted"
CanExecute="CommandBinding_OnCanExecute"/>
</Window.CommandBindings>
<Window.InputBindings>
<!-- Example KeyBinding with Command Binding to a Routed Command -->
<KeyBinding Gesture="CTRL+O" Command="ApplicationCommands.Open"/>
</Window.InputBindings>
<StackPanel>
<!-- Make sure this is at the top so that CanExecute events can use CommandSinkControl.
It defaults to hidden, so you don't have to hide it.-->
<local:CommandSinkControl x:Name="CommandSinkControl">
<!-- Sinks the ApplicationCommands.Open RoutedUICommand to the OpenCommand -->
<local:CommandSinkBinding Faucet="ApplicationCommands.Open"
Drain="{Binding OpenCommand}"/>
</local:CommandSinkControl>
<CheckBox Margin="10"
Content="Toggles the Open button's enabled state"
IsChecked="{Binding OpenCanExecute}"/>
<!-- Example Button with Command Binding to a Routed Command-->
<Button Margin="10" Content="Open" HorizontalAlignment="Left"
Command="ApplicationCommands.Open"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Input;
namespace CommandSink {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
this.InitializeComponent();
this.DataContext = new MainViewModel();
}
private void CommandBinding_OnExecuted(object sender, ExecutedRoutedEventArgs e) {
this.CommandSinkControl.DoExecuted(sender, e);
}
private void CommandBinding_OnCanExecute(object sender, CanExecuteRoutedEventArgs e) {
this.CommandSinkControl.DoCanExecute(sender, e);
}
}
}
MainViewModel.cs
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using Microsoft.Practices.Prism.Commands;
namespace CommandSink {
public class MainViewModel : INotifyPropertyChanged {
public ICommand OpenCommand { get; private set; }
private bool m_openCanExecute;
public bool OpenCanExecute {
get { return this.m_openCanExecute; }
set {
if (value != this.m_openCanExecute) {
this.m_openCanExecute = value;
this.OnPropertyChanged("OpenCanExecute");
}
}
}
public MainViewModel() {
this.OpenCommand = new DelegateCommand(this.OpenCommand_OnExecuted,
this.OpenCommand_OnCanExecute);
}
private bool OpenCommand_OnCanExecute() {
return this.OpenCanExecute;
}
private void OpenCommand_OnExecuted() {
MessageBox.Show("Open Command");
}
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) {
var handler = this.PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
兴趣点
虽然你需要在视图的代码背后添加一些接线代码,但我认为为了获得更高的可读性,这是可以接受的。
你可能还会问为什么我创建了一个不可见的 UserControl 而不是直接将其放在 Window.Resources 中。Drain="{Binding OpenCommand}"
能够正常工作至关重要。为此,我需要它成为 LogicalTree 的一部分并继承 DataContext,以便我可以使用 DataBinding。你有什么更好的解决方案吗?请告诉我!
谢谢!
感谢您阅读我的文章。我希望我表达清楚且易于理解。请提供评论、建议和/或疑虑!我很乐意听取!谢谢!