Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enhancement] Avoid parallel execution of command groups #2890

Open
thomasgalliker opened this issue Jun 14, 2023 · 0 comments
Open

[Enhancement] Avoid parallel execution of command groups #2890

thomasgalliker opened this issue Jun 14, 2023 · 0 comments

Comments

@thomasgalliker
Copy link

thomasgalliker commented Jun 14, 2023

Summary

An MVVM user interface binds input controls such as buttons or context menus to ICommand implementations in a viewmodel. Usually, we use DelegateCommands or an async counterpart that implements ICommand for this purpose. Commands on a user interface may be started in parallel which causes the underlying command handlers to run in parallel. Command's "canExecute" function can be used to check if one command is executing but this approach is not thread safe and still allows user input to happen in parallel.

In order to synchronize parallel execution of commands, we created a new thing we called "CommandGroup" (the name can be discussed, of course). The aim is to toggle an execution flag in a thread-safe way to avoid that a command can be started while another command is already running.

API Changes

Here I have reduced-to-the-max copy of the working code. I also have unit tests ready. (I'm just holding unnecessary things back, in case the feedback is, that Prism doesn't want such a thing to integrate).

public class CommandGroup : BindableObject, ICommandGroup
{
    private const long NotRunning = 0;
    private const long Running = 1;

    private readonly string _name;

    private long _currentState;

    /// <summary>
    /// Initializes a new instance of the <see cref="CommandGroup"/> class.
    /// </summary>
    public CommandGroup() : this(null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="CommandGroup"/> class
    /// with <param name="name">the name of the instance</param> for debugging purposes.
    /// </summary>
    public CommandGroup(string name)
    {
        _name = name ?? Guid.NewGuid().ToString().Substring(0, 5).ToUpperInvariant();
    }

    public bool IsAnyRunning => Interlocked.Read(ref _currentState) == Running;

    #region Create methods for Xamarin.Forms.Command
    // Removed for brevity
    #endregion

    #region Create methods for AsyncRelayCommand

    public AsyncRelayCommand CreateAsyncRelayCommand(Func<Task> execute)
    {
        return CreateAsyncRelayCommand(
            execute,
            () => true);
    }

    public AsyncRelayCommand CreateAsyncRelayCommand(Func<Task> execute, Func<bool> canExecute)
    {
        return CreateCommandWithFactory(
            execute,
            canExecute,
            (e, ce) => new AsyncRelayCommand(e, ce));
    }

    public AsyncRelayCommand<TParameter> CreateAsyncRelayCommand<TParameter>(Func<TParameter, Task> execute)
    {
        return CreateAsyncRelayCommand(
            execute,
            () => true);
    }

    public AsyncRelayCommand<TParameter> CreateAsyncRelayCommand<TParameter>(Func<TParameter, Task> execute, Func<bool> canExecute)
    {
        return CreateAsyncRelayCommand(
            execute,
            _ => canExecute());
    }

    public AsyncRelayCommand<TParameter> CreateAsyncRelayCommand<TParameter>(
        Func<TParameter, Task> execute, 
        Func<TParameter, bool> canExecute)
    {
        return CreateCommandWithFactory(
            execute,
            canExecute,
            (e, ce) => new AsyncRelayCommand<TParameter>(e, p => ce(p)));
    }

    #endregion

    private TCommand CreateCommandWithFactory<TCommand>(
        Func<Task> execute,
        Func<bool> canExecute,
        Func<Func<Task>, Func<bool>, TCommand> factory)
        where TCommand : ICommand
    {
        return CreateCommandWithFactory<TCommand, object>(
            _ => execute(),
            _ => canExecute(),
            (e, ce) => factory(() => e(null), () => ce(null)));
    }

    private TCommand CreateCommandWithFactory<TCommand, TParameter>(
        Func<TParameter, Task> execute,
        Func<TParameter, bool> canExecute,
        Func<Func<TParameter, Task>, Func<TParameter, bool>, TCommand> factory)
        where TCommand : ICommand
    {
        var command = factory(
            async p =>
            {
                if (Interlocked.CompareExchange(ref _currentState, Running, NotRunning) == NotRunning)
                {
                    OnPropertyChanged(nameof(IsAnyRunning));
                    Debug.WriteLine($"CommandGroup {_name}: Command execution started");

                    try
                    {
                        await execute(p);
                    }
                    finally
                    {
                        Debug.WriteLine($"CommandGroup {_name}: Command execution finished");
                        Interlocked.Exchange(ref _currentState, NotRunning);
                        OnPropertyChanged(nameof(IsAnyRunning));
                    }
                }
                else
                {
                    Debug.WriteLine($"CommandGroup {_name}: Command execution skipped");
                }
            },
            p =>
            {
                if (!IsAnyRunning)
                {
                    return canExecute(p);
                }

                Debug.WriteLine($"CommandGroup {_name}: CanExecute skipped");
                return false;
            });

        return command;
    }
}

API Usage

You can use CommandGroup with just minimal changes in your viewmodels. In fact, we upgraded our code with search-replace.

Before:

AcceptGtcCommand = new AsyncRelayCommand(
    execute: AcceptGtcAsync);

AbortGtcCommand = new AsyncRelayCommand(
    execute: AbortGtcAsync);

After:

var commandGroup = new CommandGroup();

AcceptGtcCommand = commandGroup.CreateAsyncRelayCommand(
    execute: AcceptGtcAsync);

AbortGtcCommand = commandGroup.CreateAsyncRelayCommand(
    execute: AbortGtcAsync);

Your Feedback

What do you think about the proposed solution?
Is this a non-problem?
How do other people solve this issue?

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant