简体   繁体   中英

GalaSoft MvvmLight not disabling UserControl with RelayCommand

I'm building a simple application using WPF and 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.

Sample button looks like that:

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

When I change the Button to my UserControl I don't get the "enable/disable" effect and my custom control is always enabled.

I've created a UserControl that looks like this (two controls shown):

在此处输入图像描述

The XAML for them looks like this:

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

The idea was to display the keyboard key that is assigned to a specific button.

My UserControl looks like this:
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>

Code behind:

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);
        }
    }
}

I have no idea why enabling/disabling works with Button but not with my UserControl. Probably I must implement something in my UserControl, but I have not a clue what.

You did not implement the command logic properly. To omit this, you can simply extend ButtonBase (or Button ) instead of UserControl . Otherwise let your ShortcutButton implement ICommandSource .

Extending Button is the recommended solution. Extending UserControl is almost always a bad decision as it does not provide the customization that a plain ContentControl , with a default Style defined in Generic.xaml , offers.

The logic to handle the command state is as followed:

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;
  }
}

Instead of implementing a custom Button control, you should use the standard Button and configure a KeyBinding for each shortcut key. For example, to make the shortcut keys global, define the input bindings on the Window element:

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

To achieve what you want, you should definitely extend Button and modify the default Style to show the additional label. Your ShortcutButton musat be modified as followed:

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), OnShortcutKeyChanged));

  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()
  {
    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);

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

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>

Usage

<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>

Note: this is not related to MvvLight at all.

The WPF ButtonBase class has hard-coded support for evaluating Command.CanExecute to provide a value for the IsEnabled property. See also IsEnabledCore in the source code .

There is not such support for UserControl, so you have to bind IsEnabled yourself.

That said, you could - instead of defining a user control - use a Button control with custom control template.

Why don't you bind directly to the Command and CommandParameter properties of the Button ?:

<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}}}"
        Command="{Binding Command, RelativeSource={RelativeSource AncestorType=UserControl}}"
        CommandParameter="{Binding CommandParameter, RelativeSource={RelativeSource AncestorType=UserControl}}" />

Then it should work provided that there is a CreateCommand / EditCommand property of the DataContext of the ShortcutButton control that returns a valid ICommand implementation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM