diff --git a/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
index 2207a2772..310001460 100644
--- a/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
+++ b/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
@@ -37,3 +37,4 @@ MVVMTK0027 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
MVVMTK0028 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
MVVMTK0029 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/error
MVVMTK0030 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/error
+MVVMTK0031 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
index 03059088f..64bcbe35d 100644
--- a/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
+++ b/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
@@ -191,17 +191,17 @@ internal static class DiagnosticDescriptors
///
/// Gets a indicating when RelayCommandAttribute.AllowConcurrentExecutions is being set for a non-asynchronous method.
///
- /// Format: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control setting, as it maps to a non-asynchronous command type".
+ /// Format: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control option, as it maps to a non-asynchronous command type".
///
///
public static readonly DiagnosticDescriptor InvalidConcurrentExecutionsParameterError = new DiagnosticDescriptor(
id: "MVVMTK0012",
- title: "Invalid concurrency control setting usage",
- messageFormat: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control setting, as it maps to a non-asynchronous command type",
+ title: "Invalid concurrency control option usage",
+ messageFormat: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a concurrency control option, as it maps to a non-asynchronous command type",
category: typeof(RelayCommandGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
- description: "Cannot apply the [RelayCommand] attribute specifying a concurrency control setting to methods mapping to non-asynchronous command types.",
+ description: "Cannot apply the [RelayCommand] attribute specifying a concurrency control option to methods mapping to non-asynchronous command types.",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
///
@@ -491,4 +491,20 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "Annotating a field with [NotifyDataErrorInfo] is not necessary if the containing type has or inherits [NotifyDataErrorInfo] at the class-level.",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
+
+ ///
+ /// Gets a indicating when RelayCommandAttribute.FlowExceptionsToTaskScheduler is being set for a non-asynchronous method.
+ ///
+ /// Format: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying an exception flow option, as it maps to a non-asynchronous command type".
+ ///
+ ///
+ public static readonly DiagnosticDescriptor InvalidFlowExceptionsToTaskSchedulerParameterError = new DiagnosticDescriptor(
+ id: "MVVMTK0031",
+ title: "Invalid task scheduler exception flow option usage",
+ messageFormat: "The method {0}.{1} cannot be annotated with the [RelayCommand] attribute specifying a task scheduler exception flow option, as it maps to a non-asynchronous command type",
+ category: typeof(RelayCommandGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Cannot apply the [RelayCommand] attribute specifying a task scheduler exception flow option to methods mapping to non-asynchronous command types.",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit");
}
diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
index fc8c859e9..f9240249f 100644
--- a/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
+++ b/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs
@@ -25,6 +25,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// The member name for the can execute check, if available.
/// The can execute expression type, if available.
/// Whether or not concurrent executions have been enabled.
+/// Whether or not exceptions should flow to the task scheduler.
/// Whether or not to also generate a cancel command.
internal sealed record CommandInfo(
string MethodName,
@@ -38,6 +39,7 @@ internal sealed record CommandInfo(
string? CanExecuteMemberName,
CanExecuteExpressionType? CanExecuteExpressionType,
bool AllowConcurrentExecutions,
+ bool FlowExceptionsToTaskScheduler,
bool IncludeCancelCommand)
{
///
@@ -59,6 +61,7 @@ protected override void AddToHashCode(ref HashCode hashCode, CommandInfo obj)
hashCode.Add(obj.CanExecuteMemberName);
hashCode.Add(obj.CanExecuteExpressionType);
hashCode.Add(obj.AllowConcurrentExecutions);
+ hashCode.Add(obj.FlowExceptionsToTaskScheduler);
hashCode.Add(obj.IncludeCancelCommand);
}
@@ -77,6 +80,7 @@ protected override bool AreEqual(CommandInfo x, CommandInfo y)
x.CanExecuteMemberName == y.CanExecuteMemberName &&
x.CanExecuteExpressionType == y.CanExecuteExpressionType &&
x.AllowConcurrentExecutions == y.AllowConcurrentExecutions &&
+ x.FlowExceptionsToTaskScheduler == y.FlowExceptionsToTaskScheduler &&
x.IncludeCancelCommand == y.IncludeCancelCommand;
}
}
diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
index 5f60675ad..1a438dd03 100644
--- a/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
+++ b/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs
@@ -71,6 +71,17 @@ internal static class Execute
goto Failure;
}
+ // Check the switch to control exception flow
+ if (!TryGetFlowExceptionsToTaskSchedulerSwitch(
+ methodSymbol,
+ attributeData,
+ commandClassType,
+ builder,
+ out bool flowExceptionsToTaskScheduler))
+ {
+ goto Failure;
+ }
+
// Get the CanExecute expression type, if any
if (!TryGetCanExecuteExpressionType(
methodSymbol,
@@ -109,6 +120,7 @@ internal static class Execute
canExecuteMemberName,
canExecuteExpressionType,
allowConcurrentExecutions,
+ flowExceptionsToTaskScheduler,
generateCancelCommand);
Failure:
@@ -205,9 +217,37 @@ public static ImmutableArray GetSyntax(CommandInfo comm
}
// Enable concurrent executions, if requested
- if (commandInfo.AllowConcurrentExecutions)
+ if (commandInfo.AllowConcurrentExecutions && !commandInfo.FlowExceptionsToTaskScheduler)
+ {
+ commandCreationArguments.Add(
+ Argument(MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
+ IdentifierName("AllowConcurrentExecutions"))));
+ }
+ else if (commandInfo.FlowExceptionsToTaskScheduler && !commandInfo.AllowConcurrentExecutions)
{
- commandCreationArguments.Add(Argument(LiteralExpression(SyntaxKind.TrueLiteralExpression)));
+ // Enable exception flow, if requested
+ commandCreationArguments.Add(
+ Argument(MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
+ IdentifierName("FlowExceptionsToTaskScheduler"))));
+ }
+ else if (commandInfo.AllowConcurrentExecutions && commandInfo.FlowExceptionsToTaskScheduler)
+ {
+ // Enable both concurrency control and exception flow
+ commandCreationArguments.Add(
+ Argument(BinaryExpression(
+ SyntaxKind.BitwiseOrExpression,
+ MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
+ IdentifierName("AllowConcurrentExecutions")),
+ MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ IdentifierName("global::CommunityToolkit.Mvvm.Input.AsyncRelayCommandOptions"),
+ IdentifierName("FlowExceptionsToTaskScheduler")))));
}
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
@@ -555,6 +595,43 @@ private static bool TryGetAllowConcurrentExecutionsSwitch(
}
}
+ ///
+ /// Checks whether or not the user has requested to configure the task scheduler exception flow option.
+ ///
+ /// The input instance to process.
+ /// The instance the method was annotated with.
+ /// The command class type name.
+ /// The current collection of gathered diagnostics.
+ /// Whether or not task scheduler exception flow have been enabled.
+ /// Whether or not a value for could be retrieved successfully.
+ private static bool TryGetFlowExceptionsToTaskSchedulerSwitch(
+ IMethodSymbol methodSymbol,
+ AttributeData attributeData,
+ string commandClassType,
+ ImmutableArray.Builder diagnostics,
+ out bool flowExceptionsToTaskScheduler)
+ {
+ // Try to get the custom switch for task scheduler exception flow (the default is false)
+ if (!attributeData.TryGetNamedArgument("FlowExceptionsToTaskScheduler", out flowExceptionsToTaskScheduler))
+ {
+ flowExceptionsToTaskScheduler = false;
+
+ return true;
+ }
+
+ // Just like with the concurrency control option, check that the target command type is asynchronous
+ if (commandClassType is "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand")
+ {
+ return true;
+ }
+ else
+ {
+ diagnostics.Add(InvalidFlowExceptionsToTaskSchedulerParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
+
+ return false;
+ }
+ }
+
///
/// Checks whether or not the user has requested to also generate a cancel command.
///
diff --git a/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs b/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs
index 20799aa2b..8b38c24e9 100644
--- a/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs
+++ b/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs
@@ -59,9 +59,9 @@ public sealed class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCo
private readonly Func? canExecute;
///
- /// Indicates whether or not concurrent executions of the command are allowed.
+ /// The options being set for the current command.
///
- private readonly bool allowConcurrentExecutions;
+ private readonly AsyncRelayCommandOptions options;
///
/// The instance to use to cancel .
@@ -91,14 +91,14 @@ public AsyncRelayCommand(Func execute)
/// Initializes a new instance of the class.
///
/// The execution logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// Thrown if is .
- public AsyncRelayCommand(Func execute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func execute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(execute);
this.execute = execute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
///
@@ -117,14 +117,14 @@ public AsyncRelayCommand(Func cancelableExecute)
/// Initializes a new instance of the class.
///
/// The cancelable execution logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// Thrown if is .
- public AsyncRelayCommand(Func cancelableExecute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func cancelableExecute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(cancelableExecute);
this.cancelableExecute = cancelableExecute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
///
@@ -147,16 +147,16 @@ public AsyncRelayCommand(Func execute, Func canExecute)
///
/// The execution logic.
/// The execution status logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// Thrown if or are .
- public AsyncRelayCommand(Func execute, Func canExecute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func execute, Func canExecute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(execute);
ArgumentNullException.ThrowIfNull(canExecute);
this.execute = execute;
this.canExecute = canExecute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
///
@@ -179,16 +179,16 @@ public AsyncRelayCommand(Func cancelableExecute, Func
/// The cancelable execution logic.
/// The execution status logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// Thrown if or are .
- public AsyncRelayCommand(Func cancelableExecute, Func canExecute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func cancelableExecute, Func canExecute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(cancelableExecute);
ArgumentNullException.ThrowIfNull(canExecute);
this.cancelableExecute = cancelableExecute;
this.canExecute = canExecute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
private Task? executionTask;
@@ -238,7 +238,7 @@ static async void MonitorTask(AsyncRelayCommand @this, Task task)
@this.PropertyChanged?.Invoke(@this, CanBeCanceledChangedEventArgs);
}
- if (!@this.allowConcurrentExecutions)
+ if ((@this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0)
{
@this.CanExecuteChanged?.Invoke(@this, EventArgs.Empty);
}
@@ -273,13 +273,20 @@ public bool CanExecute(object? parameter)
{
bool canExecute = this.canExecute?.Invoke() != false;
- return canExecute && (this.allowConcurrentExecutions || ExecutionTask is not { IsCompleted: false });
+ return canExecute && ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) != 0 || ExecutionTask is not { IsCompleted: false });
}
///
public void Execute(object? parameter)
{
- _ = ExecuteAsync(parameter);
+ Task executionTask = ExecuteAsync(parameter);
+
+ // If exceptions shouldn't flow to the task scheduler, await the resulting task. This is
+ // delegated to a separate method to keep this one more compact in case the option is set.
+ if ((this.options & AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler) == 0)
+ {
+ AwaitAndThrowIfFailed(executionTask);
+ }
}
///
@@ -304,7 +311,7 @@ public Task ExecuteAsync(object? parameter)
}
// If concurrent executions are disabled, notify the can execute change as well
- if (!this.allowConcurrentExecutions)
+ if ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0)
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
@@ -323,4 +330,24 @@ public void Cancel()
PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs);
}
}
+
+ ///
+ /// Awaits an input and throws an exception on the calling context, if the task fails.
+ ///
+ /// The input instance to await.
+ internal static async void AwaitAndThrowIfFailed(Task executionTask)
+ {
+ // Note: this method is purposefully an async void method awaiting the input task. This is done so that
+ // if an async relay command is invoked synchronously (ie. when Execute is called, eg. from a binding),
+ // exceptions in the wrapped delegate will not be ignored or just become visible through the ExecutionTask
+ // property, but will be rethrown in the original synchronization context by default. This makes the behavior
+ // more consistent with how normal commands work (where exceptions are also just normally propagated to the
+ // caller context), and avoids getting an app into an inconsistent state in case a method faults without
+ // other components being notified. It is also possible to not await this task and to instead ignore exceptions
+ // and then inspect them manually from the ExecutionTask property, by constructing an async command instance
+ // using the AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler option. That will cause this call to
+ // be skipped, and exceptions will just either normally be available through that property, or will otherwise
+ // flow to the static TaskScheduler.UnobservedTaskException event if otherwise unobserved (eg. for logging).
+ await executionTask;
+ }
}
diff --git a/CommunityToolkit.Mvvm/Input/AsyncRelayCommandOptions.cs b/CommunityToolkit.Mvvm/Input/AsyncRelayCommandOptions.cs
new file mode 100644
index 000000000..50e4393a2
--- /dev/null
+++ b/CommunityToolkit.Mvvm/Input/AsyncRelayCommandOptions.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace CommunityToolkit.Mvvm.Input;
+
+///
+/// Options to customize the behavior of and instances.
+///
+[Flags]
+public enum AsyncRelayCommandOptions
+{
+ ///
+ /// No option is specified. The and types will use their default behavior:
+ ///
+ /// Concurrent execution is disallowed: a command is disabled if there is a pending asynchronous execution running.
+ ///
+ ///
+ /// Exceptions are thrown on the calling context: calling will await the
+ /// returned for the operation, and propagate the exception on the calling context.
+ ///
+ /// This behavior is consistent with synchronous commands, where exceptions in behave the same.
+ ///
+ ///
+ ///
+ None = 0,
+
+ ///
+ /// Concurrent executions are allowed. This option makes it so that the same command can be invoked concurrently multiple times.
+ ///
+ /// Note that additional considerations should be taken into account in this case:
+ ///
+ /// If the command supports cancellation, previous invocations will automatically be canceled if a new one is started.
+ /// The property will always represent the operation that was started last.
+ ///
+ ///
+ ///
+ AllowConcurrentExecutions = 1 << 0,
+
+ ///
+ /// Exceptions are not thrown on the calling context, and are propagated to instead.
+ ///
+ /// This affects how calls to behave. When this option is used, if an operation fails, that exception will not
+ /// be rethrown on the calling context (as it is not awaited there). Instead, it will flow to .
+ ///
+ ///
+ /// This option enables more advanced scenarios, where the property can be used to inspect the state of an operation
+ /// that was queued. That is, even if the operation failed or was canceled, the details of that can be retrieved at a later time by accessing this property.
+ ///
+ ///
+ FlowExceptionsToTaskScheduler = 1 << 1
+}
diff --git a/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs b/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
index 792fc238d..d3bbd7b93 100644
--- a/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
+++ b/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
@@ -36,9 +36,9 @@ public sealed class AsyncRelayCommand : IAsyncRelayCommand, ICancellationA
private readonly Predicate? canExecute;
///
- /// Indicates whether or not concurrent executions of the command are allowed.
+ /// The options being set for the current command.
///
- private readonly bool allowConcurrentExecutions;
+ private readonly AsyncRelayCommandOptions options;
///
/// The instance to use to cancel .
@@ -68,15 +68,15 @@ public AsyncRelayCommand(Func execute)
/// Initializes a new instance of the class.
///
/// The execution logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// See notes in .
/// Thrown if is .
- public AsyncRelayCommand(Func execute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func execute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(execute);
this.execute = execute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
///
@@ -96,15 +96,15 @@ public AsyncRelayCommand(Func cancelableExecute)
/// Initializes a new instance of the class.
///
/// The cancelable execution logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// See notes in .
/// Thrown if is .
- public AsyncRelayCommand(Func cancelableExecute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func cancelableExecute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(cancelableExecute);
this.cancelableExecute = cancelableExecute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
///
@@ -128,17 +128,17 @@ public AsyncRelayCommand(Func execute, Predicate canExecute)
///
/// The execution logic.
/// The execution status logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// See notes in .
/// Thrown if or are .
- public AsyncRelayCommand(Func execute, Predicate canExecute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func execute, Predicate canExecute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(execute);
ArgumentNullException.ThrowIfNull(canExecute);
this.execute = execute;
this.canExecute = canExecute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
///
@@ -162,17 +162,17 @@ public AsyncRelayCommand(Func cancelableExecute, Pr
///
/// The cancelable execution logic.
/// The execution status logic.
- /// Whether or not to allow concurrent executions of the command.
+ /// The options to use to configure the async command.
/// See notes in .
/// Thrown if or are .
- public AsyncRelayCommand(Func cancelableExecute, Predicate canExecute, bool allowConcurrentExecutions)
+ public AsyncRelayCommand(Func cancelableExecute, Predicate canExecute, AsyncRelayCommandOptions options)
{
ArgumentNullException.ThrowIfNull(cancelableExecute);
ArgumentNullException.ThrowIfNull(canExecute);
this.cancelableExecute = cancelableExecute;
this.canExecute = canExecute;
- this.allowConcurrentExecutions = allowConcurrentExecutions;
+ this.options = options;
}
private Task? executionTask;
@@ -220,7 +220,7 @@ static async void MonitorTask(AsyncRelayCommand @this, Task task)
@this.PropertyChanged?.Invoke(@this, AsyncRelayCommand.CanBeCanceledChangedEventArgs);
}
- if (!@this.allowConcurrentExecutions)
+ if ((@this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0)
{
@this.CanExecuteChanged?.Invoke(@this, EventArgs.Empty);
}
@@ -255,7 +255,7 @@ public bool CanExecute(T? parameter)
{
bool canExecute = this.canExecute?.Invoke(parameter) != false;
- return canExecute && (this.allowConcurrentExecutions || ExecutionTask is not { IsCompleted: false });
+ return canExecute && ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) != 0 || ExecutionTask is not { IsCompleted: false });
}
///
@@ -275,13 +275,18 @@ public bool CanExecute(object? parameter)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(T? parameter)
{
- _ = ExecuteAsync(parameter);
+ Task executionTask = ExecuteAsync(parameter);
+
+ if ((this.options & AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler) == 0)
+ {
+ AsyncRelayCommand.AwaitAndThrowIfFailed(executionTask);
+ }
}
///
public void Execute(object? parameter)
{
- _ = ExecuteAsync((T?)parameter);
+ Execute((T?)parameter);
}
///
@@ -306,7 +311,7 @@ public Task ExecuteAsync(T? parameter)
}
// If concurrent executions are disabled, notify the can execute change as well
- if (!this.allowConcurrentExecutions)
+ if ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0)
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
diff --git a/CommunityToolkit.Mvvm/Input/Attributes/RelayCommandAttribute.cs b/CommunityToolkit.Mvvm/Input/Attributes/RelayCommandAttribute.cs
index ee2c268f9..0a1d7f6b2 100644
--- a/CommunityToolkit.Mvvm/Input/Attributes/RelayCommandAttribute.cs
+++ b/CommunityToolkit.Mvvm/Input/Attributes/RelayCommandAttribute.cs
@@ -75,14 +75,30 @@ public sealed class RelayCommandAttribute : Attribute
///
/// Gets or sets a value indicating whether or not to allow concurrent executions for an asynchronous command.
+ ///
/// When set for an attribute used on a method that would result in an or an
/// property to be generated, this will modify the behavior of these commands
/// when an execution is invoked while a previous one is still running. It is the same as creating an instance of
- /// these command types with a constructor such as .
+ /// these command types with a constructor such as
+ /// and using the value.
+ ///
///
/// Using this property is not valid if the target command doesn't map to an asynchronous command.
public bool AllowConcurrentExecutions { get; init; }
+ ///
+ /// Gets or sets a value indicating whether or not to exceptions should be propagated to .
+ ///
+ /// When set for an attribute used on a method that would result in an or an
+ /// property to be generated, this will modify the behavior of these commands
+ /// in case an exception is thrown by the underlying operation. It is the same as creating an instance of
+ /// these command types with a constructor such as
+ /// and using the value.
+ ///
+ ///
+ /// Using this property is not valid if the target command doesn't map to an asynchronous command.
+ public bool FlowExceptionsToTaskScheduler { get; init; }
+
///
/// Gets or sets a value indicating whether a cancel command should also be generated for an asynchronous command.
///
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index a6e8a86d7..263d66837 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -213,7 +213,7 @@ public partial class SampleViewModel
}
[TestMethod]
- public void InvalidICommandMethodSignatureError()
+ public void InvalidRelayCommandMethodSignatureError()
{
string source = @"
using CommunityToolkit.Mvvm.Input;
@@ -310,7 +310,7 @@ public partial class SampleViewModel : ObservableValidator
}
[TestMethod]
- public void UnsupportedCSharpLanguageVersion_FromICommandGenerator()
+ public void UnsupportedCSharpLanguageVersion_FromRelayCommandGenerator()
{
string source = @"
using CommunityToolkit.Mvvm.Input;
@@ -535,7 +535,7 @@ private void GreetUser(string name)
}
[TestMethod]
- public void InvalidICommandAllowConcurrentExecutionsSettings()
+ public void InvalidRelayCommandAllowConcurrentExecutionsOption()
{
string source = @"
using CommunityToolkit.Mvvm.Input;
@@ -555,7 +555,7 @@ private void GreetUser(User user)
}
[TestMethod]
- public void InvalidICommandIncludeCancelCommandSettings_SynchronousMethod()
+ public void InvalidRelayCommandIncludeCancelCommandSettings_SynchronousMethod()
{
string source = @"
using CommunityToolkit.Mvvm.Input;
@@ -575,7 +575,7 @@ private void GreetUser(User user)
}
[TestMethod]
- public void InvalidICommandIncludeCancelCommandSettings_AsynchronousMethodWithNoCancellationToken()
+ public void InvalidRelayCommandIncludeCancelCommandSettings_AsynchronousMethodWithNoCancellationToken()
{
string source = @"
using System.Threading.Tasks;
@@ -596,7 +596,7 @@ private async Task DoWorkAsync()
}
[TestMethod]
- public void InvalidICommandIncludeCancelCommandSettings_AsynchronousMethodWithParameterAndNoCancellationToken()
+ public void InvalidRelayCommandIncludeCancelCommandSettings_AsynchronousMethodWithParameterAndNoCancellationToken()
{
string source = @"
using System.Threading.Tasks;
@@ -1081,7 +1081,7 @@ public partial class MyViewModel : ObservableObject
}
[TestMethod]
- public void MultipleICommandMethodOverloads_WithOverloads()
+ public void MultipleRelayCommandMethodOverloads_WithOverloads()
{
string source = @"
using CommunityToolkit.Mvvm.Input;
@@ -1106,7 +1106,7 @@ private void GreetUser(object value)
}
[TestMethod]
- public void MultipleICommandMethodOverloads_WithOverloadInBaseType()
+ public void MultipleRelayCommandMethodOverloads_WithOverloadInBaseType()
{
string source = @"
using CommunityToolkit.Mvvm.Input;
@@ -1390,6 +1390,26 @@ public partial class MyViewModel : MyBaseViewModel
VerifyGeneratedDiagnostics(source, "MVVMTK0030");
}
+ [TestMethod]
+ public void InvalidRelayCommandFlowExceptionsToTaskSchedulerOption()
+ {
+ string source = @"
+ using CommunityToolkit.Mvvm.Input;
+
+ namespace MyApp
+ {
+ public partial class SampleViewModel
+ {
+ [RelayCommand(FlowExceptionsToTaskScheduler = false)]
+ private void GreetUser(User user)
+ {
+ }
+ }
+ }";
+
+ VerifyGeneratedDiagnostics(source, "MVVMTK0031");
+ }
+
///
/// Verifies the output of a source generator.
///
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/CommunityToolkit.Mvvm.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.UnitTests/CommunityToolkit.Mvvm.UnitTests.csproj
index 6e494cb70..bee92fbc3 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/CommunityToolkit.Mvvm.UnitTests.csproj
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/CommunityToolkit.Mvvm.UnitTests.csproj
@@ -5,6 +5,7 @@
+
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Input.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Input.cs
index 234950085..bf3d66110 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Input.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Input.cs
@@ -30,33 +30,33 @@ public void Test_ArgumentNullException_RelayCommandOfT()
public void Test_ArgumentNullException_AsyncRelayCommand()
{
Assert(() => new AsyncRelayCommand(execute: null!), "execute");
- Assert(() => new AsyncRelayCommand(execute: null!, true), "execute");
+ Assert(() => new AsyncRelayCommand(execute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "execute");
Assert(() => new AsyncRelayCommand(cancelableExecute: null!), "cancelableExecute");
- Assert(() => new AsyncRelayCommand(cancelableExecute: null!, true), "cancelableExecute");
+ Assert(() => new AsyncRelayCommand(cancelableExecute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "cancelableExecute");
Assert(() => new AsyncRelayCommand(execute: null!, () => true), "execute");
Assert(() => new AsyncRelayCommand(() => Task.CompletedTask, canExecute: null!), "canExecute");
- Assert(() => new AsyncRelayCommand(execute: null!, () => true, true), "execute");
- Assert(() => new AsyncRelayCommand(() => Task.CompletedTask, canExecute: null!, true), "canExecute");
+ Assert(() => new AsyncRelayCommand(execute: null!, () => true, AsyncRelayCommandOptions.AllowConcurrentExecutions), "execute");
+ Assert(() => new AsyncRelayCommand(() => Task.CompletedTask, canExecute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "canExecute");
Assert(() => new AsyncRelayCommand(cancelableExecute: null!, () => true), "cancelableExecute");
Assert(() => new AsyncRelayCommand(t => Task.CompletedTask, canExecute: null!), "canExecute");
- Assert(() => new AsyncRelayCommand(cancelableExecute: null!, () => true, true), "cancelableExecute");
- Assert(() => new AsyncRelayCommand(t => Task.CompletedTask, canExecute: null!, true), "canExecute");
+ Assert(() => new AsyncRelayCommand(cancelableExecute: null!, () => true, AsyncRelayCommandOptions.AllowConcurrentExecutions), "cancelableExecute");
+ Assert(() => new AsyncRelayCommand(t => Task.CompletedTask, canExecute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "canExecute");
}
[TestMethod]
public void Test_ArgumentNullException_AsyncRelayCommandOfT()
{
Assert(() => new AsyncRelayCommand(execute: null!), "execute");
- Assert(() => new AsyncRelayCommand(execute: null!, true), "execute");
+ Assert(() => new AsyncRelayCommand(execute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "execute");
Assert(() => new AsyncRelayCommand(cancelableExecute: null!), "cancelableExecute");
- Assert(() => new AsyncRelayCommand(cancelableExecute: null!, true), "cancelableExecute");
+ Assert(() => new AsyncRelayCommand(cancelableExecute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "cancelableExecute");
Assert(() => new AsyncRelayCommand(execute: null!, s => true), "execute");
Assert(() => new AsyncRelayCommand(s => Task.CompletedTask, canExecute: null!), "canExecute");
- Assert(() => new AsyncRelayCommand(execute: null!, s => true, true), "execute");
- Assert(() => new AsyncRelayCommand(s => Task.CompletedTask, canExecute: null!, true), "canExecute");
+ Assert(() => new AsyncRelayCommand(execute: null!, s => true, AsyncRelayCommandOptions.AllowConcurrentExecutions), "execute");
+ Assert(() => new AsyncRelayCommand(s => Task.CompletedTask, canExecute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "canExecute");
Assert(() => new AsyncRelayCommand(cancelableExecute: null!, s => true), "cancelableExecute");
Assert(() => new AsyncRelayCommand(t => Task.CompletedTask, canExecute: null!), "canExecute");
- Assert(() => new AsyncRelayCommand(cancelableExecute: null!, s => true, true), "cancelableExecute");
- Assert(() => new AsyncRelayCommand(t => Task.CompletedTask, canExecute: null!, true), "canExecute");
+ Assert(() => new AsyncRelayCommand(cancelableExecute: null!, s => true, AsyncRelayCommandOptions.AllowConcurrentExecutions), "cancelableExecute");
+ Assert(() => new AsyncRelayCommand(t => Task.CompletedTask, canExecute: null!, AsyncRelayCommandOptions.AllowConcurrentExecutions), "canExecute");
}
}
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand.cs
index adb3fa407..c786e0de0 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand.cs
@@ -10,6 +10,7 @@
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.UnitTests.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Nito.AsyncEx;
namespace CommunityToolkit.Mvvm.UnitTests;
@@ -190,7 +191,7 @@ public async Task Test_AsyncRelayCommand_AllowConcurrentExecutions_Enable()
new TaskCompletionSource