简体   繁体   English

带有可选 canExecute 的异步 ICommand 实现

[英]Asyncronous ICommand implementation with optional canExecute

I have following ICommand implementation, which works great, but I want to expand it so I can pass external canExecute parameter我有以下 ICommand 实现,效果很好,但我想扩展它,以便我可以传递外部 canExecute 参数

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;
        }
    }

Can this implementation be extended in a way so when caller's CanExecute() changes, both Execute1AsyncCommand and Execute2AsyncCommand will acknowledge that?能否以某种方式扩展此实现,以便当调用者的 CanExecute() 更改时, Execute1AsyncCommand 和 Execute2AsyncCommand 都会承认这一点? Here is my caller class:这是我的来电类:

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;
    }
}
}

In other callers I still want to be able to use AsyncRelayCommand() with just callback being mandatory.在其他调用者中,我仍然希望能够使用AsyncRelayCommand() ,而只是callback是强制性的。 In this case CanExecute should be evaluated internally from AsyncRelayCommand as in my original implementation.在这种情况下CanExecute应该从内部评估AsyncRelayCommand在我原来的执行。

For completeness, here is my view:为了完整起见,这是我的观点:

<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>

And ObservableTask class:和 ObservableTask 类:

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;
            }
        }
    }
}

What I want to achieve is after user press one button both become disabled until all tasks are done.我想要实现的是在用户按下一个按钮后,直到所有任务都完成为止。

Solution解决方案

I ended up with the following implementation which so far works as intended:我最终得到了以下实现,到目前为止它按预期工作:

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);
}

Usage:用法:

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;
        }
    }
}

Thank you everybody for invaluable help!感谢大家的宝贵帮助!

I have some ready-to-use solution.我有一些现成的解决方案。

  • Regular synchronous delegate, thus it can replace simple RelayCommand .常规同步委托,因此它可以代替简单的RelayCommand
  • Delegate executed on a pooled Thread.委托在池线程上执行。
  • CanExecute is false while command is executing, thus it will disable the control automatically.命令执行时CanExecutefalse ,因此会自动禁用控制。

Implementation执行

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);
}

Usage用法

private IAsyncCommand _myAsyncCommand;

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

Note: you can't deal with ObservableCollection from non-UI Thread, as workaround I suggest this one .注意:您无法处理ObservableCollection从非UI线程,作为解决办法,我建议这一个


Asynchronous delegate version异步委托版本

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);
}

Usage用法

private IAsyncCommand _myAsyncCommand;

public IAsyncCommand MyAsyncCommand => _myAsyncCommand ?? (_myAsyncCommand = new AsyncRelayCommand(async parameter =>
{
    await Task.Delay(2000);
}));

If your Caller had a method called CanExecute like this:如果您的Caller有一个名为CanExecute的方法,如下所示:

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

Then you would be able to pass it to your AsyncRelayCommand as an instance of delegate type Func<bool> , of course, if your AsyncRelayCommand defined constructor with the needed parameter:然后你就可以将它作为委托类型Func<bool>的实例传递给你的AsyncRelayCommand ,当然,如果你的AsyncRelayCommand定义了带有所需参数的构造函数:

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

Then you pass it to the constructor like this:然后将它传递给构造函数,如下所示:

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

Thus, your AsyncRelayCommand would be able to invoke canExecute delegate and will get the actual results.因此,您的AsyncRelayCommand将能够调用canExecute委托并获得实际结果。

Or you can leave CanExecute as the property, but when you create AsyncRelayCommand , wrap it to the lambda expression like this或者您可以将CanExecute作为属性保留,但是当您创建AsyncRelayCommand ,将其包装到这样的 lambda 表达式中

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

To apply fallback logic to your CanExecute for AsyncRelayCommand you can change the code in the following way:要将回退逻辑应用于CanExecuteAsyncRelayCommand您可以通过以下方式更改代码:

  • have an instance variable of type Func<bool> called, let's say, _canExecute .有一个Func<bool>类型的实例变量被调用,比方说, _canExecute Then assign it in the constructor with whatever value accepted as the argument Func<bool> canExecute even if it's null .然后在构造函数中为它分配任何接受作为参数Func<bool> canExecutenull即使它是null Then in your public CanExecute(object param) just check if _canExecute is null , just return !IsExecuting as you're doing it now, if it's not null , then return whatever _canExecute return.然后在您的public CanExecute(object param)只需检查_canExecute是否为null ,只需返回!IsExecuting就可以了,如果它不是null ,则返回_canExecute返回的任何_canExecute

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM