在WPF中,有时有些重操作,需要异步完成,来避免Ui锁死。
MVVM模式下,可以通过异步Command来实现此过程。
首先定义一个IAsyncCommand ,实现ICommand:
public interface IAsyncCommand : ICommand { Task ExecuteAsync(object parameter); }
然后实现一个实现该接口的抽象Base:
public abstract class AsyncCommandBase : IAsyncCommand { public abstract bool CanExecute(object parameter); public abstract Task ExecuteAsync(object parameter); public async void Execute(object parameter) { await ExecuteAsync(parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } protected void RaiseCanExecuteChanged() { CommandManager.InvalidateRequerySuggested(); } }
再实现上述抽象类:
public class AsyncCommand<TResult> : AsyncCommandBase, INotifyPropertyChanged { private readonly Func<CancellationToken, Task<TResult>> _command; private readonly CancelAsyncCommand _cancelCommand; private NotifyTaskCompletion<TResult> _execution; public AsyncCommand(Func<CancellationToken, Task<TResult>> command) { _command = command; _cancelCommand = new CancelAsyncCommand(); } public override bool CanExecute(object parameter) { return Execution == null || Execution.IsCompleted; } public override async Task ExecuteAsync(object parameter) { _cancelCommand.NotifyCommandStarting(); Execution = new NotifyTaskCompletion<TResult>(_command(_cancelCommand.Token)); RaiseCanExecuteChanged(); await Execution.TaskCompletion; _cancelCommand.NotifyCommandFinished(); RaiseCanExecuteChanged(); } public ICommand CancelCommand { get { return _cancelCommand; } } public NotifyTaskCompletion<TResult> Execution { get { return _execution; } private set { _execution = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } private sealed class CancelAsyncCommand : ICommand { private CancellationTokenSource _cts = new CancellationTokenSource(); private bool _commandExecuting; public CancellationToken Token { get { return _cts.Token; } } public void NotifyCommandStarting() { _commandExecuting = true; if (!_cts.IsCancellationRequested) return; _cts = new CancellationTokenSource(); RaiseCanExecuteChanged(); } public void NotifyCommandFinished() { _commandExecuting = false; RaiseCanExecuteChanged(); } bool ICommand.CanExecute(object parameter) { return _commandExecuting && !_cts.IsCancellationRequested; } void ICommand.Execute(object parameter) { _cts.Cancel(); RaiseCanExecuteChanged(); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } private void RaiseCanExecuteChanged() { CommandManager.InvalidateRequerySuggested(); } } } public static class AsyncCommand { public static AsyncCommand<object> Create(Func<Task> command) { return new AsyncCommand<object>(async _ => { await command(); return null; }); } public static AsyncCommand<TResult> Create<TResult>(Func<Task<TResult>> command) { return new AsyncCommand<TResult>(_ => command()); } public static AsyncCommand<object> Create(Func<CancellationToken, Task> command) { return new AsyncCommand<object>(async token => { await command(token); return null; }); } public static AsyncCommand<TResult> Create<TResult>(Func<CancellationToken, Task<TResult>> command) { return new AsyncCommand<TResult>(command); } }
其中的NotifyTaskCompletion实现了任务结束通知:
public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged { public NotifyTaskCompletion(Task<TResult> task) { Task = task; TaskCompletion = WatchTaskAsync(task); } private async Task WatchTaskAsync(Task task) { try { await task; } catch { } var propertyChanged = PropertyChanged; if (propertyChanged == null) return; propertyChanged(this, new PropertyChangedEventArgs("Status")); propertyChanged(this, new PropertyChangedEventArgs("IsCompleted")); propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted")); if (task.IsCanceled) { propertyChanged(this, new PropertyChangedEventArgs("IsCanceled")); } else if (task.IsFaulted) { propertyChanged(this, new PropertyChangedEventArgs("IsFaulted")); propertyChanged(this, new PropertyChangedEventArgs("Exception")); propertyChanged(this, new PropertyChangedEventArgs("InnerException")); propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage")); } else { propertyChanged(this, new PropertyChangedEventArgs("IsSuccessfullyCompleted")); propertyChanged(this, new PropertyChangedEventArgs("Result")); } } public Task<TResult> Task { get; private set; } public Task TaskCompletion { get; private set; } public TResult Result { get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); } } public TaskStatus Status { get { return Task.Status; } } public bool IsCompleted { get { return Task.IsCompleted; } } public bool IsNotCompleted { get { return !Task.IsCompleted; } } public bool IsSuccessfullyCompleted { get { return Task.Status == TaskStatus.RanToCompletion; } } public bool IsCanceled { get { return Task.IsCanceled; } } public bool IsFaulted { get { return Task.IsFaulted; } } public AggregateException Exception { get { return Task.Exception; } } public Exception InnerException { get { return (Exception == null) ? null : Exception.InnerException; } } public string ErrorMessage { get { return (InnerException == null) ? null : InnerException.Message; } } public event PropertyChangedEventHandler PropertyChanged; }
假设为一个Button绑定一个IAsyncCommand,这个Button对应的命令就可以异步执行,而不锁死UI了。
※在原文章的代码中NotifyTaskCompletion实现有个bug,下载下来的代码是修复过的。千万别复制文章中的实现~