Skip to content

Commit e2254e1

Browse files
committed
feat(source-generators): extend RelayCommand with InvertCanExecute
1 parent 3c06397 commit e2254e1

File tree

9 files changed

+149
-12
lines changed

9 files changed

+149
-12
lines changed

docs/SourceGenerators/ViewModel.md

+18-3
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ The `RelayCommand` attribute generates `IRelayCommand` properties, eliminating m
134134

135135
- `CommandName` for customization.
136136
- `CanExecute` a property or method that return `bool` to specified to control when the command is executable.
137+
- `InvertCanExecute` if the property `CanExecute` is defined, this property will invert the outcome from the property or method `CanExecute` relays on.
137138
- `ParameterValue` or `ParameterValues` for 1 or many parameter values.
138139

139140
### 🛠 Quick Start Tips for RelayCommands
@@ -158,6 +159,10 @@ public void Save(string text);
158159
// Generates a RelayCommand with CanExecute function
159160
[RelayCommand(CanExecute = nameof(CanSave))]
160161
public void Save();
162+
163+
// Generates a RelayCommand with CanExecute function
164+
[RelayCommand(CanExecute = nameof(IsConnected), InvertCanExecute = true)]
165+
public void Connect();
161166
```
162167

163168
**Note:**
@@ -199,6 +204,10 @@ public Task Save();
199204
// Generates an asynchronous RelayCommand with async keyword and CanExecute function
200205
[RelayCommand(CanExecute = nameof(CanSave))]
201206
public async Task Save();
207+
208+
// Generates an asynchronous RelayCommand async keyword and CanExecute function
209+
[RelayCommand(CanExecute = nameof(IsConnected), InvertCanExecute = true)]
210+
public async Task Connect();
202211
```
203212

204213
### 🔁 Multi-Parameter Commands
@@ -375,7 +384,7 @@ public partial class TestViewModel
375384
```csharp
376385
public partial class PersonViewModel : ViewModelBase
377386
{
378-
[ObservableProperty]
387+
[ObservableProperty(BroadcastOnChange = true)]
379388
[NotifyPropertyChangedFor(nameof(FullName))]
380389
[Required]
381390
[MinLength(2)]
@@ -398,7 +407,11 @@ public partial class PersonViewModel : ViewModelBase
398407

399408
public string FullName => $"{FirstName} {LastName}";
400409

401-
[RelayCommand]
410+
public bool IsConnected { get; set; }
411+
412+
[RelayCommand(
413+
CanExecute = nameof(IsConnected),
414+
InvertCanExecute = true)]
402415
public void ShowData()
403416
{
404417
// TODO: Implement ShowData - it could be a dialog box
@@ -428,7 +441,7 @@ public partial class PersonViewModel : ViewModelBase
428441
```csharp
429442
public partial class PersonViewModel
430443
{
431-
public IRelayCommand ShowDataCommand => new RelayCommand(ShowData);
444+
public IRelayCommand ShowDataCommand => new RelayCommand(ShowData, () => !IsConnected);
432445

433446
public IRelayCommand SaveHandlerCommand => new RelayCommand(SaveHandler, CanSaveHandler);
434447

@@ -442,9 +455,11 @@ public partial class PersonViewModel
442455
return;
443456
}
444457

458+
var oldValue = firstName;
445459
firstName = value;
446460
RaisePropertyChanged(nameof(FirstName));
447461
RaisePropertyChanged(nameof(FullName));
462+
Broadcast(nameof(FirstName), oldValue, value);
448463
}
449464
}
450465

sample/Atc.Wpf.Sample/SamplesWpfSourceGenerators/PersonViewModel.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ public partial class PersonViewModel : ViewModelBase
2525

2626
public string FullName => $"{FirstName} {LastName}";
2727

