Asynchrone ICommand-Implementierung mit optionalem canExecute

Oct 02 2020

Ich habe folgende ICommand-Implementierung, die großartig funktioniert, aber ich möchte sie erweitern, damit ich externe canExecute-Parameter übergeben kann

public class AsyncRelayCommand : ICommand
    {
        private readonly Func<object, Task> callback;
        private readonly Action<Exception> onException;
        private bool isExecuting;

        public bool IsExecuting
        {
            get => isExecuting;
            set
            {
                isExecuting = value;
                CanExecuteChanged?.Invoke(this, new EventArgs());
            }
        }
        public event EventHandler CanExecuteChanged;

        public AsyncRelayCommand(Func<object, Task> callback, Action<Exception> onException = null)
        {
            this.callback = callback;
            this.onException = onException;
        }

        public bool CanExecute(object parameter) => !IsExecuting;

        public async void Execute(object parameter)
        {
            IsExecuting = true;
            try
            {
                await callback(parameter);
            }
            catch (Exception e)
            {
                onException?.Invoke(e);
            }

            IsExecuting = false;
        }
    }

Kann diese Implementierung so erweitert werden, dass sowohl Execute1AsyncCommand als auch Execute2AsyncCommand dies bestätigen, wenn sich CanExecute () des Aufrufers ändert? Hier ist meine Anruferklasse:

public class Caller : ObservableObject
{
public ObservableTask Execute1Task { get; } = new ObservableTask();
public ObservableTask Execute2Task { get; } = new ObservableTask();

public ICommand Execute1AsyncCommand { get; }
public ICommand Execute2AsyncCommand { get; }

public Caller()
{
    Execute1AsyncCommand = new AsyncRelayCommand(Execute1Async);
    Execute2AsyncCommand = new AsyncRelayCommand(Execute2Async);
}

private bool CanExecute(object o)
{
    return Task1?.Running != true && Task2?.Running != true;
}

private async Task Execute1Async(object o)
{
    Task1.Running = true;
            
    try
    {
        await Task.Run(()=>Thread.Sleep(2000)).ConfigureAwait(true);
        Task1.RanToCompletion = true;
    }
    catch (Exception e)
    {
        Task1.Faulted = true;
    }
}
private async Task Execute2Async(object o)
{
    Task2.Running = true;

    try
    {
        await Task.Run(() => Thread.Sleep(2000)).ConfigureAwait(true);
        Task2.RanToCompletion = true;
    }
    catch (Exception e)
    {
        Task2.Faulted = true;
    }
}
}

Bei anderen Anrufern möchte ich immer noch in der Lage sein, AsyncRelayCommand()nur callbackobligatorisch zu sein. In diesem Fall CanExecutesollte intern AsyncRelayCommandwie in meiner ursprünglichen Implementierung ausgewertet werden .

Der Vollständigkeit halber hier meine Ansicht:

<StackPanel>
    <Button Content="Execute Task 1"
            Command="{Binding Execute1AsyncCommand}" />
    <Button Content="Execute Task 2"
            Command="{Binding Execute2AsyncCommand}" />
    <TextBlock Text="Task 1 running:" />
    <TextBlock Text="{Binding Task1.Running}" />
    <TextBlock Text="Task 2 running:" />
    <TextBlock Text="{Binding Task2.Running}" />
</StackPanel>

Und ObservableTask-Klasse:

public class ObservableTask : ObservableObject
{
    private bool running;
    private bool ranToCompletion;
    private bool faulted;

    public Task Task { get; set; }

    public bool WaitingForActivation => !Running && !RanToCompletion && !Faulted;

    public bool Running
    {
        get => running;
        set
        {
            running = value;
            if (running)
            {
                RanToCompletion = false;
                Faulted = false;
            }
        }
    }

    public bool RanToCompletion
    {
        get => ranToCompletion;
        set
        {
            ranToCompletion = value;
            if (ranToCompletion)
            {
                Running = false;
            }
        }
    }

    public bool Faulted
    {
        get => faulted;
        set
        {
            faulted = value;
            if (faulted)
            {
                Running = false;
            }
        }
    }
}

Was ich erreichen möchte, ist, dass nach dem Drücken einer Taste beide deaktiviert werden, bis alle Aufgaben erledigt sind.

Lösung

Am Ende hatte ich folgende Implementierung, die bisher wie beabsichtigt funktioniert:

