继电器命令可以执行和任务

12
我希望在调用继电器命令时启动一个任务,但是只要该任务正在运行,我希望禁用按钮。以下是一个示例:
private ICommand update;
public ICommand Update
        {
            get
            {
                if (update == null)
                {
                    update = new RelayCommand(
                        param => Task.Factory.StartNew(()=> StartUpdate()),
                        param => true); //true means the button will always be enabled
                }
                return update;
            }
        }

如何最好地检查任务是否正在运行?

以下是我的解决方案,但不确定它是否是最佳方法。

class Vm : ObservableObject 
    {

        Task T;
        public Vm()
        {
            T = new Task(() => doStuff());
        }

        private ICommand myCommand;
        public ICommand MyCommand
        {
            get { return myCommand ?? (myCommand = new RelayCommand( p => { T = new Task(() => doStuff()); T.Start(); }, p => T.Status != TaskStatus.Running)); }
        }


        private void doStuff()
        {
            System.Threading.Thread.Sleep(5000);
        }

    }

更新:这里的每个答案都可以正常工作,但它们仍然不同意彼此,我刚刚达到了100声望,每当我达到100时,我就会开始一个赏金,因此我正在寻找一种在.NET 4.0中以任务为基础执行的最佳非内存泄漏异步RelayCommand实现。


@MerickOWA,使用Task.IsCompleted无法正常工作,使用T.Status != TaskStatus.Running是最佳选择。 - FPGA
5个回答

24

我强烈建议您避免使用new Task以及Task.Factory.StartNew。在后台线程上启动异步任务的正确方法是使用Task.Run

您可以使用这种模式轻松创建异步RelayCommand

private bool updateInProgress;
private ICommand update;
public ICommand Update
{
  get
  {
    if (update == null)
    {
      update = new RelayCommand(
          async () =>
          {
            updateInProgress = true;
            Update.RaiseCanExecuteChanged();

            await Task.Run(() => StartUpdate());

            updateInProgress = false;
            Update.RaiseCanExecuteChanged();
          },
          () => !updateInProgress);
    }
    return update;
  }
}

当您已经指定了异步 lambda 时,为什么还要在 Task.Run 上使用 await?如果 RelayCommand 接受异步 lambda 并执行异步操作,那么 Task.Run 的目的是什么呢? - Akash Kava
1
RelayCommand 不接受 async lambda(即 Func<Task>);lambda 存在是为了将 async 代码包装成 async void(即 Action)。Task.Run 只存在是因为原始代码也在后台线程上运行 StartUpdate;在理想情况下(完全使用 async),Task.Run 将不是必需的。 - Stephen Cleary
那么标记为异步的必要性是什么?你不能通过将Raise Event包装在Dispatcher的BeginInvoke中,在Task.Run内部调用Raise Event吗? - Akash Kava
除了直接使用“Dispatcher”是代码异味之外,我认为它不会按照您的想法工作。您需要在某个地方拥有一个“async void”方法。 - Stephen Cleary

8
我认为,您可以使用这个AsyncCommand的实现。
public class AsyncCommand : ICommand, IDisposable
{
    private readonly BackgroundWorker _backgroundWorker = new BackgroundWorker {WorkerSupportsCancellation = true};
    private readonly Func<bool> _canExecute;

    public AsyncCommand(Action action, Func<bool> canExecute = null, Action<object> completed = null,
                        Action<Exception> error = null)
    {
        _backgroundWorker.DoWork += (s, e) =>
            {
                CommandManager.InvalidateRequerySuggested();
                action();
            };

        _backgroundWorker.RunWorkerCompleted += (s, e) =>
            {
                if (completed != null && e.Error == null)
                    completed(e.Result);

                if (error != null && e.Error != null)
                    error(e.Error);

                CommandManager.InvalidateRequerySuggested();
            };

        _canExecute = canExecute;
    }

    public void Cancel()
    {
        if (_backgroundWorker.IsBusy)
            _backgroundWorker.CancelAsync();
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute == null
                   ? !_backgroundWorker.IsBusy
                   : !_backgroundWorker.IsBusy && _canExecute();
    }

    public void Execute(object parameter)
    {
        _backgroundWorker.RunWorkerAsync();
    }

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

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_backgroundWorker != null)
                _backgroundWorker.Dispose();
        }
    }
}

BackgroundWorker会调度到UI线程还是启动它的任何线程?您需要调度回UI线程吗? - Tom Padilla

