异步WPF命令

13

注意本问题中的代码是deSleeper的一部分,如果您需要完整的源代码,请访问该网站。

我希望命令可以支持异步操作,并具备以下特性:按下按钮后,在命令执行期间禁用按钮并在完成后重新启用;将实际工作委托给线程池工作项来执行;最后,提供处理异步处理期间发生的任何错误的方式。

我的解决方案是一个AsyncCommand:

public abstract class AsyncCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    public event EventHandler ExecutionStarting;
    public event EventHandler<AsyncCommandCompleteEventArgs> ExecutionComplete;

    public abstract string Text { get; }
    private bool _isExecuting;
    public bool IsExecuting
    {
        get { return _isExecuting; }
        private set
        {
            _isExecuting = value;
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }
    }

    protected abstract void OnExecute(object parameter);

    public void Execute(object parameter)
    {   
        try
        {
            IsExecuting = true;
            if (ExecutionStarting != null)
                ExecutionStarting(this, EventArgs.Empty);

            var dispatcher = Dispatcher.CurrentDispatcher;
            ThreadPool.QueueUserWorkItem(
                obj =>
                {
                    try
                    {
                        OnExecute(parameter);
                        if (ExecutionComplete != null)
                            dispatcher.Invoke(DispatcherPriority.Normal, 
                                ExecutionComplete, this, 
                                new AsyncCommandCompleteEventArgs(null));
                    }
                    catch (Exception ex)
                    {
                        if (ExecutionComplete != null)
                            dispatcher.Invoke(DispatcherPriority.Normal, 
                                ExecutionComplete, this, 
                                new AsyncCommandCompleteEventArgs(ex));
                    }
                    finally
                    {
                        dispatcher.Invoke(DispatcherPriority.Normal, 
                            new Action(() => IsExecuting = false));
                    }
                });
        }
        catch (Exception ex)
        {
            IsExecuting = false;
            if (ExecutionComplete != null)
                ExecutionComplete(this, new AsyncCommandCompleteEventArgs(ex));
        }
    }

    public virtual bool CanExecute(object parameter)
    {
        return !IsExecuting;
    }
}

所以问题是:这一切都必要吗?我注意到了内置的数据绑定异步支持,那为什么不支持命令执行呢?也许这与参数问题有关,这是我的下一个问题。


这里的一个问题是CanExecute的正常设计是具有特定于命令的逻辑的。例如,UndoCommand可能会检查是否存在UndoStack。而在这里,您唯一拥有的逻辑是它是否已经在执行。 - Orion Adrian
请注意,CanExecute是虚拟的。在这个模式中,我首先重写它并调用基类来提供特定于命令的逻辑。 - nedruod
2个回答

4

我已经能够将原始样本进行精细的修改,并为任何遇到类似情况的人提供一些建议。

首先,请考虑BackgroundWorker是否能满足需求。我仍然经常使用AsyncCommand来获得自动禁用功能,但如果许多事情可以使用BackgroundWorker完成。

但是通过包装BackgroundWorker,AsyncCommand提供了具有异步行为的命令式功能(我还在博客文章中谈到了这个主题)。

public abstract class AsyncCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    public event EventHandler RunWorkerStarting;
    public event RunWorkerCompletedEventHandler RunWorkerCompleted;

    public abstract string Text { get; }
    private bool _isExecuting;
    public bool IsExecuting
    {
        get { return _isExecuting; }
        private set
        {
            _isExecuting = value;
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }
    }

    protected abstract void OnExecute(object parameter);

    public void Execute(object parameter)
    {   
        try
        {   
            onRunWorkerStarting();

            var worker = new BackgroundWorker();
            worker.DoWork += ((sender, e) => OnExecute(e.Argument));
            worker.RunWorkerCompleted += ((sender, e) => onRunWorkerCompleted(e));
            worker.RunWorkerAsync(parameter);
        }
        catch (Exception ex)
        {
            onRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
        }
    }

    private void onRunWorkerStarting()
    {
        IsExecuting = true;
        if (RunWorkerStarting != null)
            RunWorkerStarting(this, EventArgs.Empty);
    }

    private void onRunWorkerCompleted(RunWorkerCompletedEventArgs e)
    {
        IsExecuting = false;
        if (RunWorkerCompleted != null)
            RunWorkerCompleted(this, e);
    }

    public virtual bool CanExecute(object parameter)
    {
        return !IsExecuting;
    }
}

使用 BackgroundWorker 类应该对您来说没有问题,但请注意,这个类最初是为 WinForms 设计和构建的。为什么不考虑使用 Task Library 呢? - user604613
一个好的更新建议... 然而答案相当简单... 当时任务库并不存在。 - nedruod
2
在TPL的情况下,我会用以下代码替换BackgroundWorker:Task.Factory.StartNew(() => OnExecute(parameter)).ContinueWith(task => OnExecuteCompleted(task.Exception), TaskScheduler.FromCurrentSynchronizationContext()); - Luke

1

正如我在你的另一个问题中回答的那样,你可能仍然希望同步绑定,然后异步启动命令。这样你就可以避免现在遇到的问题。


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