Skip to content
Open
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 @@ -28,6 +28,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
/// <param name="IsReferenceTypeOrUnconstrainedTypeParameter">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="IncludeRequiresUnreferencedCodeOnSetAccessor">Indicates whether to annotate the setter as requiring unreferenced code.</param>
/// <param name="GenerateOnChanging">Indicates whether to generate property changing partial method.</param>
/// <param name="GenerateOnChanged">Indicates whether to generate property changed partial method.</param>
/// <param name="CallOnChanging">Indicates whether to call property changing method.</param>
/// <param name="CallOnChanged">Indicates whether to call property changed method.</param>
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated property.</param>
internal sealed record PropertyInfo(
SyntaxKind AnnotatedMemberKind,
Expand All @@ -47,4 +51,8 @@ internal sealed record PropertyInfo(
bool IsReferenceTypeOrUnconstrainedTypeParameter,
bool IncludeMemberNotNullOnSetAccessor,
bool IncludeRequiresUnreferencedCodeOnSetAccessor,
bool GenerateOnChanging,
bool GenerateOnChanged,
bool CallOnChanging,
bool CallOnChanged,
EquatableArray<AttributeInfo> ForwardedAttributes);
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ public static bool TryGetInfo(
bool hasOrInheritsClassLevelNotifyDataErrorInfo = false;
bool hasAnyValidationAttributes = false;
bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(memberSymbol, propertyName);
bool generateOnChanging = true, generateOnChanged = true, callOnChanging = true, callOnChanged = true;

token.ThrowIfCancellationRequested();

Expand Down Expand Up @@ -282,6 +283,11 @@ public static bool TryGetInfo(
{
token.ThrowIfCancellationRequested();

if (TryGetGenerateOptional(attributeData, ref generateOnChanging, ref generateOnChanged, ref callOnChanging, ref callOnChanged))
{
continue;
}

// Gather dependent property and command names
if (TryGatherDependentPropertyChangedNames(memberSymbol, attributeData, in propertyChangedNames, in builder) ||
TryGatherDependentCommandNames(memberSymbol, attributeData, in notifiedCommandNames, in builder))
Expand Down Expand Up @@ -415,6 +421,10 @@ public static bool TryGetInfo(
isReferenceTypeOrUnconstrainedTypeParameter,
includeMemberNotNullOnSetAccessor,
includeRequiresUnreferencedCodeOnSetAccessor,
generateOnChanging,
generateOnChanged,
callOnChanging,
callOnChanged,
forwardedAttributes.ToImmutable());

diagnostics = builder.ToImmutable();
Expand Down Expand Up @@ -1140,7 +1150,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
{
getterFieldIdentifierName = "field";
getterFieldExpression = setterFieldExpression = IdentifierName(getterFieldIdentifierName);
}
}
else if (propertyInfo.FieldName == "value")
{
// In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments
Expand Down Expand Up @@ -1185,14 +1195,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
.WithInitializer(EqualsValueClause(setterFieldExpression)))));
}

// Add the OnPropertyChanging() call first:
//
// On<PROPERTY_NAME>Changing(value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
.AddArgumentListArguments(Argument(IdentifierName("value")))));

// Optimization: if the previous property value is not being referenced (which we can check by looking for an existing
// symbol matching the name of either of these generated methods), we can pass a default expression and avoid generating
// a field read, which won't otherwise be elided by Roslyn. Otherwise, we just store the value in a local as usual.
Expand All @@ -1202,13 +1204,24 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
false => Argument(LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword)))
};

// Also call the overload after that:
//
// On<PROPERTY_NAME>Changing(<OLD_PROPERTY_VALUE_EXPRESSION>, value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
.AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value")))));
if (propertyInfo.CallOnChanging)
{
// Add the OnPropertyChanging() call first:
//
// On<PROPERTY_NAME>Changing(value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
.AddArgumentListArguments(Argument(IdentifierName("value")))));

// Also call the overload after that:
//
// On<PROPERTY_NAME>Changing(<OLD_PROPERTY_VALUE_EXPRESSION>, value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
.AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value")))));
}

// Gather the statements to notify dependent properties
foreach (string propertyName in propertyInfo.PropertyChangingNames)
Expand Down Expand Up @@ -1248,21 +1261,24 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
}