28-
[RelayCommand]
28+
public bool IsConnected { get; set; }
29+
30+
[RelayCommand(
31+
CanExecute = nameof(IsConnected),
32+
InvertCanExecute = true)]
2933
public void ShowData()
3034
{
3135
var dialogBox = new InfoDialogBox(

src/Atc.Wpf.SourceGenerators/Builders/CommandBuilderBase.cs

+17-7
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private static void GenerateRelayCommandWithOutParameterValues(
4949
rc.CommandName,
5050
rc.MethodName,
5151
rc.CanExecuteName,
52+
rc.InvertCanExecute,
5253
rc.UsePropertyForCanExecute);
5354
builder.AppendLine(cmd);
5455
}
@@ -64,6 +65,7 @@ private static void GenerateRelayCommandWithOutParameterValues(
6465
rc.CommandName,
6566
$"{rc.MethodName}(CancellationToken.None)",
6667
rc.CanExecuteName,
68+
rc.InvertCanExecute,
6769
isLambda: true);
6870
builder.AppendLine(cmd);
6971
}
@@ -76,6 +78,7 @@ private static void GenerateRelayCommandWithOutParameterValues(
7678
rc.CommandName,
7779
rc.MethodName,
7880
rc.CanExecuteName,
81+
rc.InvertCanExecute,
7982
rc.UsePropertyForCanExecute);
8083
builder.AppendLine(cmd);
8184
}
@@ -90,7 +93,7 @@ private static void GenerateRelayCommandWithOutParameterValues(
9093
$"{implementationType}{tupleGeneric}",
9194
rc.CommandName,
9295
$"x => {rc.MethodName}({constructorParametersMulti})",
93-
rc.CanExecuteName is null ? null : $"x => {rc.CanExecuteName}");
96+
rc.CanExecuteName is null ? null : rc.InvertCanExecute ? $"x => !{rc.CanExecuteName}" : $"x => {rc.CanExecuteName}");
9497
builder.AppendLine(cmd);
9598
}
9699
else
@@ -100,7 +103,7 @@ private static void GenerateRelayCommandWithOutParameterValues(
100103
$"{implementationType}{tupleGeneric}",
101104
rc.CommandName,
102105
$"x => {rc.MethodName}({constructorParametersMulti})",
103-
rc.CanExecuteName is null ? null : $"x => {rc.CanExecuteName}({filteredConstructorParameters})");
106+
rc.CanExecuteName is null ? null : rc.InvertCanExecute ? $"x => !{rc.CanExecuteName}({filteredConstructorParameters})" : $"x => {rc.CanExecuteName}({filteredConstructorParameters})");
104107
builder.AppendLine(cmd);
105108
}
106109
}
@@ -143,7 +146,7 @@ private static void GenerateRelayCommandWithParameterValues(
143146
implementationType,
144147
rc.CommandName,
145148
$"() => {rc.MethodName}({constructorParameters})",
146-
$"{rc.CanExecuteName}");
149+
rc.InvertCanExecute ? $"!{rc.CanExecuteName}" : $"{rc.CanExecuteName}");
147150
builder.AppendLine(cmd);
148151
}
149152
else
@@ -153,7 +156,7 @@ private static void GenerateRelayCommandWithParameterValues(
153156
implementationType,
154157
rc.CommandName,
155158
$"() => {rc.MethodName}({constructorParameters})",
156-
$"{rc.CanExecuteName}({constructorParameters})");
159+
rc.InvertCanExecute ? $"!{rc.CanExecuteName}({constructorParameters})" : $"{rc.CanExecuteName}({constructorParameters})");
157160
builder.AppendLine(cmd);
158161
}
159162
}
@@ -197,6 +200,7 @@ private static string GenerateCommandLine(
197200
string commandName,
198201
string constructorParameters,
199202
string? canExecuteName = null,
203+
bool invertCanExecute = false,
200204
bool usePropertyForCanExecute = false,
201205
bool isLambda = false)
202206
{
@@ -209,16 +213,22 @@ private static string GenerateCommandLine(
209213
{
210214
if (interfaceType.Contains('<'))
211215
{
212-
commandInstance += $", _ => {canExecuteName}";
216+
commandInstance += invertCanExecute
217+
? $", _ => !{canExecuteName}"
218+
: $", _ => {canExecuteName}";
213219
}
214220
else
215221
{
216-
commandInstance += $", () => {canExecuteName}";
222+
commandInstance += invertCanExecute
223+
? $", () => !{canExecuteName}"
224+
: $", () => {canExecuteName}";
217225
}
218226
}
219227
else
220228
{
221-
commandInstance += $", {canExecuteName}";
229+
commandInstance += invertCanExecute
230+
? $", !{canExecuteName}"
231+
: $", {canExecuteName}";
222232
}
223233
}
224234

src/Atc.Wpf.SourceGenerators/Inspectors/Attributes/RelayCommandInspector.cs

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ private static void AppendRelayCommandToGenerate(
7575
canExecuteName = canExecuteNameValue!.ExtractInnerContent();
7676
}
7777

78+
var invertCanExecute = relayCommandArgumentValues.TryGetValue(NameConstants.InvertCanExecute, out var invertCanExecuteValue) &&
79+
"true".Equals(invertCanExecuteValue, StringComparison.OrdinalIgnoreCase);
80+
7881
var usePropertyForCanExecute = false;
7982
if (canExecuteName is not null)
8083
{
@@ -111,6 +114,7 @@ is NameConstants.Task
111114
parameterTypes?.ToArray(),
112115
parameterValues.Count == 0 ? null : parameterValues.ToArray(),
113116
canExecuteName,
117+
invertCanExecute,
114118
usePropertyForCanExecute,
115119
isAsync));
116120
}

