Skip to content

Commit 08bf899

Browse files
authored
Merge pull request #301 from CommunityToolkit/dev/relay-command-argument-exceptions
Improve exceptions for invalid command arguments
2 parents 90f1db2 + bfa9e1c commit 08bf899

File tree

6 files changed

+211
-21
lines changed

6 files changed

+211
-21
lines changed

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,18 @@ public bool CanExecute(T? parameter)
262262
[MethodImpl(MethodImplOptions.AggressiveInlining)]
263263
public bool CanExecute(object? parameter)
264264
{
265-
if (default(T) is not null &&
266-
parameter is null)
265+
// Special case, see RelayCommand<T>.CanExecute(object?) for more info
266+
if (parameter is null && default(T) is not null)
267267
{
268268
return false;
269269
}
270270

271-
return CanExecute((T?)parameter);
271+
if (!RelayCommand<T>.TryGetCommandArgument(parameter, out T? result))
272+
{
273+
RelayCommand<T>.ThrowArgumentExceptionForInvalidCommandArgument(parameter);
274+
}
275+
276+
return CanExecute(result);
272277
}
273278

274279
/// <inheritdoc/>
@@ -286,7 +291,12 @@ public void Execute(T? parameter)
286291
/// <inheritdoc/>
287292
public void Execute(object? parameter)
288293
{
289-
Execute((T?)parameter);
294+
if (!RelayCommand<T>.TryGetCommandArgument(parameter, out T? result))
295+
{
296+
RelayCommand<T>.ThrowArgumentExceptionForInvalidCommandArgument(parameter);
297+
}
298+
299+
Execute(result);
290300
}
291301

292302
/// <inheritdoc/>
@@ -322,7 +332,12 @@ public Task ExecuteAsync(T? parameter)
322332
/// <inheritdoc/>
323333
public Task ExecuteAsync(object? parameter)
324334
{
325-
return ExecuteAsync((T?)parameter);
335+
if (!RelayCommand<T>.TryGetCommandArgument(parameter, out T? result))
336+
{
337+
RelayCommand<T>.ThrowArgumentExceptionForInvalidCommandArgument(parameter);
338+
}
339+
340+
return ExecuteAsync(result);
326341
}
327342

328343
/// <inheritdoc/>

CommunityToolkit.Mvvm/Input/Interfaces/IAsyncRelayCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public interface IAsyncRelayCommand : IRelayCommand, INotifyPropertyChanged
6363
/// </summary>
6464
/// <param name="parameter">The input parameter.</param>
6565
/// <returns>The <see cref="Task"/> representing the async operation being executed.</returns>
66+
/// <exception cref="System.ArgumentException">Thrown if <paramref name="parameter"/> is incompatible with the underlying command implementation.</exception>
6667
Task ExecuteAsync(object? parameter);
6768

6869
/// <summary>

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

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// more info in ThirdPartyNotices.txt in the root of the project.
77

88
using System;
9+
using System.Diagnostics.CodeAnalysis;
910
using System.Runtime.CompilerServices;
1011

1112
namespace CommunityToolkit.Mvvm.Input;
@@ -81,13 +82,19 @@ public bool CanExecute(T? parameter)
8182
/// <inheritdoc/>
8283
public bool CanExecute(object? parameter)
8384
{
84-
if (default(T) is not null &&
85-
parameter is null)
85+
// Special case a null value for a value type argument type.
86+
// This ensures that no exceptions are thrown during initialization.
87+
if (parameter is null && default(T) is not null)
8688
{
8789
return false;
8890
}
8991

90-
return CanExecute((T?)parameter);
92+
if (!TryGetCommandArgument(parameter, out T? result))
93+
{
94+
ThrowArgumentExceptionForInvalidCommandArgument(parameter);
95+
}
96+
97+
return CanExecute(result);
9198
}
9299

