0

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

enter image description here

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.

Misiu
  • 4,738
  • 21
  • 94
  • 198
  • AFAIK Galasoft MVVMLight toolkit is deprecated in favor of Microsoft MVVM Toolkit. Anyway, there is also an issue with `RelayCommand` implementation of Galasoft's version, where the UI elements are not updated even if `canExecute` action returns true, until the UI is redrawn/invalidated. You may find more information on https://stackoverflow.com/questions/2331622/weird-problem-where-button-does-not-get-re-enabled-unless-the-mouse-is-clicked – mcy Nov 29 '21 at 13:43
  • @mcy I've already added `CommandManager.InvalidateRequerySuggested();` :) thanks – Misiu Nov 29 '21 at 13:44

2 Answers2

1

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.

Julian
  • 886
  • 10
  • 21
Klaus Gütter
  • 11,151
  • 6
  • 31
  • 36
1

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

  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>

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>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • I didn't add logic to store the value of `IsEnabled` (don't know yet how to do that, but after a quick test it is working! UserControl was the first thing I've tried, but maybe I'll be able to create something by extending a button with a template. – Misiu Nov 29 '21 at 13:26
  • Yes, it works. But currently CanExecuteChanged will override the original IsEnabled value. If the control is explicitly disabled then CanExecuteChanged would override this value and forcefully enable the control. You probably don't want that. – BionicCode Nov 29 '21 at 13:32
  • I have updated the answer to show how such an implementation could look like. – BionicCode Nov 29 '21 at 13:36
  • Thank you for the update. I've also added more context to my question, to visually show what I want to achieve. If there is a better way please let me know. The current solution looks overcomplicated, and it probably is. – Misiu Nov 29 '21 at 13:39
  • P.S. the `IsEnabledChangedByCommandCanExecute` declaration is missing :) – Misiu Nov 29 '21 at 13:41
  • thank you. It's fixed. Aside from extedning Button directly to avoid the complete implementation, you should use KeyBinding instead. let me provide an example. – BionicCode Nov 29 '21 at 13:45
  • Also add `OriginalIsEnabledValue = this.IsEnabled;` to the constructor to read the initial value, otherwise `OriginalIsEnabledValue ` is always false. – Misiu Nov 29 '21 at 13:47
  • I have updated the example to show how to configure shortcut keys. – BionicCode Nov 29 '21 at 13:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/239668/discussion-between-misiu-and-bioniccode). – Misiu Nov 29 '21 at 13:50
  • I have updated the answer to show how you have to modify your ShortcutButton class. The new ShortcutButton also configures automatically a KeyBinding. – BionicCode Nov 29 '21 at 14:55
  • You can add a KeyGesture property to support KeyGesture in addition, whe configuring the KeyBinding in the UpdateShortcutKeyBinding(). – BionicCode Nov 29 '21 at 15:00
  • That's exactly what I'm trying to do. I'll give this a try in just a minute! – Misiu Nov 29 '21 at 15:02
  • Yes, but you would have to implement the button interaction behavior (visual feedback), like mouse over etc by adding triggers or the VisualStateManager to the default style in the Generic.xaml file. – BionicCode Nov 29 '21 at 15:05
  • You can find the example style with the default visual states here:[Button ControlTemplate Example](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/button-styles-and-templates?view=netframeworkdesktop-4.8#button-controltemplate-example) – BionicCode Nov 29 '21 at 15:08
  • I've simplified the template a lot, I have a StackPanelwith TextBlock and Button inside. – Misiu Nov 30 '21 at 08:13
  • One last thing: Can I update `ShortcutKeyDisplayText` at design time? Right now, I only see the correct shortcut on runtime, but it would help a lot to see that when I create the app. Can this be done? – Misiu Nov 30 '21 at 08:18
  • And why does the `ShortcutKeyDisplayTextPropertyKey` needs a PropoertyChangedCallback? Is is readonly. – Misiu Nov 30 '21 at 08:22
  • 1
    It's a copy&paste left over. It's an example. – BionicCode Nov 30 '21 at 11:43
  • Good to know :) I've removed it and everything works fine. Now the last part is the design-time support – Misiu Nov 30 '21 at 13:04
  • What do you mean by design-time support? – BionicCode Nov 30 '21 at 14:11
  • For a short time, the label above the button didn't update and I always saw `ShortcutKeyDisplayText` text when I changed something in XAML and look at Design (in VS), but now it seems to work as expected. – Misiu Nov 30 '21 at 14:35
  • Funny thing is that it displays `PageDown` key as `Next` – Misiu Nov 30 '21 at 14:35
  • 1
    Simply move the `this.ShortcutKeyDisplayText` assignment in the `UpdateShortcutKeyBinding()` method before the `if` statement to the very top. – BionicCode Nov 30 '21 at 14:39
  • Next is also an enumeration value of Key. Internally PageDown is defined to return the value of Next. So you get the PageDown enumeration value but since internally PageDown is defined as `PageDown = Next`, PageDown returns Next. – BionicCode Nov 30 '21 at 14:48
  • Thank you for all the help!!! – Misiu Dec 06 '21 at 14:52