From 1a575ebc2702a966fd075fa76c670cc9ca5df623 Mon Sep 17 00:00:00 2001 From: Ilya Pospelov Date: Sat, 6 Jul 2024 15:04:17 +0300 Subject: [PATCH] ObservableProperty: private setter --- .../ComponentModel/Models/PropertyInfo.cs | 2 + .../ObservablePropertyGenerator.Execute.cs | 28 +++++++ .../Attributes/WithPrivateSetterAttribute.cs | 40 ++++++++++ .../Test_SourceGeneratorsCodegen.cs | 78 +++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm/ComponentModel/Attributes/WithPrivateSetterAttribute.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 2bf62d0de..b6cd45687 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -20,6 +20,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Whether the old property value is being directly referenced. /// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. +/// Indicates whether to use the private keyword for the generated setter. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, @@ -32,5 +33,6 @@ internal sealed record PropertyInfo( bool NotifyDataErrorInfo, bool IsOldPropertyValueDirectlyReferenced, bool IsReferenceTypeOrUnconstraindTypeParameter, + bool UsePrivateKeywordOnSetAccessor, bool IncludeMemberNotNullOnSetAccessor, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index c6bf051a4..57627cac9 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -117,6 +117,7 @@ public static bool TryGetInfo( bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false; bool hasOrInheritsClassLevelNotifyDataErrorInfo = false; bool hasAnyValidationAttributes = false; + bool usePrivateKeywordOnSetAccessor = false; bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName); token.ThrowIfCancellationRequested(); @@ -179,6 +180,14 @@ public static bool TryGetInfo( continue; } + // Check whether the generated setter should be private + if (SetterShouldBePrivate(attributeData)) + { + usePrivateKeywordOnSetAccessor = true; + + continue; + } + // Track the current attribute for forwarding if it is a validation attribute if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) { @@ -312,6 +321,7 @@ public static bool TryGetInfo( notifyDataErrorInfo, isOldPropertyValueDirectlyReferenced, isReferenceTypeOrUnconstraindTypeParameter, + usePrivateKeywordOnSetAccessor, includeMemberNotNullOnSetAccessor, forwardedAttributes.ToImmutable()); @@ -687,6 +697,16 @@ private static bool TryGetNotifyDataErrorInfo( return false; } + /// + /// Checks whether the generated setter should be private. + /// + /// The instance for fieldSymbol. + /// Whether or not the generated setter should be private. + private static bool SetterShouldBePrivate(AttributeData attributeData) + { + return attributeData.AttributeClass?.HasFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.WithPrivateSetterAttribute") == true; + } + /// /// Checks whether the generated code has to directly reference the old property value. /// @@ -1002,6 +1022,14 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // } AccessorDeclarationSyntax setAccessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(Block(setterIfStatement)); + // Add the private keyword if needed: + // + // private + if (propertyInfo.UsePrivateKeywordOnSetAccessor) + { + setAccessor = setAccessor.AddModifiers(Token(SyntaxKind.PrivateKeyword)); + } + // Add the [MemberNotNull] attribute if needed: // // [MemberNotNull("")] diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/WithPrivateSetterAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/WithPrivateSetterAttribute.cs new file mode 100644 index 000000000..486b4467a --- /dev/null +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/WithPrivateSetterAttribute.cs @@ -0,0 +1,40 @@ +// 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.ComponentModel; + +/// +/// An attribute that can be used to support in generated properties. +/// When this attribute is used, the generated property setter will be private. +/// This can be useful to prevent an unwanted property changes. +/// If this attribute is used in a field without , it is ignored. +/// +/// This attribute can be used as follows: +/// +/// partial class MyViewModel : ObservableObject +/// { +/// [ObservableProperty] +/// [WithPrivateSetter] +/// private string name; +/// } +/// +/// +/// And with this, code analogous to this will be generated: +/// +/// partial class MyViewModel +/// { +/// public string Name +/// { +/// get => name; +/// private set => SetProperty(ref name, value); +/// } +/// } +/// +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +public sealed class WithPrivateSetterAttribute : Attribute +{ +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index b47183c85..5caa228d6 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -2733,6 +2733,84 @@ internal static class __KnownINotifyPropertyChangedArgs VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result), ("__KnownINotifyPropertyChangingArgs.g.cs", null), ("__KnownINotifyPropertyChangedArgs.g.cs", changedArgs)); } + [TestMethod] + public void ObservableProperty_WithPrivateSetter() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [WithPrivateSetter] + private Guid id; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public global::System.Guid Id + { + get => id; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(id, value)) + { + OnIdChanging(value); + OnIdChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Id); + id = value; + OnIdChanged(value); + OnIdChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Id); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnIdChanging(global::System.Guid value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnIdChanging(global::System.Guid oldValue, global::System.Guid newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnIdChanged(global::System.Guid value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnIdChanged(global::System.Guid oldValue, global::System.Guid newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + /// /// Generates the requested sources ///