93100
/// <inheritdoc/>
@@ -100,6 +107,66 @@ public void Execute(T? parameter)
100107
/// <inheritdoc/>
101108
public void Execute(object? parameter)
102109
{
103-
Execute((T?)parameter);
110+
if (!TryGetCommandArgument(parameter, out T? result))
111+
{
112+
ThrowArgumentExceptionForInvalidCommandArgument(parameter);
113+
}
114+
115+
Execute(result);
116+
}
117+
118+
/// <summary>
119+
/// Tries to get a command argument of compatible type <typeparamref name="T"/> from an input <see cref="object"/>.
120+
/// </summary>
121+
/// <param name="parameter">The input parameter.</param>
122+
/// <param name="result">The resulting <typeparamref name="T"/> value, if any.</param>
123+
/// <returns>Whether or not a compatible command argument could be retrieved.</returns>
124+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
125+
internal static bool TryGetCommandArgument(object? parameter, out T? result)
126+
{
127+
// If the argument is null and the default value of T is also null, then the
128+
// argument is valid. T might be a reference type or a nullable value type.
129+
if (parameter is null && default(T) is null)
130+
{
131+
result = default;
132+
133+
return true;
134+
}
135+
136+
// Check if the argument is a T value, so either an instance of a type or a derived
137+
// type of T is a reference type, an interface implementation if T is an interface,
138+
// or a boxed value type in case T was a value type.
139+
if (parameter is T argument)
140+
{
141+
result = argument;
142+
143+
return true;
144+
}
145+
146+
result = default;
147+
148+
return false;
149+
}
150+
151+
/// <summary>
152+
/// Throws an <see cref="ArgumentException"/> if an invalid command argument is used.
153+
/// </summary>
154+
/// <param name="parameter">The input parameter.</param>
155+
/// <exception cref="ArgumentException">Thrown with an error message to give info on the invalid parameter.</exception>
156+
[DoesNotReturn]
157+
internal static void ThrowArgumentExceptionForInvalidCommandArgument(object? parameter)
158+
{
159+
[MethodImpl(MethodImplOptions.NoInlining)]
160+
static Exception GetException(object? parameter)
161+
{
162+
if (parameter is null)
163+
{
164+
return new ArgumentException($"Parameter \"{nameof(parameter)}\" (object) must not be null, as the command type requires an argument of type {typeof(T)}.", nameof(parameter));
165+
}
166+
167+
return new ArgumentException($"Parameter \"{nameof(parameter)}\" (object) cannot be of type {parameter.GetType()}, as the command type requires an argument of type {typeof(T)}.", nameof(parameter));
168+
}
169+
170+
throw GetException(parameter);
104171
}
105172
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace CommunityToolkit.Mvvm.UnitTests.Helpers;
9+
10+
/// <summary>
11+
/// A helper class to validate scenarios related to <see cref="Exception"/>-s.
12+
/// </summary>
13+
internal static class ExceptionHelper
14+
{
15+
/// <summary>
16+
/// Asserts that a given action throws an <see cref="ArgumentException"/> with a specific parameter name.
17+
/// </summary>
18+
/// <param name="action">The input <see cref="Action"/> to invoke.</param>
19+
/// <param name="parameterName">The expected parameter name.</param>
20+
public static void ThrowsArgumentExceptionWithParameterName(Action action, string parameterName)
21+
{
22+
bool success = false;
23+
24+
try
25+
{
26+
action();
27+
}
28+
catch (Exception e)
29+
{
30+
Assert.IsTrue(e.GetType() == typeof(ArgumentException));
31+
Assert.AreEqual(parameterName, ((ArgumentException)e).ParamName);
32+
33+
success = true;
34+
}
35+
36+
Assert.IsTrue(success);
37+
}
38+
}

tests/CommunityToolkit.Mvvm.UnitTests/Test_AsyncRelayCommand{T}.cs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ public async Task Test_AsyncRelayCommandOfT_AlwaysEnabled()
6262

6363
Assert.AreEqual(ticks, 2);
6464

65-
_ = Assert.ThrowsException<InvalidCastException>(() => command.Execute(new object()));
65+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(new object()), "parameter");
66+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(42), "parameter");
67+
68+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(new object()), "parameter");
69+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(42), "parameter");
6670
}
6771

6872
[TestMethod]
@@ -87,6 +91,12 @@ public void Test_AsyncRelayCommandOfT_WithCanExecuteFunctionTrue()
8791
command.Execute("2");
8892

8993
Assert.AreEqual(ticks, 2);
94+
95+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(new object()), "parameter");
96+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(42), "parameter");
97+
98+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(new object()), "parameter");
99+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(42), "parameter");
90100
}
91101

92102
[TestMethod]
@@ -112,10 +122,16 @@ public void Test_AsyncRelayCommandOfT_WithCanExecuteFunctionFalse()
112122
command.Execute("42");
113123

114124
Assert.AreEqual(ticks, 42);
125+
126+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(new object()), "parameter");
127+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(42), "parameter");
128+
129+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(new object()), "parameter");
130+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(42), "parameter");
115131
}
116132

117133
[TestMethod]
118-
public void Test_AsyncRelayCommandOfT_NullWithValueType()
134+
public void Test_AsyncRelayCommandOfT_InvalidArgumentWithValueType()
119135
{
120136
int n = 0;
121137

@@ -125,18 +141,38 @@ public void Test_AsyncRelayCommandOfT_NullWithValueType()
125141
return Task.CompletedTask;
126142
});
127143

144+
// Special case
128145
Assert.IsFalse(command.CanExecute(null));
129-
_ = Assert.ThrowsException<NullReferenceException>(() => command.Execute(null));
130146

