diff --git a/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index e6d40de29..d3f51330c 100644 --- a/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -181,9 +181,10 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( string propertyName = GetGeneratedPropertyName(fieldSymbol); INamedTypeSymbol alsoNotifyChangeForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute")!; + INamedTypeSymbol alsoNotifyCanExecuteForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute")!; INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"); - List dependentPropertyNotificationStatements = new(); + List dependentNotificationStatements = new(); List validationAttributes = new(); foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) @@ -191,48 +192,30 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( // Add dependent property notifications, if needed if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyChangeForAttributeSymbol)) { - foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments) + foreach (string dependentPropertyName in attributeData.GetConstructorArguments()) { - if (attributeArgument.IsNull) - { - continue; - } - - if (attributeArgument.Kind == TypedConstantKind.Primitive && - attributeArgument.Value is string dependentPropertyName) - { - propertyChangedNames.Add(dependentPropertyName); - - // OnPropertyChanged("OtherPropertyName"); - dependentPropertyNotificationStatements.Add(ExpressionStatement( - InvocationExpression(IdentifierName("OnPropertyChanged")) - .AddArgumentListArguments(Argument(MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), - IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}")))))); - } - else if (attributeArgument.Kind == TypedConstantKind.Array) - { - foreach (TypedConstant nestedAttributeArgument in attributeArgument.Values) - { - if (nestedAttributeArgument.IsNull) - { - continue; - } - - string currentPropertyName = (string)nestedAttributeArgument.Value!; - - propertyChangedNames.Add(currentPropertyName); - - // Additional property names - dependentPropertyNotificationStatements.Add(ExpressionStatement( - InvocationExpression(IdentifierName("OnPropertyChanged")) - .AddArgumentListArguments(Argument(MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), - IdentifierName($"{currentPropertyName}{nameof(PropertyChangedEventArgs)}")))))); - } - } + propertyChangedNames.Add(dependentPropertyName); + + // OnPropertyChanged(""); + dependentNotificationStatements.Add(ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanged")) + .AddArgumentListArguments(Argument(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), + IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}")))))); + } + } + else if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyCanExecuteForAttributeSymbol)) + { + // Add dependent relay command notifications, if needed + foreach (string commandName in attributeData.GetConstructorArguments()) + { + // .NotifyCanExecuteChanged(); + dependentNotificationStatements.Add(ExpressionStatement( + InvocationExpression(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(commandName), + IdentifierName("NotifyCanExecuteChanged"))))); } } else if (validationAttributeSymbol is not null && @@ -276,14 +259,18 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( // // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(this., value)) // { - // OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs); // Optional + // OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangingEventArgs); // Optional // this. = value; - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs); + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); // ValidateProperty(value, ); - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs); + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); // Optional + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); // ... - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs); + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); + // .NotifyCanExecuteChanged(); // Optional + // .NotifyCanExecuteChanged(); + // ... + // .NotifyCanExecuteChanged(); // } // // The reason why the code is explicitly generated instead of just calling ObservableValidator.SetProperty() is so that we can @@ -327,7 +314,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( .AddArgumentListArguments( Argument(IdentifierName("value")), Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName)))))) - .AddStatements(dependentPropertyNotificationStatements.ToArray()))); + .AddStatements(dependentNotificationStatements.ToArray()))); } } else @@ -367,19 +354,23 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}")))))); // Add the dependent property notifications at the end - updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentPropertyNotificationStatements.ToArray()); + updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentNotificationStatements.ToArray()); // Generate the inner setter block as follows: // // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) // { - // OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs); // Optional + // OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangingEventArgs); // Optional // = value; - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs); - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs); + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); // Optional + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); + // ... + // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyChangedEventArgs); + // .NotifyCanExecuteChanged(); // Optional + // .NotifyCanExecuteChanged(); // ... - // OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs); + // .NotifyCanExecuteChanged(); // } setterBlock = Block( IfStatement( diff --git a/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs index 235bfc406..914badff1 100644 --- a/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -69,6 +69,42 @@ public static bool TryGetNamedArgument(this AttributeData attributeData, stri return false; } + /// + /// Enumerates all items in a flattened sequence of constructor arguments for a given instance. + /// + /// The type of constructor arguments to retrieve. + /// The target instance to get the arguments from. + /// A sequence of all constructor arguments of the specified type from . + [Pure] + public static IEnumerable GetConstructorArguments(this AttributeData attributeData) + { + static IEnumerable Enumerate(IEnumerable constants) + { + foreach (TypedConstant constant in constants) + { + if (constant.IsNull) + { + continue; + } + + if (constant.Kind == TypedConstantKind.Primitive && + constant.Value is T value) + { + yield return value; + } + else if (constant.Kind == TypedConstantKind.Array) + { + foreach (T item in Enumerate(constant.Values)) + { + yield return item; + } + } + } + } + + return Enumerate(attributeData.ConstructorArguments); + } + /// /// Creates an node that is equivalent to the input instance. /// diff --git a/CommunityToolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyCanExecuteForAttribute.cs b/CommunityToolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyCanExecuteForAttribute.cs new file mode 100644 index 000000000..03e4e7b7a --- /dev/null +++ b/CommunityToolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyCanExecuteForAttribute.cs @@ -0,0 +1,80 @@ +// 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; +using System.Linq; +using CommunityToolkit.Mvvm.Input; + +namespace CommunityToolkit.Mvvm.ComponentModel; + +/// +/// An attribute that can be used to support properties in generated properties. When this attribute is +/// used, the generated property setter will also call for the properties specified +/// in the attribute data, causing the validation logic for the command to be executed again. This can be useful to keep the code compact +/// when there are one or more dependent commands that should also be notified when a property is updated. If this attribute is used in +/// a field without , it is ignored (just like ). +/// +/// In order to use this attribute, the target property has to implement the interface. +/// +/// +/// This attribute can be used as follows: +/// +/// partial class MyViewModel : ObservableObject +/// { +/// [ObservableProperty] +/// [AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))] +/// private string name; +/// +/// public IRelayCommand GreetUserCommand { get; } +/// } +/// +/// +/// And with this, code analogous to this will be generated: +/// +/// partial class MyViewModel +/// { +/// public string Name +/// { +/// get => name; +/// set +/// { +/// if (SetProperty(ref name, value)) +/// { +/// GreetUserCommand.NotifyCanExecuteChanged(); +/// } +/// } +/// } +/// } +/// +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)] +public sealed class AlsoNotifyCanExecuteForAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the command to also notify when the annotated property changes. + public AlsoNotifyCanExecuteForAttribute(string commandName) + { + CommandNames = new[] { commandName }; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the property to also notify when the annotated property changes. + /// + /// The other command names to also notify when the annotated property changes. This parameter can optionally + /// be used to indicate a series of dependent commands from the same attribute, to keep the code more compact. + /// + public AlsoNotifyCanExecuteForAttribute(string commandName, params string[] otherCommandNames) + { + CommandNames = new[] { commandName }.Concat(otherCommandNames).ToArray(); + } + + /// + /// Gets the command names to also notify when the annotated property changes. + /// + public string[] CommandNames { get; } +} diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 2902ad7ac..53cc533cd 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Reflection; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.Mvvm.UnitTests; @@ -19,7 +20,7 @@ public partial class Test_ObservablePropertyAttribute [TestMethod] public void Test_ObservablePropertyAttribute_Events() { - SampleModel? model = new(); + SampleModel model = new(); (PropertyChangingEventArgs, int) changing = default; (PropertyChangedEventArgs, int) changed = default; @@ -58,7 +59,7 @@ public void Test_ObservablePropertyAttribute_Events() [TestMethod] public void Test_ObservablePropertyAttributeWithinRegion_Events() { - SampleModel? model = new(); + SampleModel model = new(); (PropertyChangingEventArgs, int) changing = default; (PropertyChangedEventArgs, int) changed = default; @@ -97,7 +98,7 @@ public void Test_ObservablePropertyAttributeWithinRegion_Events() [TestMethod] public void Test_ObservablePropertyAttributeRightBelowRegion_Events() { - SampleModel? model = new(); + SampleModel model = new(); (PropertyChangingEventArgs, string?) changing = default; (PropertyChangedEventArgs, string?) changed = default; @@ -135,7 +136,7 @@ public void Test_ObservablePropertyAttributeRightBelowRegion_Events() [TestMethod] public void Test_AlsoNotifyChangeForAttribute_Events() { - DependentPropertyModel? model = new(); + DependentPropertyModel model = new(); List propertyNames = new(); @@ -150,7 +151,7 @@ public void Test_AlsoNotifyChangeForAttribute_Events() [TestMethod] public void Test_ValidationAttributes() { - PropertyInfo? nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name))!; + PropertyInfo nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name))!; Assert.IsNotNull(nameProperty.GetCustomAttribute()); Assert.IsNotNull(nameProperty.GetCustomAttribute()); @@ -158,17 +159,17 @@ public void Test_ValidationAttributes() Assert.IsNotNull(nameProperty.GetCustomAttribute()); Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100); - PropertyInfo? ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age))!; + PropertyInfo ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age))!; Assert.IsNotNull(ageProperty.GetCustomAttribute()); Assert.AreEqual(ageProperty.GetCustomAttribute()!.Minimum, 0); Assert.AreEqual(ageProperty.GetCustomAttribute()!.Maximum, 120); - PropertyInfo? emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email))!; + PropertyInfo emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email))!; Assert.IsNotNull(emailProperty.GetCustomAttribute()); - PropertyInfo? comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat))!; + PropertyInfo comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat))!; TestValidationAttribute testAttribute = comboProperty.GetCustomAttribute()!; @@ -195,7 +196,7 @@ public void Test_ValidationAttributes() [TestMethod] public void Test_ObservablePropertyWithValueNamedField() { - ModelWithValueProperty? model = new(); + ModelWithValueProperty model = new(); List propertyNames = new(); @@ -212,7 +213,7 @@ public void Test_ObservablePropertyWithValueNamedField() [TestMethod] public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributes() { - ModelWithValuePropertyWithValidation? model = new(); + ModelWithValuePropertyWithValidation model = new(); List propertyNames = new(); @@ -229,7 +230,7 @@ public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributes( [TestMethod] public void Test_GeneratedPropertiesWithValidationAttributesOverFields() { - ViewModelWithValidatableGeneratedProperties? model = new(); + ViewModelWithValidatableGeneratedProperties model = new(); List propertyNames = new(); @@ -254,6 +255,24 @@ public void Test_GeneratedPropertiesWithValidationAttributesOverFields() CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.Last) }, validationErrors[1].MemberNames.ToArray()); } + [TestMethod] + public void Test_AlsoNotifyChangeFor() + { + DependentPropertyModel model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Surname = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + public partial class SampleModel : ObservableObject { /// @@ -283,11 +302,14 @@ public sealed partial class DependentPropertyModel [ObservableProperty] [AlsoNotifyChangeFor(nameof(FullName), nameof(Alias))] + [AlsoNotifyCanExecuteFor(nameof(MyCommand))] private string? surname; public string FullName => $"{Name} {Surname}"; public string Alias => $"{Name?.ToLower()}{Surname?.ToLower()}"; + + public RelayCommand MyCommand { get; } = new(() => { }); } public partial class MyFormViewModel : ObservableValidator