public class AsyncRelayCommand : ICommand
{
    private bool isExecuting;
    private readonly Func<object, Task> execute;
    private readonly Predicate<object> canExecute;
    private readonly Action<Exception, object> onException;

    private Dispatcher Dispatcher { get; }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public AsyncRelayCommand(Func<object, Task> execute, Predicate<object> canExecute = null, Action<Exception, object> onException = null)
    {
        this.execute = execute;
        this.canExecute = canExecute;
        this.onException = onException;
        Dispatcher = Application.Current.Dispatcher;
    }

    private void InvalidateRequerySuggested()
    {
        if (Dispatcher.CheckAccess())
            CommandManager.InvalidateRequerySuggested();
        else
            Dispatcher.Invoke(CommandManager.InvalidateRequerySuggested);
    }

    public bool CanExecute(object parameter) => !isExecuting && (canExecute == null || canExecute(parameter));

    private async Task ExecuteAsync(object parameter)
    {
        if (CanExecute(parameter))
        {
            try
            {
                isExecuting = true;
                InvalidateRequerySuggested();
                await execute(parameter);
            }
            catch (Exception e)
            {
                onException?.Invoke(e, parameter);
            }
            finally
            {
                isExecuting = false;
                InvalidateRequerySuggested();
            }
        }
    }

    public void Execute(object parameter) => _ = ExecuteAsync(parameter);
}

Verwendung:

public class Caller: ObservableObject
{
    public ObservableTask Task1 { get; } = new ObservableTask();
    public ObservableTask Task2 { get; } = new ObservableTask();
    public ObservableTask Task3 { get; } = new ObservableTask();

    public ICommand Execute1AsyncCommand { get; }
    public ICommand Execute2AsyncCommand { get; }
    public ICommand Execute3AsyncCommand { get; }

    public Caller()
    {
        // Command with callers CanExecute method and error handled by callers method.
        Execute1AsyncCommand = new AsyncRelayCommand(Execute1Async, CanExecuteAsMethod, Execute1ErrorHandler);

        // Command with callers CanExecute parameter and error handled inside task therefore not needed.
        Execute2AsyncCommand = new AsyncRelayCommand(Execute2Async, _=>CanExecuteAsParam);

        // Some other, independent command.
        // Minimum example - CanExecute is evaluated inside command, error handled inside task.
        Execute3AsyncCommand = new AsyncRelayCommand(Execute3Async);
    }

    public bool CanExecuteAsParam => !(Task1.Running || Task2.Running);
    private bool CanExecuteAsMethod(object o)
    {
        return !(Task1.Running || Task2.Running);
    }

    private async Task Execute1Async(object o)
    {
        Task1.Running = true;
        await Task.Run(() => { Thread.Sleep(2000); }).ConfigureAwait(true);
        Task1.RanToCompletion = true;
    }
    private void Execute1ErrorHandler(Exception e, object o)
    {
        Task1.Faulted = true;
    }

    private async Task Execute2Async(object o)
    {
        try
        {
            Task2.Running = true;
            await Task.Run(() => { Thread.Sleep(2000); }).ConfigureAwait(true);
            Task2.RanToCompletion = true;
        }
        catch (Exception e)
        {
            Task2.Faulted = true;
        }
    }

    private async Task Execute3Async(object o)
    {
        try
        {
            Task3.Running = true;
            await Task.Run(() => { Thread.Sleep(2000); }).ConfigureAwait(true);
            Task3.RanToCompletion = true;
        }
        catch (Exception e)
        {
            Task3.Faulted = true;
        }
    }
}

Vielen Dank an alle für die unschätzbare Hilfe!

Antworten

2 aepot Oct 03 2020 at 01:47

Ich habe eine gebrauchsfertige Lösung.

  • Regelmäßiger synchroner Delegat, somit kann er einfach ersetzen RelayCommand.
  • Delegat auf einem gepoolten Thread ausgeführt.
  • CanExecuteist , falsewährend Befehl ausgeführt wird , so wird es die Steuerung automatisch deaktivieren.

Implementierung

public interface IAsyncCommand : ICommand
{
    Task ExecuteAsync(object param);
}

public class AsyncRelayCommand : IAsyncCommand
{
    private bool _isExecuting;
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    private Dispatcher Dispatcher { get; }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public AsyncRelayCommand(Action<object> execute, Predicate<object> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
        Dispatcher = Application.Current.Dispatcher;
    }

    private void InvalidateRequerySuggested()
    {
        if (Dispatcher.CheckAccess())
            CommandManager.InvalidateRequerySuggested();
        else
            Dispatcher.Invoke(CommandManager.InvalidateRequerySuggested);
    }

    public bool CanExecute(object parameter) => !_isExecuting && (_canExecute == null || _canExecute(parameter));

    public async Task ExecuteAsync(object parameter)
    {
        if (CanExecute(parameter))
        {
            try
            {
                _isExecuting = true;
                InvalidateRequerySuggested();
                await Task.Run(() => _execute(parameter));
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            finally
            {
                _isExecuting = false;
                InvalidateRequerySuggested();
            }
        }
    }

    public void Execute(object parameter) => _ = ExecuteAsync(parameter);
}

Verwendung

private IAsyncCommand _myAsyncCommand;

public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(parameter =>
{
    Thread.Sleep(2000);
}));

Hinweis: Sie können nicht mit umgehen ObservableCollectionvon Nicht-UI - Thread, da Abhilfe , die ich dies vorschlagen ein .


Asynchrone Delegatenversion

public class AsyncRelayCommand : IAsyncCommand
{
    private bool _isExecuting;
    private readonly Func<object, Task> _executeAsync;
    private readonly Predicate<object> _canExecute;

    private Dispatcher Dispatcher { get; }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public AsyncRelayCommand(Func<object, Task> executeAsync, Predicate<object> canExecute = null)
    {
        _executeAsync = executeAsync;
        _canExecute = canExecute;
        Dispatcher = Application.Current.Dispatcher;
    }

    private void InvalidateRequerySuggested()
    {
        if (Dispatcher.CheckAccess())
            CommandManager.InvalidateRequerySuggested();
        else
            Dispatcher.Invoke(CommandManager.InvalidateRequerySuggested);
    }

    public bool CanExecute(object parameter) => !_isExecuting && (_canExecute == null || _canExecute(parameter));

    public async Task ExecuteAsync(object parameter)
    {
        if (CanExecute(parameter))
        {
            try
            {
                _isExecuting = true;
                InvalidateRequerySuggested();
                await _executeAsync(parameter);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
            finally
            {
                _isExecuting = false;
                InvalidateRequerySuggested();
            }
        }
    }

    public void Execute(object parameter) => _ = ExecuteAsync(parameter);
}

Verwendung

private IAsyncCommand _myAsyncCommand;

public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(async parameter =>
{
    await Task.Delay(2000);
}));
1 E.Shcherbo Oct 02 2020 at 20:08

Wenn Sie Callereine Methode CanExecutewie diese hatten:

 private bool CanExecute()
 {
     return SomeCondition && OtherCondition;
 }

Dann könnten Sie es natürlich AsyncRelayCommandals Instanz des Delegatentyps an Ihren übergeben Func<bool>, wenn Ihr AsyncRelayCommandKonstruktor den erforderlichen Parameter enthält:

    public AsyncRelayCommand(Func<object, Task> callback, Func<bool> canExecute, Action<Exception> onException = null)
    {
        this.callback = callback;
        this.onException = onException;
        this.canExecute = canExecute;
    }

Dann übergeben Sie es wie folgt an den Konstruktor:

MyAsyncCommand = new AsyncRelayCommand(ExecuteAsync, CanExecute, ErrorHandler);

Auf AsyncRelayCommanddiese Weise können Sie den canExecuteDelegaten aufrufen und erhalten die tatsächlichen Ergebnisse.

Oder Sie können CanExecutedie Eigenschaft verlassen, aber wenn Sie sie erstellen AsyncRelayCommand, wickeln Sie sie wie folgt in den Lambda-Ausdruck ein

MyAsyncCommand = new AsyncRelayCommand(ExecuteAsync, () => CanExecute, ErrorHandler);

Um die Fallback-Logik auf Ihr CanExecutefor anzuwenden, können AsyncRelayCommandSie den Code folgendermaßen ändern:

  • Func<bool>Lassen Sie eine Instanzvariable vom Typ beispielsweise sagen _canExecute. Weisen Sie es dann im Konstruktor mit dem Wert zu, der als Argument akzeptiert wird, Func<bool> canExecuteauch wenn dies der Fall ist null. public CanExecute(object param)Wenn Sie nur prüfen, ob dies der Fall _canExecuteist null, kehren !IsExecutingSie einfach so zurück, wie Sie es jetzt tun. Wenn dies nicht der Fall ist , geben Sie die nullRückgabe _canExecutezurück.