Search code examples
c#wpf

Not just a simple custom Button


How to create a custom button that would contain an image that changes depending on IsExecuting in AsyncCommand? It needs to be implemented without creating a UserControl, only by inheritance from Button (public class CustomButton : Button) and Style in XAML.

public class AsyncCommand : ViewModelBase, ICommand
{
    protected readonly Func<Task> _execute;
    protected readonly Func<bool>? _canExecute;

    private bool _isExecuting;
    public bool IsExecuting
    {
        get => _isExecuting;
        set => RisePropertyChanged(ref _isExecuting, value);
    }

    public AsyncCommand(Func<Task> execute, Func<bool>? canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public event EventHandler? CanExecuteChanged;

    public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true);

    public async void Execute(object? parameter)
    {
        if (CanExecute(parameter))
        {
            try
            {
                IsExecuting = true;
                await ExecuteAsync(parameter);
            }
            finally
            {
                IsExecuting = false;
            }
        }

        OnCanExecuteChanged();
    }

    protected virtual async Task ExecuteAsync(object? parameter) => await _execute();

    protected virtual void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

Perhaps something like using an event EventHandler? CanExecuteChanged, which is used in ICommand and is reacted to by Button, instead of RaisePropertyChanged, but I'm out of ideas already.

I searched many places and tried many things to implement it, but without success.


Solution

  • You can achieve your behavior using a Style and a DataTrigger. You can define this Style as an application resource or as part of the default Style of your custom Button.

    The following example shows how to toggle the Button.Content value. You can modify the trigger to toggle any other dependency property like an image source. It's not clear how your button looks like so I decided to show the trigger targeting the Content property:

    <Button Command="{Binding SomeCommand}">
      <Button.Style>
        <Style TargetType="Button">
          <Setter Property="Content"
                  Value="Not running" />
    
          <Style.Triggers>
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Command.IsExecuting}"
                         Value="True">
              <Setter Property="Content"
                      Value="Command is Running..." />
            </DataTrigger>
          </Style.Triggers>
        </Style>
      </Button.Style>
    </Button>
    

    A note on your ICommand implementation: your command neither links the ICommand.CanExecuteChanged event to the CommandManager.RequerySuggested event nor does it enable the client to raise it manually. The event is currently only raised after the command has been executed, which is pretty pointless.

    What is the point of an ExecuteAsync when you don't make it public to add it to the commands API?

    If you want to make ExecuteAsync virtual to allow inheritors to override it, you should consider to add the execute logic to this virtual method to enable inheritors to change the behavior in contrast to only adding behavior.Alternatively, define the Icommand.Execute implentation as virtual too.

    Internally modifying the CanExecute condition by adding the IsExecuting condition is also questionable as it removes some of the flexibility of the way the type can be used.

    Your code contains a typo: RisePropertyChanged instead of RaisePropertyChanged.

    A suggested improved version of your AsyncCommand:

    public class AsyncCommand : ViewModelBase, ICommand
    {
      protected readonly Func<Task> _execute;
      protected readonly Func<bool>? _canExecute;
    
      private bool _isExecuting;
      public bool IsExecuting
      {
        get => _isExecuting;
        set => RisePropertyChanged(ref _isExecuting, value);
      }
    
      public bool IsCommandMangerRequerySuggestedEnabled { get; set; }
    
      public AsyncCommand(Func<Task> execute, Func<bool>? canExecute = null)
      {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    
        this.IsCommandMangerRequerySuggestedEnabled = true;
        CommandManager.RequerySuggested += OnCommandManagerRequerySuggested;
      }
    
      private void OnCommandManagerRequerySuggested(object? sender, EventArgs e)
      {
        if (this.IsCommandMangerRequerySuggestedEnabled)
        {
          this.CanExecuteChanged?.Invoke(sender, e);
        }
      }
    
      // Manually raise the CanExecuteChanged event
      public void InvalidateCommand()
        => this.CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    
      public event EventHandler? CanExecuteChanged;
    
      public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true);
    
      public async virtual void Execute(object? parameter)
      {
        if (CanExecute(parameter))
        {
          try
          {
            IsExecuting = true;
            await ExecuteAsync(parameter);
          }
          finally
          {
            IsExecuting = false;
          }
        }
      }
    
      public virtual async Task ExecuteAsync(object? parameter) => await _execute();
    
      protected virtual void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }