mirror of
https://github.com/CommunityToolkit/dotnet.git
synced 2026-10-20 06:07:37 +08:00
764 lines
42 KiB
C#
764 lines
42 KiB
C#
// 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.Collections.Immutable;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
|
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
|
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
|
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
|
|
|
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
|
|
|
/// <inheritdoc/>
|
|
partial class ObservablePropertyGenerator
|
|
{
|
|
/// <summary>
|
|
/// A container for all the logic for <see cref="ObservablePropertyGenerator"/>.
|
|
/// </summary>
|
|
internal static class Execute
|
|
{
|
|
/// <summary>
|
|
/// Processes a given field.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
|
|
/// <returns>The resulting <see cref="PropertyInfo"/> instance for <paramref name="fieldSymbol"/>, if successful.</returns>
|
|
public static PropertyInfo? TryGetInfo(IFieldSymbol fieldSymbol, out ImmutableArray<Diagnostic> diagnostics)
|
|
{
|
|
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
|
|
|
|
// Validate the target type
|
|
if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging))
|
|
{
|
|
builder.Add(
|
|
InvalidContainingTypeForObservablePropertyFieldError,
|
|
fieldSymbol,
|
|
fieldSymbol.ContainingType,
|
|
fieldSymbol.Name);
|
|
|
|
diagnostics = builder.ToImmutable();
|
|
|
|
return null;
|
|
}
|
|
|
|
// Get the property type and name
|
|
string typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations();
|
|
string fieldName = fieldSymbol.Name;
|
|
string propertyName = GetGeneratedPropertyName(fieldSymbol);
|
|
|
|
// Check for name collisions
|
|
if (fieldName == propertyName)
|
|
{
|
|
builder.Add(
|
|
ObservablePropertyNameCollisionError,
|
|
fieldSymbol,
|
|
fieldSymbol.ContainingType,
|
|
fieldSymbol.Name);
|
|
|
|
diagnostics = builder.ToImmutable();
|
|
|
|
// If the generated property would collide, skip generating it entirely. This makes sure that
|
|
// users only get the helpful diagnostic about the collsiion, and not the normal compiler error
|
|
// about a definition for "Property" already existing on the target type, which might be confusing.
|
|
return null;
|
|
}
|
|
|
|
ImmutableArray<string>.Builder propertyChangedNames = ImmutableArray.CreateBuilder<string>();
|
|
ImmutableArray<string>.Builder propertyChangingNames = ImmutableArray.CreateBuilder<string>();
|
|
ImmutableArray<string>.Builder notifiedCommandNames = ImmutableArray.CreateBuilder<string>();
|
|
ImmutableArray<AttributeInfo>.Builder validationAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
|
|
bool alsoBroadcastChange = false;
|
|
|
|
// Track the property changing event for the property, if the type supports it
|
|
if (shouldInvokeOnPropertyChanging)
|
|
{
|
|
propertyChangingNames.Add(propertyName);
|
|
}
|
|
|
|
// The current property is always notified
|
|
propertyChangedNames.Add(propertyName);
|
|
|
|
// Gather attributes info
|
|
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
|
|
{
|
|
// Gather dependent property and command names
|
|
if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, propertyChangedNames, builder) ||
|
|
TryGatherDependentCommandNames(fieldSymbol, attributeData, notifiedCommandNames, builder) ||
|
|
TryGetIsBroadcastingChanges(fieldSymbol, attributeData, builder, out alsoBroadcastChange))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Track the current validation attribute, if applicable
|
|
if (attributeData.AttributeClass?.InheritsFromFullyQualifiedName("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
|
|
{
|
|
validationAttributes.Add(AttributeInfo.From(attributeData));
|
|
}
|
|
}
|
|
|
|
// Log the diagnostics if needed
|
|
if (validationAttributes.Count > 0 &&
|
|
!fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
|
|
{
|
|
builder.Add(
|
|
MissingObservableValidatorInheritanceError,
|
|
fieldSymbol,
|
|
fieldSymbol.ContainingType,
|
|
fieldSymbol.Name,
|
|
validationAttributes.Count);
|
|
|
|
// Remove all validation attributes so that the generated code doesn't cause a build error about the
|
|
// "ValidateProperty" method not existing (as the type doesn't inherit from ObservableValidator). The
|
|
// compilation will still fail due to this diagnostics, but with just this easier to understand error.
|
|
validationAttributes.Clear();
|
|
}
|
|
|
|
diagnostics = builder.ToImmutable();
|
|
|
|
return new(
|
|
typeNameWithNullabilityAnnotations,
|
|
fieldName,
|
|
propertyName,
|
|
propertyChangingNames.ToImmutable(),
|
|
propertyChangedNames.ToImmutable(),
|
|
notifiedCommandNames.ToImmutable(),
|
|
alsoBroadcastChange,
|
|
validationAttributes.ToImmutable());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the diagnostics for a field with invalid attribute uses.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <returns>The resulting <see cref="Diagnostic"/> instance for <paramref name="fieldSymbol"/>.</returns>
|
|
public static Diagnostic GetDiagnosticForFieldWithOrphanedDependentAttributes(IFieldSymbol fieldSymbol)
|
|
{
|
|
return FieldWithOrphanedDependentObservablePropertyAttributesError.CreateDiagnostic(
|
|
fieldSymbol,
|
|
fieldSymbol.ContainingType,
|
|
fieldSymbol.Name);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the containing type for a given field being annotated.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <param name="shouldInvokeOnPropertyChanging">Whether or not property changing events should also be raised.</param>
|
|
/// <returns>Whether or not the containing type for <paramref name="fieldSymbol"/> is valid.</returns>
|
|
private static bool IsTargetTypeValid(
|
|
IFieldSymbol fieldSymbol,
|
|
out bool shouldInvokeOnPropertyChanging)
|
|
{
|
|
// The [ObservableProperty] attribute can only be used in types that are known to expose the necessary OnPropertyChanged and OnPropertyChanging methods.
|
|
// That means that the containing type for the field needs to match one of the following conditions:
|
|
// - It inherits from ObservableObject (in which case it also implements INotifyPropertyChanging).
|
|
// - It has the [ObservableObject] attribute (on itself or any of its base types).
|
|
// - It has the [INotifyPropertyChanged] attribute (on itself or any of its base types).
|
|
bool isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject");
|
|
bool hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute");
|
|
bool hasINotifyPropertyChangedAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute");
|
|
|
|
shouldInvokeOnPropertyChanging = isObservableObject || hasObservableObjectAttribute;
|
|
|
|
return isObservableObject || hasObservableObjectAttribute || hasINotifyPropertyChangedAttribute;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to gather dependent properties from the given attribute.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
|
|
/// <param name="propertyChangedNames">The target collection of dependent property names to populate.</param>
|
|
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
|
/// <returns>Whether or not <paramref name="attributeData"/> was an attribute containing any dependent properties.</returns>
|
|
private static bool TryGatherDependentPropertyChangedNames(
|
|
IFieldSymbol fieldSymbol,
|
|
AttributeData attributeData,
|
|
ImmutableArray<string>.Builder propertyChangedNames,
|
|
ImmutableArray<Diagnostic>.Builder diagnostics)
|
|
{
|
|
// Validates a property name using existing properties
|
|
bool IsPropertyNameValid(string propertyName)
|
|
{
|
|
return fieldSymbol.ContainingType.GetAllMembers(propertyName).OfType<IPropertySymbol>().Any();
|
|
}
|
|
|
|
// Validate a property name including generated properties too
|
|
bool IsPropertyNameValidWithGeneratedMembers(string propertyName)
|
|
{
|
|
foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers())
|
|
{
|
|
if (member is IFieldSymbol otherFieldSymbol &&
|
|
!SymbolEqualityComparer.Default.Equals(fieldSymbol, otherFieldSymbol) &&
|
|
otherFieldSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") &&
|
|
propertyName == GetGeneratedPropertyName(otherFieldSymbol))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute") == true)
|
|
{
|
|
foreach (string? dependentPropertyName in attributeData.GetConstructorArguments<string>())
|
|
{
|
|
// Each target must be a (not null and not empty) string matching the name of a property from the containing type
|
|
// of the annotated field being processed, or alternatively it must match the name of a property being generated.
|
|
if (dependentPropertyName is not (null or "") &&
|
|
(IsPropertyNameValid(dependentPropertyName) ||
|
|
IsPropertyNameValidWithGeneratedMembers(dependentPropertyName)))
|
|
{
|
|
propertyChangedNames.Add(dependentPropertyName);
|
|
}
|
|
else
|
|
{
|
|
diagnostics.Add(
|
|
AlsoNotifyChangeForInvalidTargetError,
|
|
fieldSymbol,
|
|
dependentPropertyName ?? "",
|
|
fieldSymbol.ContainingType);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to gather dependent commands from the given attribute.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
|
|
/// <param name="notifiedCommandNames">The target collection of dependent command names to populate.</param>
|
|
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
|
/// <returns>Whether or not <paramref name="attributeData"/> was an attribute containing any dependent commands.</returns>
|
|
private static bool TryGatherDependentCommandNames(
|
|
IFieldSymbol fieldSymbol,
|
|
AttributeData attributeData,
|
|
ImmutableArray<string>.Builder notifiedCommandNames,
|
|
ImmutableArray<Diagnostic>.Builder diagnostics)
|
|
{
|
|
// Validates a command name using existing properties
|
|
bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembersToo)
|
|
{
|
|
// Each target must be a string matching the name of a property from the containing type of the annotated field, and the
|
|
// property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code).
|
|
if (fieldSymbol.ContainingType.GetAllMembers(commandName).OfType<IPropertySymbol>().FirstOrDefault() is IPropertySymbol propertySymbol)
|
|
{
|
|
// If there is a property member with the specified name, check that it's valid. If it isn't, the
|
|
// target is definitely not valid, and the additional checks below can just be skipped. The property
|
|
// is valid if it's of type IRelayCommand, or it has IRelayCommand in the set of all interfaces.
|
|
if (propertySymbol.Type is INamedTypeSymbol typeSymbol &&
|
|
(typeSymbol.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.IRelayCommand") ||
|
|
typeSymbol.HasInterfaceWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.IRelayCommand")))
|
|
{
|
|
shouldLookForGeneratedMembersToo = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
// If a property with this name exists but is not valid, the search should stop immediately, as
|
|
// the target is already known not to be valid, so there is no reason to look for other members.
|
|
shouldLookForGeneratedMembersToo = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
shouldLookForGeneratedMembersToo = true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Validate a command name including generated command too
|
|
bool IsCommandNameValidWithGeneratedMembers(string commandName)
|
|
{
|
|
foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers())
|
|
{
|
|
if (member is IMethodSymbol methodSymbol &&
|
|
methodSymbol.HasAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.ICommandAttribute") &&
|
|
commandName == ICommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol).PropertyName)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") == true)
|
|
{
|
|
foreach (string? commandName in attributeData.GetConstructorArguments<string>())
|
|
{
|
|
// Each command must be a (not null and not empty) string matching the name of an existing command from the containing
|
|
// type (just like for properties), or it must match a generated command. The only caveat is the case where a property
|
|
// with the requested name does exist, but it is not of the right type. In that case the search should stop immediately.
|
|
if (commandName is not (null or "") &&
|
|
(IsCommandNameValid(commandName, out bool shouldLookForGeneratedMembersToo) ||
|
|
shouldLookForGeneratedMembersToo && IsCommandNameValidWithGeneratedMembers(commandName)))
|
|
{
|
|
notifiedCommandNames.Add(commandName);
|
|
}
|
|
else
|
|
{
|
|
diagnostics.Add(
|
|
AlsoNotifyCanExecuteForInvalidTargetError,
|
|
fieldSymbol,
|
|
commandName ?? "",
|
|
fieldSymbol.ContainingType);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether a given generated property should also broadcast changes.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="fieldSymbol"/>.</param>
|
|
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
|
/// <param name="alsoBroadcastChange">Whether or not the resulting property should also broadcast changes.</param>
|
|
/// <returns>Whether or not the generated property for <paramref name="fieldSymbol"/> used <c>[AlsoBroadcastChange]</c>.</returns>
|
|
private static bool TryGetIsBroadcastingChanges(
|
|
IFieldSymbol fieldSymbol,
|
|
AttributeData attributeData,
|
|
ImmutableArray<Diagnostic>.Builder diagnostics,
|
|
out bool alsoBroadcastChange)
|
|
{
|
|
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoBroadcastChangeAttribute") == true)
|
|
{
|
|
// If the containing type is valid, track it
|
|
if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") ||
|
|
fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
|
|
{
|
|
alsoBroadcastChange = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Otherwise just emit the diagnostic and then ignore the attribute
|
|
diagnostics.Add(
|
|
InvalidContainingTypeForAlsoBroadcastChangeFieldError,
|
|
fieldSymbol,
|
|
fieldSymbol.ContainingType,
|
|
fieldSymbol.Name);
|
|
|
|
alsoBroadcastChange = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
alsoBroadcastChange = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
|
|
/// </summary>
|
|
/// <param name="names">The sequence of property names to cache args for.</param>
|
|
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
|
public static CompilationUnitSyntax? GetKnownPropertyChangingArgsSyntax(ImmutableArray<string> names)
|
|
{
|
|
return GetKnownPropertyChangingOrChangedArgsSyntax(
|
|
"__KnownINotifyPropertyChangingArgs",
|
|
"global::System.ComponentModel.PropertyChangingEventArgs",
|
|
names);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changed notifications.
|
|
/// </summary>
|
|
/// <param name="names">The sequence of property names to cache args for.</param>
|
|
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
|
public static CompilationUnitSyntax? GetKnownPropertyChangedArgsSyntax(ImmutableArray<string> names)
|
|
{
|
|
return GetKnownPropertyChangingOrChangedArgsSyntax(
|
|
"__KnownINotifyPropertyChangedArgs",
|
|
"global::System.ComponentModel.PropertyChangedEventArgs",
|
|
names);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="MemberDeclarationSyntax"/> instance for the input field.
|
|
/// </summary>
|
|
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
|
|
/// <returns>The generated <see cref="MemberDeclarationSyntax"/> instance for <paramref name="propertyInfo"/>.</returns>
|
|
public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInfo)
|
|
{
|
|
ImmutableArray<StatementSyntax>.Builder setterStatements = ImmutableArray.CreateBuilder<StatementSyntax>();
|
|
|
|
// Get the property type syntax
|
|
TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations);
|
|
|
|
// In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments
|
|
// with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter.
|
|
ExpressionSyntax fieldExpression = propertyInfo.FieldName switch
|
|
{
|
|
"value" => MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("value")),
|
|
string name => IdentifierName(name)
|
|
};
|
|
|
|
if (propertyInfo.AlsoBroadcastChange)
|
|
{
|
|
// If broadcasting changes are required, also store the old value.
|
|
// This code generates a statement as follows:
|
|
//
|
|
// <PROPERTY_TYPE> __oldValue = <FIELD_EXPRESSIONS>;
|
|
setterStatements.Add(
|
|
LocalDeclarationStatement(
|
|
VariableDeclaration(propertyType)
|
|
.AddVariables(
|
|
VariableDeclarator(Identifier("__oldValue"))
|
|
.WithInitializer(EqualsValueClause(fieldExpression)))));
|
|
}
|
|
|
|
// Add the OnPropertyChanging() call first:
|
|
//
|
|
// On<PROPERTY_NAME>Changing(value);
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing"))
|
|
.AddArgumentListArguments(Argument(IdentifierName("value")))));
|
|
|
|
// Gather the statements to notify dependent properties
|
|
foreach (string propertyName in propertyInfo.PropertyChangingNames)
|
|
{
|
|
// This code generates a statement as follows:
|
|
//
|
|
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.<PROPERTY_NAME>);
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(IdentifierName("OnPropertyChanging"))
|
|
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs"),
|
|
IdentifierName(propertyName))))));
|
|
}
|
|
|
|
// Add the assignment statement:
|
|
//
|
|
// <FIELD_EXPRESSION> = value;
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
AssignmentExpression(
|
|
SyntaxKind.SimpleAssignmentExpression,
|
|
fieldExpression,
|
|
IdentifierName("value"))));
|
|
|
|
// If there are validation attributes, add a call to ValidateProperty:
|
|
//
|
|
// ValidateProperty(value, <PROPERTY_NAME>);
|
|
if (propertyInfo.ValidationAttributes.Length > 0)
|
|
{
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(IdentifierName("ValidateProperty"))
|
|
.AddArgumentListArguments(
|
|
Argument(IdentifierName("value")),
|
|
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")))));
|
|
|
|
// Gather the statements to notify dependent properties
|
|
foreach (string propertyName in propertyInfo.PropertyChangedNames)
|
|
{
|
|
// This code generates a statement as follows:
|
|
//
|
|
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.<PROPERTY_NAME>);
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(IdentifierName("OnPropertyChanged"))
|
|
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs"),
|
|
IdentifierName(propertyName))))));
|
|
}
|
|
|
|
// Gather the statements to notify commands
|
|
foreach (string commandName in propertyInfo.NotifiedCommandNames)
|
|
{
|
|
// This code generates a statement as follows:
|
|
//
|
|
// <COMMAND_NAME>.NotifyCanExecuteChanged();
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
IdentifierName(commandName),
|
|
IdentifierName("NotifyCanExecuteChanged")))));
|
|
}
|
|
|
|
// Also broadcast the change, if requested
|
|
if (propertyInfo.AlsoBroadcastChange)
|
|
{
|
|
// This code generates a statement as follows:
|
|
//
|
|
// Broadcast(__oldValue, value, "<PROPERTY_NAME>");
|
|
setterStatements.Add(
|
|
ExpressionStatement(
|
|
InvocationExpression(IdentifierName("Broadcast"))
|
|
.AddArgumentListArguments(
|
|
Argument(IdentifierName("__oldValue")),
|
|
Argument(IdentifierName("value")),
|
|
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
|
|
}
|
|
|
|
// Generate the inner setter block as follows:
|
|
//
|
|
// if (!global::System.Collections.Generic.EqualityComparer<<PROPERTY_TYPE>>.Default.Equals(<FIELD_EXPRESSION>, value))
|
|
// {
|
|
// <STATEMENTS>
|
|
// }
|
|
IfStatementSyntax setterIfStatement =
|
|
IfStatement(
|
|
PrefixUnaryExpression(
|
|
SyntaxKind.LogicalNotExpression,
|
|
InvocationExpression(
|
|
MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
|
|
.AddTypeArgumentListArguments(propertyType),
|
|
IdentifierName("Default")),
|
|
IdentifierName("Equals")))
|
|
.AddArgumentListArguments(
|
|
Argument(fieldExpression),
|
|
Argument(IdentifierName("value")))),
|
|
Block(setterStatements));
|
|
|
|
// Prepare the validation attributes, if any
|
|
ImmutableArray<AttributeListSyntax> validationAttributes =
|
|
propertyInfo.ValidationAttributes
|
|
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
|
|
.ToImmutableArray();
|
|
|
|
// Construct the generated property as follows:
|
|
//
|
|
// /// <inheritdoc cref="<FIELD_NAME>"/>
|
|
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
|
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
|
// <VALIDATION_ATTRIBUTES>
|
|
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
|
|
// {
|
|
// get => <FIELD_NAME>;
|
|
// set
|
|
// {
|
|
// <BODY>
|
|
// }
|
|
// }
|
|
return
|
|
PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName))
|
|
.AddAttributeLists(
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
|
.AddArgumentListArguments(
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
|
|
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{propertyInfo.FieldName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())),
|
|
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
|
|
.AddAttributeLists(validationAttributes.ToArray())
|
|
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
|
.AddAccessorListAccessors(
|
|
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
|
|
.WithExpressionBody(ArrowExpressionClause(IdentifierName(propertyInfo.FieldName)))
|
|
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
|
|
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
|
|
.WithBody(Block(setterIfStatement)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="MemberDeclarationSyntax"/> instances for the <c>OnPropertyChanging</c> and <c>OnPropertyChanged</c> methods for the input field.
|
|
/// </summary>
|
|
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
|
|
/// <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)
|
|
{
|
|
// Get the property type syntax
|
|
TypeSyntax parameterType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations);
|
|
|
|
// Construct the generated method as follows:
|
|
//
|
|
// /// <summary>Executes the logic for when <see cref="<PROPERTY_NAME>"/> is changing.</summary>
|
|
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
|
// partial void On<PROPERTY_NAME>Changing(<PROPERTY_TYPE> value);
|
|
MemberDeclarationSyntax onPropertyChangingDeclaration =
|
|
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing"))
|
|
.AddModifiers(Token(SyntaxKind.PartialKeyword))
|
|
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
|
|
.AddAttributeLists(
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
|
.AddArgumentListArguments(
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
|
|
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> is changing.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
|
|
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
|
|
|
|
// Construct the generated method as follows:
|
|
//
|
|
// /// <summary>Executes the logic for when <see cref="<PROPERTY_NAME>"/> ust changed.</summary>
|
|
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
|
// partial void On<PROPERTY_NAME>Changed(<PROPERTY_TYPE> value);
|
|
MemberDeclarationSyntax onPropertyChangedDeclaration =
|
|
MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed"))
|
|
.AddModifiers(Token(SyntaxKind.PartialKeyword))
|
|
.AddParameterListParameters(Parameter(Identifier("value")).WithType(parameterType))
|
|
.AddAttributeLists(
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
|
.AddArgumentListArguments(
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
|
|
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>Executes the logic for when <see cref=\"{propertyInfo.PropertyName}\"/> just changed.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
|
|
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
|
|
|
|
return ImmutableArray.Create(onPropertyChangingDeclaration, onPropertyChangedDeclaration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args of a specified type.
|
|
/// </summary>
|
|
/// <param name="ContainingTypeName">The name of the generated type.</param>
|
|
/// <param name="ArgsTypeName">The argument type name.</param>
|
|
/// <param name="names">The sequence of property names to cache args for.</param>
|
|
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
|
private static CompilationUnitSyntax? GetKnownPropertyChangingOrChangedArgsSyntax(
|
|
string ContainingTypeName,
|
|
string ArgsTypeName,
|
|
ImmutableArray<string> names)
|
|
{
|
|
if (names.IsEmpty)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// This code takes a class symbol and produces a compilation unit as follows:
|
|
//
|
|
// // <auto-generated/>
|
|
// #pragma warning disable
|
|
// #nullable enable
|
|
// namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
|
|
// {
|
|
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
|
// [global::System.Diagnostics.DebuggerNonUserCode]
|
|
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
|
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
|
// [global::System.Obsolete("This type is not intended to be used directly by user code")]
|
|
// internal static class <CONTAINING_TYPE_NAME>
|
|
// {
|
|
// <FIELDS>
|
|
// }
|
|
// }
|
|
return
|
|
CompilationUnit().AddMembers(
|
|
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
|
Comment("// <auto-generated/>"),
|
|
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)),
|
|
Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)))).AddMembers(
|
|
ClassDeclaration(ContainingTypeName).AddModifiers(
|
|
Token(SyntaxKind.InternalKeyword),
|
|
Token(SyntaxKind.StaticKeyword)).AddAttributeLists(
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
|
|
.AddArgumentListArguments(
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
|
|
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))),
|
|
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
|
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))),
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
|
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
|
AttributeArgument(LiteralExpression(
|
|
SyntaxKind.StringLiteralExpression,
|
|
Literal("This type is not intended to be used directly by user code")))))))
|
|
.AddMembers(names.Select(name => CreateFieldDeclaration(ArgsTypeName, name)).ToArray())))
|
|
.NormalizeWhitespace();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a field declaration for a cached property changing/changed name.
|
|
/// </summary>
|
|
/// <param name="typeName">The field type name (either <see cref="PropertyChangedEventArgs"/> or <see cref="PropertyChangingEventArgs"/>).</param>
|
|
/// <param name="propertyName">The name of the cached property name.</param>
|
|
/// <returns>A <see cref="FieldDeclarationSyntax"/> instance for the input cached property name.</returns>
|
|
private static FieldDeclarationSyntax CreateFieldDeclaration(string typeName, string propertyName)
|
|
{
|
|
// Create a static field with a cached property changed/changing argument for a specified property.
|
|
// This code produces a field declaration as follows:
|
|
//
|
|
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
|
// [global::System.Obsolete("This field is not intended to be referenced directly by user code")]
|
|
// public static readonly <ARG_TYPE> <PROPERTY_NAME> = new("<PROPERTY_NAME>");
|
|
return
|
|
FieldDeclaration(
|
|
VariableDeclaration(IdentifierName(typeName))
|
|
.AddVariables(
|
|
VariableDeclarator(Identifier(propertyName))
|
|
.WithInitializer(EqualsValueClause(
|
|
ImplicitObjectCreationExpression()
|
|
.AddArgumentListArguments(Argument(
|
|
LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName))))))))
|
|
.AddModifiers(
|
|
Token(SyntaxKind.PublicKeyword),
|
|
Token(SyntaxKind.StaticKeyword),
|
|
Token(SyntaxKind.ReadOnlyKeyword))
|
|
.AddAttributeLists(
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
|
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
|
AttributeList(SingletonSeparatedList(
|
|
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
|
AttributeArgument(LiteralExpression(
|
|
SyntaxKind.StringLiteralExpression,
|
|
Literal("This field is not intended to be referenced directly by user code")))))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the generated property name for an input field.
|
|
/// </summary>
|
|
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
|
/// <returns>The generated property name for <paramref name="fieldSymbol"/>.</returns>
|
|
public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol)
|
|
{
|
|
string propertyName = fieldSymbol.Name;
|
|
|
|
if (propertyName.StartsWith("m_"))
|
|
{
|
|
propertyName = propertyName.Substring(2);
|
|
}
|
|
else if (propertyName.StartsWith("_"))
|
|
{
|
|
propertyName = propertyName.TrimStart('_');
|
|
}
|
|
|
|
return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}";
|
|
}
|
|
}
|
|
}
|