3

您使用RelayCommand的解决方案几乎可行。问题在于任务完成后,UI不会立即更新。这是因为需要触发ICommand的CanExecuteChanged事件才能使UI正确更新。

解决此问题的一种方法是创建一种新的ICommand。例如:

  class AsyncRelayCommand : ICommand
  {
    private Func<object, Task> _action;
    private Task _task;

    public AsyncRelayCommand(Func<object,Task> action)
    {
      _action = action;
    }

    public bool CanExecute(object parameter)
    {
      return _task == null || _task.IsCompleted;
    }

    public event EventHandler CanExecuteChanged;

    public async void Execute(object parameter)
    {
      _task = _action(parameter);
      OnCanExecuteChanged();
      await _task;
      OnCanExecuteChanged();
    }

    private void OnCanExecuteChanged()
    {
      var handler = this.CanExecuteChanged;
      if (handler != null)
        handler(this, EventArgs.Empty);
    }
  }

现在您的视图模型可以执行以下操作:
private ICommand myCommand;
public ICommand MyCommand
{
  get { return myCommand ?? (myCommand = new AsyncRelayCommand(p => Task.Factory.StartNew(doStuff))); }
}

private void doStuff()
{
  System.Threading.Thread.Sleep(5000);
}

或者您可以将doStuff函数设置为“async”函数,如下所示

private ICommand myCommand2;
public ICommand MyCommand2
{
  get { return myCommand2 ?? (myCommand2 = new AsyncRelayCommand(p => doStuff2())); }
}
private async Task doStuff2()
{
  await Task.Delay(5000);
}

两个问题:1)StartNew是危险的(正如我在我的博客中所解释的那样),2)你的CanExecuteChanged实现会泄漏内存 - Stephen Cleary
@StephenCleary 1) 我的AsyncRelayCommand实现并没有使用Task.StartNew,我只是在适应给定的示例代码,使其尽可能易于理解。2) 根据我的阅读,这个问题已经在4.5中得到了修复,并且重点是更改控件或ICommandSource,而不是更改ICommand的实现。 - MerickOWA

0
你可以定义一个静态变量IsRunning,在任务开始时将其设置为True,在任务完成时将其设置为false,并将该启用按钮绑定到IsRunning的状态。

我已经做过了,但是没有额外变量的方法吗? - FPGA
嗯,有些人需要了解某些事情...否则你该怎么做呢? :) - Noctis
也许通过将任务设为全局,可以解决问题?但这会很麻烦,因为我有很多命令,并且需要在相关任务运行时禁用按钮。 - FPGA
静态标志只应在最简单的情况下使用,当事情变得更加复杂时,这种方法很快就会崩溃。 - slugster
@slugster听起来对我来说是一个简单的情景。否则,你在建议什么?看起来我得到了一个反对票,而且OP从未得到过其他选择... - Noctis

0

我正在尝试避免使用Prism库,以使我的控件在引用程序集的数量方面尽可能简单,并最终采用了这种解决方案

_cmd = new RelayCommand(async delegate
{
   await Task.Run(() => <YourMethod>());
}, delegate { return !IsInProgress; }) );

看起来工作得很好(如果您不需要传递commandParameter)。不幸的是,这仍然是一个问题。

RelayCommand类继承自ICommand。

public class RelayCommand : ICommand
{
    private Action<object> _execute;

    private Predicate<object> _canExecute;

    private event EventHandler CanExecuteChangedInternal;

    public RelayCommand(Action<object> execute)
        : this(execute, DefaultCanExecute)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
        {
            throw new ArgumentNullException("execute");
        }

        if (canExecute == null)
        {
            throw new ArgumentNullException("canExecute");
        }

        _execute = execute;
        _canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            CanExecuteChangedInternal += value;
        }

        remove
        {
            CommandManager.RequerySuggested -= value;
            CanExecuteChangedInternal -= value;
        }
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute != null && _canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    public void OnCanExecuteChanged()
    {
        EventHandler handler = CanExecuteChangedInternal;
        if (handler != null)
        {
            //DispatcherHelper.BeginInvokeOnUIThread(() => handler.Invoke(this, EventArgs.Empty));
            handler.Invoke(this, EventArgs.Empty);
        }
    }

    public void Destroy()
    {
        _canExecute = _ => false;
        _execute = _ => { return; };
    }

    private static bool DefaultCanExecute(object parameter)
    {
        return true;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接