GalaSoft MvvmLight 不使用 RelayCommand 禁用 UserControl

GalaSoft MvvmLight not disabling UserControl with RelayCommand

我正在使用 WPF 和 GalaSoft MvvmLight (5.4.1.1) 构建一个简单的应用程序。
Everything works fine, I have a grid, and when a row is selected I enable/disable buttons that have actions assigned.

示例按钮如下所示:

<Button Command="{Binding MarkRouteAsCompletedCommand, Mode=OneTime}">Mak as Completed</Button>

当我将 Button 更改为我的 UserControl 时,我没有得到“enable/disable”效果并且我的自定义控件始终处于启用状态。

我创建了一个如下所示的 UserControl(显示了两个控件):

他们的 XAML 看起来像这样:

<controls:ShortcutButton Text="Create" Command="{Binding CreateCommand, Mode=OneTime}" Shortcut="Insert"/>
<controls:ShortcutButton Text="Edit" Command="{Binding EditCommand, Mode=OneTime}" Shortcut="F2"/>

想法是显示分配给特定按钮的键盘键。

我的用户控件如下所示:
XAML:

<UserControl x:Class="ABC.Desktop.Wpf.Controls.Buttons.ShortcutButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <StackPanel>
        <TextBlock Margin="3,0,3,0" FontSize="10" Text="{Binding Shortcut, Mode=OneWay, Converter={StaticResource ObjectToStringConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}"/>
        <Button MinWidth="80"
                Content="{Binding Text, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}"
                IsCancel="{Binding IsCancel, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
            <Button.InputBindings>
                <MouseBinding Gesture="LeftClick" Command="{Binding Command, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}"
                CommandParameter="{Binding CommandParameter, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}"/>
            </Button.InputBindings>
        </Button>
    </StackPanel>
</UserControl>

后面的代码:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace ABC.Desktop.Wpf.Controls.Buttons
{
    public partial class ShortcutButton : UserControl
    {
        public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ShortcutButton), new PropertyMetadata(null));
        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(ShortcutButton), new PropertyMetadata(null));
        public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(ShortcutButton), new PropertyMetadata(null));

        public ShortcutButton()
        {
            InitializeComponent();
        }

        public Key? Shortcut { get; set; }

        public bool IsCancel { get; set; }

        public string Text
        {
            get => (string)GetValue(TextProperty);
            set => SetValue(TextProperty, value);
        }

        public ICommand Command
        {
            get => (ICommand)GetValue(CommandProperty);
            set => SetValue(CommandProperty, value);
        }

        public object CommandParameter
        {
            get => GetValue(CommandParameterProperty);
            set => SetValue(CommandParameterProperty, value);
        }
    }
}

我不知道为什么 enabling/disabling 适用于 Button 而不是我的 UserControl。 可能我必须在我的用户控件中实现一些东西,但我不知道是什么。

注意:这与 MvvLight 完全无关。

WPF ButtonBase class 具有对计算 Command.CanExecute 的硬编码支持,以便为 IsEnabled 属性 提供值。另请参阅 source code 中的 IsEnabledCore

UserControl没有这样的支持,需要自己绑定IsEnabled

也就是说,您可以 - 而不是定义用户控件 - 使用具有自定义控件模板的 Button 控件。

您没有正确实现命令逻辑。要忽略这一点,您可以简单地扩展 ButtonBase(或 Button)而不是 UserControl。否则让你的 ShortcutButton 实现 ICommandSource.

扩展 Button 是推荐的解决方案。扩展 UserControl 几乎总是一个错误的决定,因为它不提供普通 ContentControl 的自定义,默认 StyleGeneric.xaml[= 中定义52=], 报价。

命令状态的处理逻辑如下:

public partial class ShortcutButton : UserControl, ICommandSource
{
  public static readonly DependencyProperty CommandProperty =
      DependencyProperty.Register(
        "Command",
        typeof(ICommand),
        typeof(ShortcutButton),
        new PropertyMetadata(default(ICommand), OnCommandChanged));

  public ICommand Command
  {
    get => (ICommand)GetValue(CommandProperty);
    set => SetValue(CommandProperty, value);
  }

  private bool OriginalIsEnabledValue { get; set; }
  private bool IsEnabledChangedByCommandCanExecute { get; set; }

  public ShortcutButton()
  {
    this.OriginalIsEnabledValue = this.IsEnabled;
    this.IsEnabledChanged += OnIsEnabledChanged;
  }

  private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
  {
    if (this.IsEnabledChangedByCommandCanExecute)
    {
      return;
    }
    
    this.OriginalIsEnabledValue = (bool)e.NewValue;    
  }

  private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var this_ = d as ShortcutButton;
    if (e.OldValue is ICommand oldCommand)
    {
      CanExecuteChangedEventManager.RemoveHandler(this_.Command, this_.OnCommandCanExecuteChanged);
    }

    if (e.NewValue is ICommand newCommand)
    {
      CanExecuteChangedEventManager.AddHandler(this_.Command, this_.OnCommandCanExecuteChanged);
    }
  }

  private void OnCommandCanExecuteChanged(object sender, EventArgs e)
  {
    this.IsEnabledChangedByCommandCanExecute = true;
    this.IsEnabled = this.OriginalIsEnabledValue 
      && this.Command.CanExecute(this.CommandParameter);
    this.IsEnabledChangedByCommandCanExecute = false;
  }
}