src/Atc.Wpf.SourceGenerators/Models/AttributeToGenerate/RelayCommandToGenerate.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal sealed class RelayCommandToGenerate(
77
string[]? parameterTypes,
88
string[]? parameterValues,
99
string? canExecuteName,
10+
bool invertCanExecute,
1011
bool usePropertyForCanExecute,
1112
bool isAsync)
1213
{
@@ -20,10 +21,12 @@ internal sealed class RelayCommandToGenerate(
2021

2122
public string? CanExecuteName { get; } = canExecuteName;
2223

24+
public bool InvertCanExecute { get; } = invertCanExecute;
25+
2326
public bool UsePropertyForCanExecute { get; } = usePropertyForCanExecute;
2427

2528
public bool IsAsync { get; } = isAsync;
2629

2730
public override string ToString()
28-
=> $"{nameof(CommandName)}: {CommandName}, {nameof(MethodName)}: {MethodName}, {nameof(ParameterTypes)}.Count: {ParameterTypes?.Length}, {nameof(ParameterValues)}.Count: {ParameterValues?.Length}, {nameof(CanExecuteName)}: {CanExecuteName}, {nameof(UsePropertyForCanExecute)}: {UsePropertyForCanExecute}, {nameof(IsAsync)}: {IsAsync}";
31+
=> $"{nameof(CommandName)}: {CommandName}, {nameof(MethodName)}: {MethodName}, {nameof(ParameterTypes)}.Count: {ParameterTypes?.Length}, {nameof(ParameterValues)}.Count: {ParameterValues?.Length}, {nameof(CanExecuteName)}: {CanExecuteName}, {nameof(InvertCanExecute)}: {InvertCanExecute}, {nameof(UsePropertyForCanExecute)}: {UsePropertyForCanExecute}, {nameof(IsAsync)}: {IsAsync}";
2932
}

src/Atc.Wpf.SourceGenerators/NameConstants.cs

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal static class NameConstants
2525
public const string FrameworkElement = nameof(FrameworkElement);
2626
public const string Handler = nameof(Handler);
2727
public const string IObservableObject = nameof(IObservableObject);
28+
public const string InvertCanExecute = nameof(InvertCanExecute);
2829
public const string IRelayCommand = nameof(IRelayCommand);
2930
public const string IRelayCommandAsync = nameof(IRelayCommandAsync);
3031
public const string IsAnimationProhibited = nameof(IsAnimationProhibited);

src/Atc.Wpf/Mvvm/Attributes/RelayCommandAttribute.cs

+16
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ public RelayCommandAttribute(
3535
/// </summary>
3636
public string? CanExecute { get; set; }
3737

38+
/// <summary>
39+
/// Gets or sets a value indicating whether the canExecute value should be inverted.
40+
/// <para>
41+
/// For example, if CanExecute is specified as "IsConnected" property and InvertCanExecute is true,
42+
/// the generated lambda will be:
43+
/// <code language="csharp">
44+
/// () => !IsConnected
45+
/// </code>
46+
/// instead of:
47+
/// <code language="csharp">
48+
/// () => IsConnected
49+
/// </code>
50+
/// </para>
51+
/// </summary>
52+
public bool InvertCanExecute { get; set; }
53+
3854
/// <summary>
3955
/// Gets or sets the parameter value.
4056
/// </summary>

test/Atc.Wpf.SourceGenerators.Tests/Generators/ViewModelGeneratorTests_AsyncRelayCommand.cs

+42
Original file line numberDiff line numberDiff line change
@@ -1014,4 +1014,46 @@ public partial class TestViewModel
10141014

10151015
AssertGeneratorRunResultAsEqual(expectedCode, generatorResult);
10161016
}
1017+
1018+
[Fact]
1019+
public void AsyncRelayCommand_NoParameter_CanExecute_Inverted()
1020+
{
1021+
const string inputCode =
1022+
"""
1023+
namespace TestNamespace;
1024+
1025+
public partial class TestViewModel : ViewModelBase
1026+
{
1027+
[RelayCommand(CanExecute = nameof(CanSave), InvertCanExecute = true)]
1028+
public Task Save()
1029+
{
1030+
}
1031+
1032+
public bool CanSave()
1033+
{
1034+
return true;
1035+
}
1036+
}
1037+
""";
1038+
1039+
const string expectedCode =
1040+
"""
1041+
// <auto-generated>
1042+
#nullable enable
1043+
using Atc.Wpf.Command;
1044+
1045+
namespace TestNamespace;
1046+
1047+
public partial class TestViewModel
1048+
{
1049+
public IRelayCommandAsync SaveCommand => new RelayCommandAsync(Save, !CanSave);
1050+
}
1051+
1052+
#nullable disable
1053+
""";
1054+
1055+
var generatorResult = RunGenerator<ViewModelGenerator>(inputCode);
1056+
1057+
AssertGeneratorRunResultAsEqual(expectedCode, generatorResult);
1058+
}
10171059
}

