2015-04-28 16 views
9

Tôi đã làm rất nhiều Googling, và thậm chí Bing-ing và tôi đã không đưa ra bất cứ điều gì thỏa mãn.Thử nghiệm một async ICommand

Tôi có một ViewModel có một số lệnh, chẳng hạn như SaveCommand và NewCommand và DeleteCommand. SaveCommand của tôi thực hiện thao tác lưu vào một tệp, mà tôi muốn nó hoạt động Async để giao diện người dùng không chờ đợi nó.

SaveCommand của tôi là phiên bản AsyncCommand, cài đặt ICommand.

SaveCommand = new AsyncCommand(
    async param => 
     { 
      Connection con = await Connection.GetInstanceAsync(m_configurationPath); 
      con.Shoppe.Configurations = new List<CouchDbConfig>(m_configurations); 
      await con.SaveConfigurationAsync(m_configurationPath); 
      //now that its saved, we reload the Data. 
      await LoadDataAsync(m_configurationPath); 
     }, 
...etc 

Bây giờ tôi đang tạo thử nghiệm cho ViewModel của mình. Trong đó, tôi tạo ra một điều mới với NewCommand và sau đó tôi sửa đổi nó, và sau đó sử dụng SaveCommand.

vm.SaveCommand.Execute(null); 
Assert.IsFalse(vm.SaveCommand.CanExecute(null)); 

Phương thức CanExecute của tôi (không được hiển thị) của SaveCommand sẽ trả về Sai ngay sau khi mục đã được Lưu. (không có điểm tiết kiệm một mục không thay đổi). Tuy nhiên, Assert hiển thị ở trên thất bại tất cả các thời gian vì tôi không chờ đợi cho SaveCommand để hoàn thành thực hiện.

Bây giờ, tôi không thể chờ nó hoàn thành việc thực thi vì tôi không thể. ICommand.Execute không trả về một nhiệm vụ. Và nếu tôi thay đổi AsyncCommand để có Execute trả về một Task thì nó sẽ không thực hiện đúng giao diện ICommand.

Vì vậy, điều duy nhất tôi nghĩ rằng tôi có thể làm bây giờ, cho mục đích thử nghiệm, là cho AsynCommand để có một chức năng mới,

public async Task ExecuteAsync(object parm) { ... } 

Và do đó, thử nghiệm của tôi sẽ chạy (và chờ đợi) các ExecuteAsync và giao diện XAML sẽ chạy phương thức ICommand.Execute, trong đó nó không chờ đợi.

Tôi không cảm thấy hạnh phúc khi thực hiện phương pháp giải pháp được đề xuất như tôi nghĩ, và hy vọng, và mong rằng có một cách tốt hơn.

Là những gì tôi đề xuất, hợp lý? Có cách nào tốt hơn?

+0

Bạn có bất cứ tài sản trong lệnh của bạn nói rằng "Chạy" hoặc "Thi công"? Có thể một cách là thêm cờ Thi hành vào lệnh. Cũng có thể sửa chữa tại sao thử nghiệm không thành công, bạn có thể sử dụng cờ Thực thi trong CanExecute để đảm bảo rằng chúng không chạy lệnh hai lần. –

+0

Tôi hiểu ý của bạn là gì. Hãy để tôi thử điều đó. * backsoon –

+0

Trên thực tế, AsyncCommand đã có cờ Thi hành. Khi tôi tìm thấy vấn đề ban đầu, TBH, tôi đã không sử dụng AsyncCommand, thay vào đó một thứ khác thì không. Có lẽ tôi không thực sự có một vấn đề bây giờ! .. kiểm tra –

Trả lời

2

Có vẻ như câu trả lời đang sử dụng cờ với đối tượng AsyncCommand. Sử dụng cờ Executing của AsyncCommand trong phương thức CanExecute sẽ đảm bảo rằng người dùng không thể thực thi lệnh trong khi một phiên bản khác đang chạy.

Ngoài ra với đơn vị kiểm tra, bạn có thể làm cho nó chờ đợi sau khi khẳng định bằng cách sử dụng một vòng lặp while:

while (vm.SaveCommand.Executing) ; 

Vì vậy mà các thử nghiệm thoát sạch.

+0

Vì vậy, bạn có nghĩa là điều này là sạch hơn so với thêm một phương pháp mà trả về một 'Task'? –

+0

Có, nếu nó có nghĩa là 'Task' chỉ được sử dụng cho mục đích thử nghiệm. Tôi sẽ tránh viết mã bổ sung để vượt qua một bài kiểm tra, vì nó loại đánh bại mục đích của một bài kiểm tra. –

+0

Tôi nghĩ nó sạch hơn. Vì không có hai phương thức Thực thi khó hiểu. –

11

gì bạn đề nghị là hợp lý, và là chính xác những gì AsyncCommand implementation created by Stephen Cleary làm (ông là một trong những quan trọng nhất experts on the subject of async đang IMHO)

Đây là một thực hiện đầy đủ các mã từ bài viết (cộng với một vài tinh chỉnh tôi đã thực hiện đối với trường hợp sử dụng tôi đã sử dụng.)

AsyncCommand.cs

/* 
* Based on the article: Patterns for Asynchronous MVVM Applications: Commands 
* http://msdn.microsoft.com/en-us/magazine/dn630647.aspx 
* 
* Modified by Scott Chamberlain 11-19-2014 
* - Added parameter support 
* - Added the ability to shut off the single invocation restriction. 
* - Made a non-generic version of the class that called the generic version with a <object> return type. 
*/ 
using System; 
using System.ComponentModel; 
using System.Runtime.CompilerServices; 
using System.Threading; 
using System.Threading.Tasks; 
using System.Windows.Input; 

namespace Infrastructure 
{ 
    public class AsyncCommand : AsyncCommand<object> 
    { 
     public AsyncCommand(Func<object, Task> command) 
      : base(async (parmater, token) => { await command(parmater); return null; }, null) 
     { 
     } 

     public AsyncCommand(Func<object, Task> command, Func<object, bool> canExecute) 
      : base(async (parmater, token) => { await command(parmater); return null; }, canExecute) 
     { 
     } 

     public AsyncCommand(Func<object, CancellationToken, Task> command) 
      : base(async (parmater, token) => { await command(parmater, token); return null; }, null) 
     { 
     } 

     public AsyncCommand(Func<object, CancellationToken, Task> command, Func<object, bool> canExecute) 
      : base(async (parmater, token) => { await command(parmater, token); return null; }, canExecute) 
     { 
     } 
    } 

    public class AsyncCommand<TResult> : AsyncCommandBase, INotifyPropertyChanged 
    { 
     private readonly Func<object, CancellationToken, Task<TResult>> _command; 
     private readonly CancelAsyncCommand _cancelCommand; 
     private readonly Func<object, bool> _canExecute; 
     private NotifyTaskCompletion<TResult> _execution; 
     private bool _allowMultipleInvocations; 

     public AsyncCommand(Func<object, Task<TResult>> command) 
      : this((parmater, token) => command(parmater), null) 
     { 
     } 

     public AsyncCommand(Func<object, Task<TResult>> command, Func<object, bool> canExecute) 
      : this((parmater, token) => command(parmater), canExecute) 
     { 
     } 

     public AsyncCommand(Func<object, CancellationToken, Task<TResult>> command) 
      : this(command, null) 
     { 
     } 

     public AsyncCommand(Func<object, CancellationToken, Task<TResult>> command, Func<object, bool> canExecute) 
     { 
      _command = command; 
      _canExecute = canExecute; 
      _cancelCommand = new CancelAsyncCommand(); 
     } 


     public override bool CanExecute(object parameter) 
     { 
      var canExecute = _canExecute == null || _canExecute(parameter); 
      var executionComplete = (Execution == null || Execution.IsCompleted); 

      return canExecute && (AllowMultipleInvocations || executionComplete); 
     } 

     public override async Task ExecuteAsync(object parameter) 
     { 
      _cancelCommand.NotifyCommandStarting(); 
      Execution = new NotifyTaskCompletion<TResult>(_command(parameter, _cancelCommand.Token)); 
      RaiseCanExecuteChanged(); 
      await Execution.TaskCompletion; 
      _cancelCommand.NotifyCommandFinished(); 
      RaiseCanExecuteChanged(); 
     } 

     public bool AllowMultipleInvocations 
     { 
      get { return _allowMultipleInvocations; } 
      set 
      { 
       if (_allowMultipleInvocations == value) 
        return; 

       _allowMultipleInvocations = value; 
       OnPropertyChanged(); 
      } 
     } 

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

AsyncCommandBase.cs

/* 
* Based on the article: Patterns for Asynchronous MVVM Applications: Commands 
* http://msdn.microsoft.com/en-us/magazine/dn630647.aspx 
*/ 
using System; 
using System.Threading.Tasks; 
using System.Windows.Input; 

namespace Infrastructure 
{ 
    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(); 
     } 
    } 
} 

NotifyTaskCompletion.cs

/* 
* Based on the article: Patterns for Asynchronous MVVM Applications: Commands 
* http://msdn.microsoft.com/en-us/magazine/dn630647.aspx 
* 
* Modifed by Scott Chamberlain on 12/03/2014 
* Split in to two classes, one that does not return a result and a 
* derived class that does. 
*/ 

using System; 
using System.ComponentModel; 
using System.Threading.Tasks; 

namespace Infrastructure 
{ 
    public sealed class NotifyTaskCompletion<TResult> : NotifyTaskCompletion 
    { 
     public NotifyTaskCompletion(Task<TResult> task) 
      : base(task) 
     { 
     } 

     public TResult Result 
     { 
      get 
      { 
       return (Task.Status == TaskStatus.RanToCompletion) ? 
        ((Task<TResult>)Task).Result : default(TResult); 
      } 
     } 
    } 

    public class NotifyTaskCompletion : INotifyPropertyChanged 
    { 
     public NotifyTaskCompletion(Task task) 
     { 
      Task = task; 
      if (!task.IsCompleted) 
       TaskCompletion = WatchTaskAsync(task); 
      else 
       TaskCompletion = Task; 
     } 

     private async Task WatchTaskAsync(Task task) 
     { 
      try 
      { 
       await task; 
      } 
      catch 
      { 
       //This catch is intentionally empty, the errors will be handled lower on the "task.IsFaulted" branch. 
      } 
      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 Task { get; private set; } 
     public Task TaskCompletion { get; private set; } 
     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; 
    } 
} 
+0

Wowsers. Điều đó dường như làm mọi thứ kể cả bồn rửa nhà bếp! Tôi đồng ý rằng đó là một thực hiện dường như rất hoàn chỉnh bao gồm hủy bỏ, vv Bây giờ, câu hỏi của tôi là, câu trả lời nào tôi đánh dấu đúng? Đối với các nhu cầu trước mắt của tôi, và có lẽ tất cả những gì tôi cần trong một thời gian là vòng lặp while trong trường hợp thử nghiệm của tôi. Nhưng tôi hiểu câu trả lời này là hoàn chỉnh nhất và cuối cùng giải quyết vấn đề trước mắt của tôi và có lẽ tất cả những gì tôi sẽ có trong tương lai. –

Các vấn đề liên quan