在开发基于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
}
}
讨论
虽然需要在视图的代码后台添加一些连接代码,但为了提高可读性,这是可以接受的。