Skip to content

Commit 8ae1006

Browse files
committed
Add IAsyncRelayCommandExtensions.CreateCancelCommand
1 parent 3fc61fe commit 8ae1006

File tree

6 files changed

+160
-2
lines changed

6 files changed

+160
-2
lines changed

CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using CommunityToolkit.Mvvm.ComponentModel.__Internals;
11+
using CommunityToolkit.Mvvm.Input.Internals;
1112

1213
#pragma warning disable CS0618
1314

@@ -19,7 +20,7 @@ namespace CommunityToolkit.Mvvm.Input;
1920
/// action, and providing an <see cref="ExecutionTask"/> property that notifies changes when
2021
/// <see cref="ExecuteAsync"/> is invoked and when the returned <see cref="Task"/> completes.
2122
/// </summary>
22-
public sealed class AsyncRelayCommand : IAsyncRelayCommand
23+
public sealed class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand
2324
{
2425
/// <summary>
2526
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="ExecutionTask"/>.
@@ -252,6 +253,9 @@ static async void MonitorTask(AsyncRelayCommand @this, Task task)
252253
/// <inheritdoc/>
253254
public bool IsRunning => ExecutionTask is { IsCompleted: false };
254255

256+
/// <inheritdoc/>
257+
bool ICancellationAwareCommand.IsCancellationSupported => this.execute is null;
258+
255259
/// <inheritdoc/>
256260
public void NotifyCanExecuteChanged()
257261
{

CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using CommunityToolkit.Mvvm.ComponentModel.__Internals;
11+
using CommunityToolkit.Mvvm.Input.Internals;
1112

1213
#pragma warning disable CS0618
1314

@@ -17,7 +18,7 @@ namespace CommunityToolkit.Mvvm.Input;
1718
/// A generic command that provides a more specific version of <see cref="AsyncRelayCommand"/>.
1819
/// </summary>
1920
/// <typeparam name="T">The type of parameter being passed as input to the callbacks.</typeparam>
20-
public sealed class AsyncRelayCommand<T> : IAsyncRelayCommand<T>
21+
public sealed class AsyncRelayCommand<T> : IAsyncRelayCommand<T>, ICancellationAwareCommand
2122
{
2223
/// <summary>
2324
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute(T)"/> is used.
@@ -234,6 +235,9 @@ static async void MonitorTask(AsyncRelayCommand<T> @this, Task task)
234235
/// <inheritdoc/>
235236
public bool IsRunning => ExecutionTask is { IsCompleted: false };
236237

238+
/// <inheritdoc/>
239+
bool ICancellationAwareCommand.IsCancellationSupported => this.execute is null;
240+
237241
/// <inheritdoc/>
238242
public void NotifyCanExecuteChanged()
239243
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Windows.Input;
6+
using CommunityToolkit.Mvvm.Input.Internals;
7+
8+
namespace CommunityToolkit.Mvvm.Input;
9+
10+
/// <summary>
11+
/// Extensions for the <see cref="IAsyncRelayCommand"/> type.
12+
/// </summary>
13+
public static class IAsyncRelayCommandExtensions
14+
{
15+
/// <summary>
16+
/// Creates an <see cref="ICommand"/> instance that can be used to cancel execution on the input command.
17+
/// The returned command will also notify when it can be executed based on the state of the wrapped command.
18+
/// </summary>
19+
/// <param name="command">The input <see cref="IAsyncRelayCommand"/> instance to create a cancellation command for.</param>
20+
/// <returns>An <see cref="ICommand"/> instance that can be used to monitor and signal cancellation for <paramref name="command"/>.</returns>
21+
/// <remarks>The reeturned instance is not guaranteed to be unique across multiple invocations with the same arguments.</remarks>
22+
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="command"/> is <see langword="null"/>.</exception>
23+
public static ICommand CreateCancelCommand(this IAsyncRelayCommand command)
24+
{
25+
ArgumentNullException.ThrowIfNull(command);
26+
27+
// If the command is known not to ever allow cancellation, just reuse the same instance
28+
if (command is ICancellationAwareCommand { IsCancellationSupported: false })
29+
{
30+
return DisabledCommand.Instance;
31+
}
32+
33+
// Create a new cancel command wrapping the input one
34+
return new CancelCommand(command);
35+
}
36+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.ComponentModel;
7+
using System.Windows.Input;
8+
9+
namespace CommunityToolkit.Mvvm.Input.Internals;
10+
11+
/// <summary>
12+
/// A <see cref="ICommand"/> implementation wrapping <see cref="IAsyncRelayCommand"/> to support cancellation.
13+
/// </summary>
14+
internal sealed class CancelCommand : ICommand
15+
{
16+
/// <summary>
17+
/// The wrapped <see cref="IAsyncRelayCommand"/> instance.
18+
/// </summary>
19+
private readonly IAsyncRelayCommand command;
20+
21+
/// <summary>
22+
/// Creates a new <see cref="CancelCommand"/> instance.
23+
/// </summary>
24+
/// <param name="command">The <see cref="IAsyncRelayCommand"/> instance to wrap.</param>
25+
public CancelCommand(IAsyncRelayCommand command)
26+
{
27+
this.command = command;
28+
29+
this.command.PropertyChanged += OnPropertyChanged;
30+
}
31+
32+
/// <inheritdoc/>
33+
public event EventHandler? CanExecuteChanged;
34+
35+
/// <inheritdoc/>
36+
public bool CanExecute(object? parameter)
37+
{
38+
return this.command.CanBeCanceled;
39+
}
40+
41+
/// <inheritdoc/>
42+
public void Execute(object? parameter)
43+
{
44+
this.command.Cancel();
45+
}
46+
47+
/// <inheritdoc cref="PropertyChangedEventHandler"/>
48+
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
49+
{
50+
if (e.PropertyName is null or nameof(IAsyncRelayCommand.CanBeCanceled))
51+
{
52+
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
53+
}
54+
}
55+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Windows.Input;
7+
8+
namespace CommunityToolkit.Mvvm.Input.Internals;
9+
10+
/// <summary>
11+
/// A reusable <see cref="ICommand"/> instance that is always disabled.
12+
/// </summary>
13+
internal sealed class DisabledCommand : ICommand
14+
{
15+
/// <inheritdoc/>
16+
public event EventHandler? CanExecuteChanged
17+
{
18+
add { }
19+
remove { }
20+
}
21+
22+
/// <summary>
23+
/// Gets a shared, reusable <see cref="DisabledCommand"/> instance.
24+
/// </summary>
25+
/// <remarks>
26+
/// This instance can safely be used across multiple objects without having
27+
/// to worry about this static keeping others alive, as the event uses a
28+
/// custom accessor that just discards items (as the event is known to never
29+
/// be raised). As such, this instance will never act as root for other objects.
30+
/// </remarks>
31+
public static DisabledCommand Instance { get; } = new();
32+
33+
/// <inheritdoc/>
34+
public bool CanExecute(object? parameter)
35+
{
36+
return false;
37+
}
38+
39+
/// <inheritdoc/>
40+
public void Execute(object? parameter)
41+
{
42+
}
43+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace CommunityToolkit.Mvvm.Input.Internals;
6+
7+
/// <summary>
8+
/// An interface for commands that know whether they support cancellation or not.
9+
/// </summary>
10+
internal interface ICancellationAwareCommand
11+
{
12+
/// <summary>
13+
/// Gets whether or not the current command supports cancellation.
14+
/// </summary>
15+
bool IsCancellationSupported { get; }
16+
}

0 commit comments

Comments
 (0)