您应该使用标准 Button 并为每个快捷键配置一个 KeyBinding,而不是实现自定义 Button 控件。例如,要使快捷键成为全局快捷键,请在 Window 元素上定义输入绑定:

<Window>
  <Window.InputBindings>
    <KeyBinding Key="F2" Command="{Binding EditCommand, Mode=OneTime}" />
  </Window.InputBindings>
</Window>

要实现您想要的效果,您一定要扩展 Button 并修改默认的 Style 以显示附加标签。您的 ShortcutButton 必须修改如下:

ShortcutButton.cs

public class ShortcutButton : Button
{
  public static readonly DependencyProperty ShortcutModifierKeysProperty =
      DependencyProperty.Register(
        "ShortcutModifierKeys",
        typeof(ModifierKeys),
        typeof(ShortcutButton),
        new PropertyMetadata(default(ModifierKeys), OnShortcutModifierKeysChanged));

  public ModifierKeys ShortcutModifierKeys
  {
    get => (ModifierKeys)GetValue(ShortcutModifierKeysProperty);
    set => SetValue(ShortcutModifierKeysProperty, value);
  }

  public static readonly DependencyProperty ShortcutKeyProperty =
      DependencyProperty.Register(
        "ShortcutKey",
        typeof(Key),
        typeof(ShortcutButton),
        new PropertyMetadata(default(Key), OnShortcutKeyChanged));

  public Key ShortcutKey
  {
    get => (Key)GetValue(ShortcutKeyProperty);
    set => SetValue(ShortcutKeyProperty, value);
  }

  public static readonly DependencyProperty ShortcutKeyTargetProperty =
      DependencyProperty.Register(
        "ShortcutKeyTarget",
        typeof(UIElement),
        typeof(ShortcutButton),
        new PropertyMetadata(default(UIElement), OnShortcutKeyTargetChanged));

  public UIElement ShortcutKeyTarget
  {
    get => (UIElement)GetValue(ShortcutKeyTargetProperty);
    set => SetValue(ShortcutKeyTargetProperty, value);
  }

  private static readonly DependencyPropertyKey ShortcutKeyDisplayTextPropertyKey =
      DependencyProperty.RegisterReadOnly(
        "ShortcutKeyDisplayText",
        typeof(string),
        typeof(ShortcutButton),
        new PropertyMetadata(default(string)));

  public static readonly DependencyProperty ShortcutKeyDisplayTextProperty = ShortcutKeyDisplayTextPropertyKey.DependencyProperty;

  public string ShortcutKeyDisplayText
  {
    get => (string)GetValue(ShortcutKeyDisplayTextProperty);
    private set => SetValue(ShortcutKeyDisplayTextPropertyKey, value);
  }

  private KeyBinding ShortcutKeyBinding { get; set; }

  static ShortcutButton()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(ShortcutButton), new FrameworkPropertyMetadata(typeof(ShortcutButton)));
    CommandProperty.OverrideMetadata(typeof(ShortcutButton), new FrameworkPropertyMetadata(OnCommandChanged));
  }

  private static void OnShortcutModifierKeysChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var this_ = d as ShortcutButton;
    this_.UpdateShortcutKeyBinding();
  }

  private static void OnShortcutKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var this_ = d as ShortcutButton;
    this_.UpdateShortcutKeyBinding();
  }

  private static void OnShortcutKeyTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var this_ = d as ShortcutButton;
    this_.UpdateShortcutKeyBinding();
  }

  private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var this_ = d as ShortcutButton;
    this_.UpdateShortcutKeyBinding();
  }


  private void UpdateShortcutKeyBinding()
  {
    this.ShortcutKeyDisplayText = this.ShortcutModifierKeys != ModifierKeys.None 
      ? $"{this.ShortcutModifierKeys}+{this.ShortcutKey}" 
      : this.ShortcutKey.ToString();

    if (this.Command == null || this.ShortcutKeyTarget == null)
    {
      return;
    }

    this.ShortcutKeyTarget.InputBindings.Remove(this.ShortcutKeyBinding);

    this.ShortcutKeyBinding = new KeyBinding(this.Command, this.ShortcutKey, this.ShortcutModifierKeys);
    this.ShortcutKeyBinding.Freeze();
    this.ShortcutKeyTarget.InputBindings.Add(this.ShortcutKeyBinding);
  }
}

Generic.xaml

<Style TargetType="{x:Type local:ShortcutButton}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:ShortcutButton}">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">
          <StackPanel>
            <TextBlock Text="{TemplateBinding ShortcutKeyDisplayText}" />
            <ContentPresenter />
          </StackPanel>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

用法

<Window x:Name="Window">
  <local:ShortcutButton Content="Edit"
                        Command="{Binding EditCommand}" 
                        ShortcutKey="{x:Static Key.F2}" 
                        ShortcutModifierKeys="{x:Static ModifierKeys.Alt}"
                        ShortcutKeyTarget="{Binding ElementName=Window}" />
</Window>