Skip to content

Commit 6def774

Browse files
committed
Enabled [ICommand] generation for all command types
1 parent 44762f4 commit 6def774

File tree

2 files changed

+145
-19
lines changed

2 files changed

+145
-19
lines changed

Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System;
56
using System.Collections.Generic;
7+
using System.Diagnostics.CodeAnalysis;
68
using System.Diagnostics.Contracts;
79
using System.Linq;
810
using System.Text;
@@ -129,12 +131,15 @@ private static IEnumerable<MemberDeclarationSyntax> CreateCommandMembers(Generat
129131
fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}";
130132

131133
// Get the command type symbols
132-
ExtractCommandTypesFromMethod(
134+
if (!TryMapCommandTypesFromMethod(
133135
context,
134136
methodSymbol,
135-
out ITypeSymbol commandInterfaceTypeSymbol,
136-
out ITypeSymbol commandClassTypeSymbol,
137-
out ITypeSymbol delegateTypeSymbol);
137+
out ITypeSymbol? commandInterfaceTypeSymbol,
138+
out ITypeSymbol? commandClassTypeSymbol,
139+
out ITypeSymbol? delegateTypeSymbol))
140+
{
141+
return Array.Empty<MemberDeclarationSyntax>();
142+
}
138143

139144
// Construct the generated field as follows:
140145
//
@@ -193,33 +198,95 @@ private static IEnumerable<MemberDeclarationSyntax> CreateCommandMembers(Generat
193198
/// <param name="commandInterfaceTypeSymbol">The command interface type symbol.</param>
194199
/// <param name="commandClassTypeSymbol">The command class type symbol.</param>
195200
/// <param name="delegateTypeSymbol">The delegate type symbol for the wrapped method.</param>
196-
private static void ExtractCommandTypesFromMethod(
201+
/// <returns>Whether or not <paramref name="methodSymbol"/> was valid and the requested types have been set.</returns>
202+
private static bool TryMapCommandTypesFromMethod(
197203
GeneratorExecutionContext context,
198204
IMethodSymbol methodSymbol,
199-
out ITypeSymbol commandInterfaceTypeSymbol,
200-
out ITypeSymbol commandClassTypeSymbol,
201-
out ITypeSymbol delegateTypeSymbol)
205+
[NotNullWhen(true)] out ITypeSymbol? commandInterfaceTypeSymbol,
206+
[NotNullWhen(true)] out ITypeSymbol? commandClassTypeSymbol,
207+
[NotNullWhen(true)] out ITypeSymbol? delegateTypeSymbol)
202208
{
209+
// Map <void, void> to IRelayCommand, RelayCommand, Action
203210
if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 0)
204211
{
205212
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IRelayCommand")!;
206213
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.RelayCommand")!;
207214
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action")!;
215+
216+
return true;
208217
}
209-
else if (methodSymbol.ReturnsVoid &&
210-
methodSymbol.Parameters.Length == 1 &&
211-
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None } parameter)
218+
219+
// Map <T, void> to IRelayCommand<T>, RelayCommand<T>, Action<T>
220+
if (methodSymbol.ReturnsVoid &&
221+
methodSymbol.Parameters.Length == 1 &&
222+
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } parameter)
212223
{
213-
commandInterfaceTypeSymbol = null;
214-
commandClassTypeSymbol = null;
215-
delegateTypeSymbol = null;
224+
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IRelayCommand`1")!.Construct(parameter.Type);
225+
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.RelayCommand`1")!.Construct(parameter.Type);
226+
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action`1")!.Construct(parameter.Type);
227+
228+
return true;
216229
}
217-
else
230+
231+
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!))
218232
{
219-
commandInterfaceTypeSymbol = null;
220-
commandClassTypeSymbol = null;
221-
delegateTypeSymbol = null;
233+
// Map <void, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<Task>
234+
if (methodSymbol.Parameters.Length == 0)
235+
{
236+
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand")!;
237+
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand")!;
238+
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`1")!.Construct(context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
239+
240+
return true;
241+
}
242+
243+
if (methodSymbol.Parameters.Length == 1 &&
244+
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } singleParameter)
245+
{
246+
// Map <CancellationToken, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<CancellationToken, Task>
247+
if (SymbolEqualityComparer.Default.Equals(singleParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!))
248+
{
249+
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand")!;
250+
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand")!;
251+
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct(
252+
context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!,
253+
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
254+
255+
return true;
256+
}
257+
258+
// Map <T, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, Task>
259+
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(singleParameter.Type);
260+
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(singleParameter.Type);
261+
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct(
262+
singleParameter.Type,
263+
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
264+
265+
return true;
266+
}
267+
268+
// Map <T, CancellationToken, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, CancellationToken, Task>
269+
if (methodSymbol.Parameters.Length == 2 &&
270+
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } firstParameter &&
271+
methodSymbol.Parameters[1] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } secondParameter &&
272+
SymbolEqualityComparer.Default.Equals(secondParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!))
273+
{
274+
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(firstParameter.Type);
275+
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(firstParameter.Type);
276+
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`3")!.Construct(
277+
firstParameter.Type,
278+
secondParameter.Type,
279+
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
280+
281+
return true;
282+
}
222283
}
284+
285+
commandInterfaceTypeSymbol = null;
286+
commandClassTypeSymbol = null;
287+
delegateTypeSymbol = null;
288+
289+
return false;
223290
}
224291
}
225292
}

UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
#pragma warning disable CS0618
66

7-
using System;
7+
using System.Threading;
8+
using System.Threading.Tasks;
89
using Microsoft.Toolkit.Mvvm.Input;
910
using Microsoft.VisualStudio.TestTools.UnitTesting;
1011

@@ -24,6 +25,26 @@ public void Test_ICommandAttribute_RelayCommand()
2425
model.IncrementCounterCommand.Execute(null);
2526

2627
Assert.AreEqual(model.Counter, 1);
28+
29+
model.IncrementCounterWithValueCommand.Execute(5);
30+
31+
Assert.AreEqual(model.Counter, 6);
32+
33+
model.IncrementCounterAsyncCommand.Execute(null);
34+
35+
Assert.AreEqual(model.Counter, 7);
36+
37+
model.IncrementCounterWithTokenAsyncCommand.Execute(null);
38+
39+
Assert.AreEqual(model.Counter, 8);
40+
41+
model.IncrementCounterWithValueAsyncCommand.Execute(5);
42+
43+
Assert.AreEqual(model.Counter, 13);
44+
45+
model.IncrementCounterWithValueAndTokenAsyncCommand.Execute(5);
46+
47+
Assert.AreEqual(model.Counter, 18);
2748
}
2849

2950
public sealed partial class MyViewModel
@@ -35,6 +56,44 @@ private void IncrementCounter()
3556
{
3657
Counter++;
3758
}
59+
60+
[ICommand]
61+
private void IncrementCounterWithValue(int count)
62+
{
63+
Counter += count;
64+
}
65+
66+
[ICommand]
67+
private Task IncrementCounterAsync()
68+
{
69+
Counter += 1;
70+
71+
return Task.CompletedTask;
72+
}
73+
74+
[ICommand]
75+
private Task IncrementCounterWithTokenAsync(CancellationToken token)
76+
{
77+
Counter += 1;
78+
79+
return Task.CompletedTask;
80+
}
81+
82+
[ICommand]
83+
private Task IncrementCounterWithValueAsync(int count)
84+
{
85+
Counter += count;
86+
87+
return Task.CompletedTask;
88+
}
89+
90+
[ICommand]
91+
private Task IncrementCounterWithValueAndTokenAsync(int count, CancellationToken token)
92+
{
93+
Counter += count;
94+
95+
return Task.CompletedTask;
96+
}
3897
}
3998
}
4099
}

0 commit comments

Comments
 (0)