// Add the OnPropertyChanged() call:
//
// On<PROPERTY_NAME>Changed(value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed"))
.AddArgumentListArguments(Argument(IdentifierName("value")))));
if (propertyInfo.CallOnChanged)
{
// Add the OnPropertyChanged() call:
//
// On<PROPERTY_NAME>Changed(value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed"))
.AddArgumentListArguments(Argument(IdentifierName("value")))));

// Do the same for the overload, as above:
//
// On<PROPERTY_NAME>Changed(<OLD_PROPERTY_VALUE_EXPRESSION>, value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed"))
.AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value")))));
// Do the same for the overload, as above:
//
// On<PROPERTY_NAME>Changed(<OLD_PROPERTY_VALUE_EXPRESSION>, value);
setterStatements.Add(
ExpressionStatement(
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed"))
.AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value")))));
}

// Gather the statements to notify dependent properties
foreach (string propertyName in propertyInfo.PropertyChangedNames)
Expand Down Expand Up @@ -1456,6 +1472,9 @@ private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo)
/// <returns>The generated <see cref="MemberDeclarationSyntax"/> instances for the <c>OnPropertyChanging</c> and <c>OnPropertyChanged</c> methods.</returns>
public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethodsSyntax(PropertyInfo propertyInfo)
{
if (!propertyInfo.GenerateOnChanged && !propertyInfo.GenerateOnChanging)
return ImmutableArray<MemberDeclarationSyntax>.Empty;

// Get the property type syntax
TypeSyntax parameterType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations);

Expand All @@ -1466,7 +1485,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
// /// <remarks>This method is invoked right before the value of <see cref="<PROPERTY_NAME>"/> is changed.</remarks>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// partial void On<PROPERTY_NAME>Changing(<PROPERTY_TYPE> value);
MemberDeclarationSyntax onPropertyChangingDeclaration =
MemberDeclarationSyntax? onPropertyChangingDeclaration = propertyInfo.GenerateOnChanging ?
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing"))
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
Expand All @@ -1480,7 +1499,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> is changing.</summary>"),
Comment("/// <param name=\"value\">The new property value being set.</param>"),
Comment($"/// <remarks>This method is invoked right before the value of <see cref=\"{propertyInfo.PropertyName}\"/> is changed.</remarks>")), SyntaxKind.OpenBracketToken, TriviaList())))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) : null;

// Get the type for the 'oldValue' parameter (which can be null on first invocation)
TypeSyntax oldValueTypeSyntax = GetPropertyTypeForOldValue(propertyInfo);
Expand All @@ -1493,7 +1512,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
// /// <remarks>This method is invoked right before the value of <see cref="<PROPERTY_NAME>"/> is changed.</remarks>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// partial void On<PROPERTY_NAME>Changing(<OLD_VALUE_TYPE> oldValue, <PROPERTY_TYPE> newValue);
MemberDeclarationSyntax onPropertyChanging2Declaration =
MemberDeclarationSyntax? onPropertyChanging2Declaration = propertyInfo.GenerateOnChanging ?
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing"))
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.AddParameterListParameters(
Expand All @@ -1510,7 +1529,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
Comment("/// <param name=\"oldValue\">The previous property value that is being replaced.</param>"),
Comment("/// <param name=\"newValue\">The new property value being set.</param>"),
Comment($"/// <remarks>This method is invoked right before the value of <see cref=\"{propertyInfo.PropertyName}\"/> is changed.</remarks>")), SyntaxKind.OpenBracketToken, TriviaList())))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) : null;

// Construct the generated method as follows:
//
Expand All @@ -1519,7 +1538,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
// /// <remarks>This method is invoked right after the value of <see cref="<PROPERTY_NAME>"/> is changed.</remarks>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// partial void On<PROPERTY_NAME>Changed(<PROPERTY_TYPE> value);
MemberDeclarationSyntax onPropertyChangedDeclaration =
MemberDeclarationSyntax? onPropertyChangedDeclaration = propertyInfo.GenerateOnChanged ?
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed"))
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
Expand All @@ -1533,7 +1552,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> just changed.</summary>"),
Comment("/// <param name=\"value\">The new property value that was set.</param>"),
Comment($"/// <remarks>This method is invoked right after the value of <see cref=\"{propertyInfo.PropertyName}\"/> is changed.</remarks>")), SyntaxKind.OpenBracketToken, TriviaList())))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) : null;