test/Atc.Wpf.SourceGenerators.Tests/Generators/ViewModelGeneratorTests_RelayCommand.cs

+42
Original file line numberDiff line numberDiff line change
@@ -816,4 +816,46 @@ public bool MyCanSave
816816

817817
AssertGeneratorRunResultAsEqual(expectedCode, generatorResult);
818818
}
819+
820+
[Fact]
821+
public void RelayCommand_NoParameter_CanExecute_Inverted()
822+
{
823+
const string inputCode =
824+
"""
825+
namespace TestNamespace;
826+
827+
public partial class TestViewModel : ViewModelBase
828+
{
829+
[RelayCommand(CanExecute = nameof(CanSave), InvertCanExecute = true)]
830+
public void Save()
831+
{
832+
}
833+
834+
public bool CanSave()
835+
{
836+
return true;
837+
}
838+
}
839+
""";
840+
841+
const string expectedCode =
842+
"""
843+
// <auto-generated>
844+
#nullable enable
845+
using Atc.Wpf.Command;
846+
847+
namespace TestNamespace;
848+
849+
public partial class TestViewModel
850+
{
851+
public IRelayCommand SaveCommand => new RelayCommand(Save, !CanSave);
852+
}
853+
854+
#nullable disable
855+
""";
856+
857+
var generatorResult = RunGenerator<ViewModelGenerator>(inputCode);
858+
859+
AssertGeneratorRunResultAsEqual(expectedCode, generatorResult);
860+
}
819861
}

0 commit comments

Comments
 (0)