Skip to content

Non-cancelling, non-concurrent AsyncRelayCommand #1125

@chrisc-onaorg

Description

@chrisc-onaorg

Overview

There are contexts where if an async command is executed for any reason while an execution of the command is already in progress, we don't want to cancel the in-progress execution and start over. It would be useful, therefore, to add the ability to set an option for AsyncRelayCommand (and AsyncRelayCommand) that skips the normal behaviour of ExecuteAsync() when an execution is already ongoing.

API breakdown

namespace CommunityToolkit.Mvvm.Input;

[Flags]
public enum AsyncRelayCommandOptions
{
    /// ... existing members ...

    /// <summary>
    /// <para>Concurrent executions are disallowed. This options makes it so that the same command cannot be invoked while it is already executing.</para>
    /// <para>
    /// Note that additional considerations should be taken into account in this case:
    /// <list type="bullet">
    ///     <item>If the command supports cancellation, the current invocation can still be canceled before starting a new one.</item>
    ///     <item>The <see cref="AsyncRelayCommand.ExecutionTask"/> property will always represent the current operation.</item>
    ///     <item>This option and <see cref="AllowConcurrentExecutions"/> are mutually exclusive.</item>
    /// </list>
    /// </para>
    /// </summary>
    ContinueExistingExecutions = 1 << 2
}

public sealed class RelayCommandAttribute : Attribute
{
    /// ... existing members ...

    /// <summary>
    /// Gets or sets a value indicating whether or not to ignore concurrent execution attempts for an asynchronous command.
    /// <para>
    /// When set for an attribute used on a method that would result in a <see cref="AsyncRelayCommand"/> or an
    /// <see cref="AsyncRelayCommand{T}"/> property to be generated, this will modify the behavior of these commands
    /// when an execution is invoked while a previous one is still running. It is the same as creating an instance of
    /// these command types with a constructor such as <see cref="AsyncRelayCommand(Func{System.Threading.Tasks.Task}, AsyncRelayCommandOptions)"/>
    /// and using the <see cref="AsyncRelayCommandOptions.ContinueExistingExecutions"/> value.
    /// </para>
    /// </summary>
    /// <remarks>
    /// <para>Using this property is not valid if the target command doesn't map to an asynchronous command.</para>
    /// <para>Using this property in conjunction with <see cref="AllowConcurrentExecutions"/> is not supported.</para>
    /// </summary>
    public bool ContinueExistingExecutions { get; init; }
}

Usage example

class ViewModel
{
    [RelayCommand(ContinueExistingExecutions = true)]
    private Task PerformNonRestartableOperationAsync(CancellationToken cancellationToken = default)
    {
        // do something that shouldn't be restarted whenever the command is invoked
    }
}

Breaking change?

I'm not sure

Alternatives

As an alternative I've created modified AsyncRelayCommandAlt and AsyncRelayCommandAlt<T> classes in our project based on the MVVM Toolkit implementations, but without the checks for the AllowConcurrentExecutions option and with the following changes at the start of the ExecuteAsync methods:

public Task ExecuteAsync(object? parameter)
{
    // If we're already running, no-op out
    if (ExecutionTask is { IsCompleted: false })
        return Task.Completed;

    // ... existing implementation ...
}

This is far less convenient however as it means we can't take advantage of code generation through RelayCommandAttribute.

Additional context

While I've used the name ContinueExistingExecutions I think it's a pretty bad name, I just couldn't think of anything better right now. I'd be very happy if someone could suggest a better name whether or not this proposal gets accepted.

Help us help you

Yes, but only if others can assist

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature request 📬A request for new changes to improve functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions