Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/Nito.Mvvm.Async/AsyncCommandT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows.Input;

namespace Nito.Mvvm
{
/// <summary>
/// A strongly typed parameterized asynchronous command, which forwards it's implementation to <see cref="AsyncCommand"/>.
/// </summary>
public sealed class AsyncCommand<T> : AsyncCommandBase, IAsyncCommand<T>, INotifyPropertyChanged
{
private readonly AsyncCommand _asyncCommand;

/// <summary>
/// Creates a new asynchronous command, with the specified asynchronous delegate as its implementation.
/// </summary>
/// <param name="executeAsync">The implementation of <see cref="IAsyncCommand{T}.ExecuteAsync(T)"/>.</param>
/// <param name="canExecuteChangedFactory">The factory for the implementation of <see cref="ICommand.CanExecuteChanged"/>.</param>
public AsyncCommand(Func<T, Task> executeAsync, Func<object, ICanExecuteChanged> canExecuteChangedFactory)
: base(canExecuteChangedFactory)
{
_asyncCommand = new AsyncCommand(async parameter => await executeAsync((T)parameter), canExecuteChangedFactory);
_asyncCommand.PropertyChanged += (object sender, PropertyChangedEventArgs e) =>
{
switch (e.PropertyName)
{
case nameof(_asyncCommand.IsExecuting):
PropertyChanged?.Invoke(this, PropertyChangedEventArgsCache.Instance.Get(nameof(IsExecuting)));
break;
case nameof(_asyncCommand.Execution):
PropertyChanged?.Invoke(this, PropertyChangedEventArgsCache.Instance.Get(nameof(Execution)));
break;
}
};
((ICommand)_asyncCommand).CanExecuteChanged += (object sender, EventArgs e) => OnCanExecuteChanged();
}

/// <summary>
/// Creates a new asynchronous command, with the specified asynchronous delegate as its implementation.
/// </summary>
/// <param name="executeAsync">The implementation of <see cref="IAsyncCommand{T}.ExecuteAsync(T)"/>.</param>
public AsyncCommand(Func<T, Task> executeAsync)
: this(executeAsync, CanExecuteChangedFactories.DefaultCanExecuteChangedFactory)
{
}

/// <summary>
/// Raised when any properties on this instance have changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// Represents the most recent execution of the asynchronous command. Returns <c>null</c> until the first execution of this command.
/// </summary>
public NotifyTask Execution => _asyncCommand.Execution;

/// <summary>
/// Whether the asynchronous command is currently executing.
/// </summary>
public bool IsExecuting => _asyncCommand.IsExecuting;


/// <summary>
/// The implementation of <see cref="ICommand.CanExecute(object)"/>. Returns <c>false</c> whenever the async command is in progress.
/// </summary>
/// <param name="parameter">The parameter for the command.</param>
protected override bool CanExecute(object parameter) => ((ICommand)_asyncCommand).CanExecute(parameter);

/// <summary>
/// Executes the command asynchronously.
/// </summary>
/// <param name="parameter">The parameter for the command.</param>
public override Task ExecuteAsync(object parameter)
{
return ExecuteAsync((T)parameter);
}

/// <summary>
/// Executes the command asynchronously.
/// </summary>
/// <param name="parameter">The parameter for the command.</param>
public Task ExecuteAsync(T parameter)
{
return _asyncCommand.ExecuteAsync(parameter);
}
}
}
16 changes: 16 additions & 0 deletions src/Nito.Mvvm.Async/IAsyncCommandT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Threading.Tasks;

namespace Nito.Mvvm
{
/// <summary>
/// A strongly typed async version of <see cref="IAsyncCommand"/>.
/// </summary>
public interface IAsyncCommand<T> : IAsyncCommand
{
/// <summary>
/// Executes the asynchronous command.
/// </summary>
/// <param name="parameter">The parameter for the command.</param>
Task ExecuteAsync(T parameter);
}
}
99 changes: 99 additions & 0 deletions test/UnitTests/AsyncCommandTUnitTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Nito.Mvvm;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Xunit;

namespace UnitTests
{
public class AsyncCommandTUnitTests
{
[Fact]
public void AfterConstruction_IsNotExecuting()
{
var command = new AsyncCommand<string>(_ => Task.CompletedTask);
Assert.False(command.IsExecuting);
Assert.Null(command.Execution);
Assert.True(((ICommand)command).CanExecute(""));
}

[Fact]
public void AfterSynchronousExecutionComplete_IsNotExecuting()
{
var command = new AsyncCommand<int>(_ => Task.CompletedTask);

((ICommand)command).Execute(0);

Assert.False(command.IsExecuting);
Assert.NotNull(command.Execution);
Assert.True(((ICommand)command).CanExecute(0));
}

[Fact]
public async Task ExecuteWithWrongParameterType_ThrowsCastException()
{
var command = new AsyncCommand<bool>(_ => Task.CompletedTask);

await Assert.ThrowsAsync<InvalidCastException>(() =>
command.ExecuteAsync("A STRING")
);
}

[Fact]
public void StartExecution_IsExecuting()
{
var signal = new TaskCompletionSource<object>();
var command = new AsyncCommand<object>(_ => signal.Task);

((ICommand)command).Execute(null);

Assert.True(command.IsExecuting);
Assert.NotNull(command.Execution);
Assert.False(((ICommand)command).CanExecute(null));

signal.SetResult(null);
}

[Fact]
public void Execute_DelaysExecutionUntilCommandIsInExecutingState()
{
bool isExecuting = false;
NotifyTask execution = null;
bool canExecute = true;

AsyncCommand<int> command = null;
command = new AsyncCommand<int>((intParam) =>
{
isExecuting = command.IsExecuting;
execution = command.Execution;
canExecute = ((ICommand)command).CanExecute(intParam);
return Task.CompletedTask;
});

((ICommand)command).Execute(0);

Assert.True(isExecuting);
Assert.NotNull(execution);
Assert.False(canExecute);
}

[Fact]
public void StartExecution_NotifiesPropertyChanges()
{
var signal = new TaskCompletionSource<object>();
var command = new AsyncCommand<object>(_ => signal.Task);
var isExecutingNotification = TestUtils.PropertyNotified(command, n => n.IsExecuting);
var executionNotification = TestUtils.PropertyNotified(command, n => n.Execution);
var sawCanExecuteChanged = false;
((ICommand)command).CanExecuteChanged += (_, __) => { sawCanExecuteChanged = true; };

((ICommand)command).Execute(null);

Assert.True(isExecutingNotification());
Assert.True(executionNotification());
Assert.True(sawCanExecuteChanged);

signal.SetResult(null);
}
}
}