MVVM设计模式中的路由命令处理

在开发基于WPF的应用程序时,经常会遇到需要使用第三方控件的情况。这些控件可能实现了标准的路由命令,如System.Windows.Input中的ApplicationCommands、MediaCommands和NavigationCommands。然而,如果希望保持MVVM(Model-View-ViewModel)设计模式的一致性,就需要找到一种方法来将这些路由命令绑定到ViewModel中的命令。

问题的提出

Josh Smith提出了一种解决方案,但当XAML代码实现时,可读性较差。因此,需要寻找一种更优雅的实现方式。

解决方案

可以通过添加一个不可见的UserControl,名为CommandSinkControl,来关联路由命令和ViewModel中的ICommand。然后,在正常的CommandBindings中,将事件调用转发到CommandSinkControl的公共方法DoExecuted和DoCanExecute来处理事件。

首先,需要添加一个名为CommandSinkControl的控件到代码库中。

<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> 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路由命令绑定到ViewModel中定义的OpenCommand。

<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. --> <local:CommandSinkControl x:Name="CommandSinkControl"> <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> using System.Windows; using System.Windows.Input; namespace CommandSink { 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); } } } 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 } }

讨论

虽然需要在视图的代码后台添加一些连接代码,但为了提高可读性,这是可以接受的。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485