Home > Blockchain >  GalaSoft MvvmLight not disabling UserControl with RelayCommand
GalaSoft MvvmLight not disabling UserControl with RelayCommand

Time:11-30

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.

CodePudding user response:

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>

CodePudding user response:

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.

CodePudding user response:

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.

  • Related