Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ObservableProperty: private setter #899

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
/// <param name="IsOldPropertyValueDirectlyReferenced">Whether the old property value is being directly referenced.</param>
/// <param name="IsReferenceTypeOrUnconstraindTypeParameter">Indicates whether the property is of a reference type or an unconstrained type parameter.</param>
/// <param name="IncludeMemberNotNullOnSetAccessor">Indicates whether to include nullability annotations on the setter.</param>
/// <param name="UsePrivateKeywordOnSetAccessor">Indicates whether to use the private keyword for the generated setter.</param>
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated property.</param>
internal sealed record PropertyInfo(
string TypeNameWithNullabilityAnnotations,
Expand All @@ -32,5 +33,6 @@ internal sealed record PropertyInfo(
bool NotifyDataErrorInfo,
bool IsOldPropertyValueDirectlyReferenced,
bool IsReferenceTypeOrUnconstraindTypeParameter,
bool UsePrivateKeywordOnSetAccessor,
bool IncludeMemberNotNullOnSetAccessor,
EquatableArray<AttributeInfo> ForwardedAttributes);
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -312,6 +321,7 @@ public static bool TryGetInfo(
notifyDataErrorInfo,
isOldPropertyValueDirectlyReferenced,
isReferenceTypeOrUnconstraindTypeParameter,
usePrivateKeywordOnSetAccessor,
includeMemberNotNullOnSetAccessor,
forwardedAttributes.ToImmutable());

Expand Down Expand Up @@ -687,6 +697,16 @@ private static bool TryGetNotifyDataErrorInfo(
return false;
}

/// <summary>
/// Checks whether the generated setter should be private.
/// </summary>
/// <param name="attributeData">The <see cref="AttributeData"/> instance for fieldSymbol.</param>
/// <returns>Whether or not the generated setter should be private.</returns>
private static bool SetterShouldBePrivate(AttributeData attributeData)
{
return attributeData.AttributeClass?.HasFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.WithPrivateSetterAttribute") == true;
}

/// <summary>
/// Checks whether the generated code has to directly reference the old property value.
/// </summary>
Expand Down Expand Up @@ -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 <SET_ACCESSOR>
if (propertyInfo.UsePrivateKeywordOnSetAccessor)
{
setAccessor = setAccessor.AddModifiers(Token(SyntaxKind.PrivateKeyword));
}

// Add the [MemberNotNull] attribute if needed:
//
// [MemberNotNull("<FIELD_NAME>")]
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// An attribute that can be used to support <see cref="ObservablePropertyAttribute"/> 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 <see cref="ObservablePropertyAttribute"/>, it is ignored.
/// <para>
/// This attribute can be used as follows:
/// <code>
/// partial class MyViewModel : ObservableObject
/// {
/// [ObservableProperty]
/// [WithPrivateSetter]
/// private string name;
/// }
/// </code>
/// </para>
/// And with this, code analogous to this will be generated:
/// <code>
/// partial class MyViewModel
/// {
/// public string Name
/// {
/// get => name;
/// private set => SetProperty(ref name, value);
/// }
/// }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public sealed class WithPrivateSetterAttribute : Attribute
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MyApp
{
/// <inheritdoc/>
partial class MyViewModel
{
/// <inheritdoc cref="id"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public global::System.Guid Id
{
get => id;
private set
{
if (!global::System.Collections.Generic.EqualityComparer<global::System.Guid>.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);
}
}
}

/// <summary>Executes the logic for when <see cref="Id"/> is changing.</summary>
/// <param name="value">The new property value being set.</param>
/// <remarks>This method is invoked right before the value of <see cref="Id"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
partial void OnIdChanging(global::System.Guid value);
/// <summary>Executes the logic for when <see cref="Id"/> is changing.</summary>
/// <param name="oldValue">The previous property value that is being replaced.</param>
/// <param name="newValue">The new property value being set.</param>
/// <remarks>This method is invoked right before the value of <see cref="Id"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
partial void OnIdChanging(global::System.Guid oldValue, global::System.Guid newValue);
/// <summary>Executes the logic for when <see cref="Id"/> just changed.</summary>
/// <param name="value">The new property value that was set.</param>
/// <remarks>This method is invoked right after the value of <see cref="Id"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
partial void OnIdChanged(global::System.Guid value);
/// <summary>Executes the logic for when <see cref="Id"/> just changed.</summary>
/// <param name="oldValue">The previous property value that was replaced.</param>
/// <param name="newValue">The new property value that was set.</param>
/// <remarks>This method is invoked right after the value of <see cref="Id"/> is changed.</remarks>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", <ASSEMBLY_VERSION>)]
partial void OnIdChanged(global::System.Guid oldValue, global::System.Guid newValue);
}
}
""";

VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result));
}

/// <summary>
/// Generates the requested sources
/// </summary>
Expand Down