// Construct the generated method as follows:
//
Expand All @@ -1543,7 +1562,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
// /// <remarks>This method is invoked right after the value of <see cref="<PROPERTY_NAME>"/> is changed.</remarks>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// partial void On<PROPERTY_NAME>Changed(<OLD_VALUE_TYPE> oldValue, <PROPERTY_TYPE> newValue);
MemberDeclarationSyntax onPropertyChanged2Declaration =
MemberDeclarationSyntax? onPropertyChanged2Declaration = propertyInfo.GenerateOnChanged ?
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed"))
.AddModifiers(Token(SyntaxKind.PartialKeyword))
.AddParameterListParameters(
Expand All @@ -1560,13 +1579,28 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
Comment("/// <param name=\"oldValue\">The previous property value that was replaced.</param>"),
Comment("/// <param name=\"newValue\">The new property value that was set.</param>"),
Comment($"/// <remarks>This method is invoked right after the value of <see cref=\"{propertyInfo.PropertyName}\"/> is changed.</remarks>")), SyntaxKind.OpenBracketToken, TriviaList())))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) : null;

return ImmutableArray.Create(
onPropertyChangingDeclaration,
onPropertyChanging2Declaration,
onPropertyChangedDeclaration,
onPropertyChanged2Declaration);
if (propertyInfo.GenerateOnChanged && propertyInfo.GenerateOnChanging)
{
return ImmutableArray.Create(
onPropertyChangingDeclaration!,
onPropertyChanging2Declaration!,
onPropertyChangedDeclaration!,
onPropertyChanged2Declaration!);
}
else if (propertyInfo.GenerateOnChanged)
{
return ImmutableArray.Create(
onPropertyChangedDeclaration!,
onPropertyChanged2Declaration!);
}
else
{
return ImmutableArray.Create(
onPropertyChangingDeclaration!,
onPropertyChanging2Declaration!);
}
}

/// <summary>
Expand Down Expand Up @@ -1752,5 +1786,33 @@ public static string GetGeneratedPropertyName(ISymbol memberSymbol)

return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}";
}

/// <summary>
/// Tries to gather generate and call optional from ObservablePropertyAttribute.
/// </summary>
/// <param name="attributeData">The <see cref="AttributeData"/> instance.</param>
/// <param name="generateOnChanging">Field which indicates whether to generate property On{PropertyName}Changing partial method.</param>
/// <param name="generateOnChanged">Field which indicates whether to generate property On{PropertyName}Changed partial method.</param>
/// <param name="callOnChanging">Field which indicates whether to call property On{PropertyName}Changing method.</param>
/// <param name="callOnChanged">Field which indicates whether to call property On{PropertyName}Changed method.</param>
/// <returns>Whether or not <paramref name="attributeData"/> was ObservablePropertyAttribute.</returns>
private static bool TryGetGenerateOptional(AttributeData attributeData, ref bool generateOnChanging, ref bool generateOnChanged, ref bool callOnChanging, ref bool callOnChanged)
{
bool value;
if (attributeData.AttributeClass?.HasFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") == true)
{
if (attributeData.TryGetNamedArgument<bool>("GenerateOnChanging", out value))
generateOnChanging = value;
if (attributeData.TryGetNamedArgument<bool>("GenerateOnChanged", out value))
generateOnChanged = value;
if (attributeData.TryGetNamedArgument<bool>("CallOnChanging", out value))
callOnChanging = value;
if (attributeData.TryGetNamedArgument<bool>("CallOnChanged", out value))
callOnChanged = value;
return true;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,23 @@ namespace CommunityToolkit.Mvvm.ComponentModel;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class ObservablePropertyAttribute : Attribute
{
/// <summary>
/// Indicates whether to generate property On{PropertyName}Changing partial method.
/// </summary>
public bool GenerateOnChanging { get; set; } = true;

/// <summary>
/// Indicates whether to generate property On{PropertyName}Changed partial method.
/// </summary>
public bool GenerateOnChanged { get; set; } = true;

/// <summary>
/// Indicates whether to call property On{PropertyName}Changing method.
/// </summary>
public bool CallOnChanging { get; set; } = true;

/// <summary>
/// Indicates whether to call property On{PropertyName}Changed method.
/// </summary>
public bool CallOnChanged { get; set; } = true;
}
Loading