131-
command = new AsyncRelayCommand<int>(
147+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute("Hello"), "parameter");
148+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(3.14f), "parameter");
149+
150+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(null), "parameter");
151+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute("Hello"), "parameter");
152+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(3.14f), "parameter");
153+
}
154+
155+
[TestMethod]
156+
public void Test_AsyncRelayCommandOfT_InvalidArgumentWithValueType_WithCanExecute()
157+
{
158+
int n = 0;
159+
160+
AsyncRelayCommand<int>? command = new(
132161
i =>
133162
{
134163
n = i;
135164
return Task.CompletedTask;
136165
}, i => i > 0);
137166

167+
// Special case
138168
Assert.IsFalse(command.CanExecute(null));
139-
_ = Assert.ThrowsException<NullReferenceException>(() => command.Execute(null));
169+
170+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute("Hello"), "parameter");
171+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(3.14f), "parameter");
172+
173+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(null), "parameter");
174+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute("Hello"), "parameter");
175+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(3.14f), "parameter");
140176
}
141177

142178
[TestMethod]
@@ -239,7 +275,11 @@ private static async Task Test_AsyncRelayCommandOfT_AllowConcurrentExecutions_Te
239275

240276
Assert.IsFalse(command.IsRunning);
241277

242-
_ = Assert.ThrowsException<InvalidCastException>(() => command.Execute(new object()));
278+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(new object()), "parameter");
279+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(3.14f), "parameter");
280+
281+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(new object()), "parameter");
282+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(3.14f), "parameter");
243283
}
244284

245285
[TestMethod]

tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommand{T}.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using CommunityToolkit.Mvvm.Input;
7+
using CommunityToolkit.Mvvm.UnitTests.Helpers;
78
using Microsoft.VisualStudio.TestTools.UnitTesting;
89

910
namespace CommunityToolkit.Mvvm.UnitTests;
@@ -21,7 +22,11 @@ public void Test_RelayCommandOfT_AlwaysEnabled()
2122
Assert.IsTrue(command.CanExecute("Text"));
2223
Assert.IsTrue(command.CanExecute(null));
2324

24-
_ = Assert.ThrowsException<InvalidCastException>(() => command.CanExecute(new object()));
25+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(new object()), "parameter");
26+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(42), "parameter");
27+
28+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(new object()), "parameter");
29+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(42), "parameter");
2530

2631
(object?, EventArgs?) args = default;
2732

@@ -51,7 +56,11 @@ public void Test_RelayCommand_WithCanExecuteFunction()
5156
Assert.IsTrue(command.CanExecute("Text"));
5257
Assert.IsFalse(command.CanExecute(null));
5358

54-
_ = Assert.ThrowsException<InvalidCastException>(() => command.CanExecute(new object()));
59+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(new object()), "parameter");
60+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(42), "parameter");
61+
62+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(new object()), "parameter");
63+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(42), "parameter");
5564

5665
command.Execute((object)"Hello");
5766

@@ -64,18 +73,38 @@ public void Test_RelayCommand_WithCanExecuteFunction()
6473
}
6574

6675
[TestMethod]
67-
public void Test_RelayCommand_NullWithValueType()
76+
public void Test_RelayCommand_InvalidArgumentWithValueType()
6877
{
6978
int n = 0;
7079

7180
RelayCommand<int>? command = new(i => n = i);
7281

82+
// Special case
7383
Assert.IsFalse(command.CanExecute(null));
74-
_ = Assert.ThrowsException<NullReferenceException>(() => command.Execute(null));
7584

76-
command = new RelayCommand<int>(i => n = i, i => i > 0);
85+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute("Hello"), "parameter");
86+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(3.14f), "parameter");
7787

88+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(null), "parameter");
89+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute("Hello"), "parameter");
90+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(3.14f), "parameter");
91+
}
92+
93+
[TestMethod]
94+
public void Test_RelayCommand_InvalidArgumentWithValueType_WithCanExecute()
95+
{
96+
int n = 0;
97+
98+
RelayCommand<int>? command = new(i => n = i, i => i > 0);
99+
100+
// Special case
78101
Assert.IsFalse(command.CanExecute(null));
79-
_ = Assert.ThrowsException<NullReferenceException>(() => command.Execute(null));
102+
103+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute("Hello"), "parameter");
104+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.CanExecute(3.14f), "parameter");
105+
106+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(null), "parameter");
107+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute("Hello"), "parameter");
108+
ExceptionHelper.ThrowsArgumentExceptionWithParameterName(() => command.Execute(3.14f), "parameter");
80109
}
81110
}

0 commit comments

Comments
 (0)