diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 1fee5b622..4ea9f15e1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -28,6 +28,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// 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 annotate the setter as requiring unreferenced code. +/// Indicates whether to generate property changing partial method. +/// Indicates whether to generate property changed partial method. +/// Indicates whether to call property changing method. +/// Indicates whether to call property changed method. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( SyntaxKind AnnotatedMemberKind, @@ -47,4 +51,8 @@ internal sealed record PropertyInfo( bool IsReferenceTypeOrUnconstrainedTypeParameter, bool IncludeMemberNotNullOnSetAccessor, bool IncludeRequiresUnreferencedCodeOnSetAccessor, + bool GenerateOnChanging, + bool GenerateOnChanged, + bool CallOnChanging, + bool CallOnChanged, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 2b2756ec4..0de8bbee9 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -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(); @@ -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)) @@ -415,6 +421,10 @@ public static bool TryGetInfo( isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, includeRequiresUnreferencedCodeOnSetAccessor, + generateOnChanging, + generateOnChanged, + callOnChanging, + callOnChanged, forwardedAttributes.ToImmutable()); diagnostics = builder.ToImmutable(); @@ -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 @@ -1185,14 +1195,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf .WithInitializer(EqualsValueClause(setterFieldExpression))))); } - // Add the OnPropertyChanging() call first: - // - // OnChanging(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. @@ -1202,13 +1204,24 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf false => Argument(LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword))) }; - // Also call the overload after that: - // - // OnChanging(, value); - setterStatements.Add( - ExpressionStatement( - InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing")) - .AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value"))))); + if (propertyInfo.CallOnChanging) + { + // Add the OnPropertyChanging() call first: + // + // OnChanging(value); + setterStatements.Add( + ExpressionStatement( + InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changing")) + .AddArgumentListArguments(Argument(IdentifierName("value"))))); + + // Also call the overload after that: + // + // OnChanging(, 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) @@ -1248,21 +1261,24 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName)))))); } - // Add the OnPropertyChanged() call: - // - // OnChanged(value); - setterStatements.Add( - ExpressionStatement( - InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed")) - .AddArgumentListArguments(Argument(IdentifierName("value"))))); + if (propertyInfo.CallOnChanged) + { + // Add the OnPropertyChanged() call: + // + // OnChanged(value); + setterStatements.Add( + ExpressionStatement( + InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed")) + .AddArgumentListArguments(Argument(IdentifierName("value"))))); - // Do the same for the overload, as above: - // - // OnChanged(, value); - setterStatements.Add( - ExpressionStatement( - InvocationExpression(IdentifierName($"On{propertyInfo.PropertyName}Changed")) - .AddArgumentListArguments(oldPropertyValueArgument, Argument(IdentifierName("value"))))); + // Do the same for the overload, as above: + // + // OnChanged(, 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) @@ -1456,6 +1472,9 @@ private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo) /// The generated instances for the OnPropertyChanging and OnPropertyChanged methods. public static ImmutableArray GetOnPropertyChangeMethodsSyntax(PropertyInfo propertyInfo) { + if (!propertyInfo.GenerateOnChanged && !propertyInfo.GenerateOnChanging) + return ImmutableArray.Empty; + // Get the property type syntax TypeSyntax parameterType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); @@ -1466,7 +1485,7 @@ public static ImmutableArray GetOnPropertyChangeMethods // /// This method is invoked right before the value of is changed. // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // partial void OnChanging( 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)) @@ -1480,7 +1499,7 @@ public static ImmutableArray GetOnPropertyChangeMethods Comment($"/// Executes the logic for when is changing."), Comment("/// The new property value being set."), Comment($"/// This method is invoked right before the value of is changed.")), 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); @@ -1493,7 +1512,7 @@ public static ImmutableArray GetOnPropertyChangeMethods // /// This method is invoked right before the value of is changed. // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // partial void OnChanging( oldValue, newValue); - MemberDeclarationSyntax onPropertyChanging2Declaration = + MemberDeclarationSyntax? onPropertyChanging2Declaration = propertyInfo.GenerateOnChanging ? MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing")) .AddModifiers(Token(SyntaxKind.PartialKeyword)) .AddParameterListParameters( @@ -1510,7 +1529,7 @@ public static ImmutableArray GetOnPropertyChangeMethods Comment("/// The previous property value that is being replaced."), Comment("/// The new property value being set."), Comment($"/// This method is invoked right before the value of is changed.")), SyntaxKind.OpenBracketToken, TriviaList()))) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) : null; // Construct the generated method as follows: // @@ -1519,7 +1538,7 @@ public static ImmutableArray GetOnPropertyChangeMethods // /// This method is invoked right after the value of is changed. // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // partial void OnChanged( 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)) @@ -1533,7 +1552,7 @@ public static ImmutableArray GetOnPropertyChangeMethods Comment($"/// Executes the logic for when just changed."), Comment("/// The new property value that was set."), Comment($"/// This method is invoked right after the value of is changed.")), SyntaxKind.OpenBracketToken, TriviaList()))) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) : null; // Construct the generated method as follows: // @@ -1543,7 +1562,7 @@ public static ImmutableArray GetOnPropertyChangeMethods // /// This method is invoked right after the value of is changed. // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // partial void OnChanged( oldValue, newValue); - MemberDeclarationSyntax onPropertyChanged2Declaration = + MemberDeclarationSyntax? onPropertyChanged2Declaration = propertyInfo.GenerateOnChanged ? MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed")) .AddModifiers(Token(SyntaxKind.PartialKeyword)) .AddParameterListParameters( @@ -1560,13 +1579,28 @@ public static ImmutableArray GetOnPropertyChangeMethods Comment("/// The previous property value that was replaced."), Comment("/// The new property value that was set."), Comment($"/// This method is invoked right after the value of is changed.")), 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!); + } } /// @@ -1752,5 +1786,33 @@ public static string GetGeneratedPropertyName(ISymbol memberSymbol) return $"{char.ToUpper(propertyName[0], CultureInfo.InvariantCulture)}{propertyName.Substring(1)}"; } + + /// + /// Tries to gather generate and call optional from ObservablePropertyAttribute. + /// + /// The instance. + /// Field which indicates whether to generate property On{PropertyName}Changing partial method. + /// Field which indicates whether to generate property On{PropertyName}Changed partial method. + /// Field which indicates whether to call property On{PropertyName}Changing method. + /// Field which indicates whether to call property On{PropertyName}Changed method. + /// Whether or not was ObservablePropertyAttribute. + 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("GenerateOnChanging", out value)) + generateOnChanging = value; + if (attributeData.TryGetNamedArgument("GenerateOnChanged", out value)) + generateOnChanged = value; + if (attributeData.TryGetNamedArgument("CallOnChanging", out value)) + callOnChanging = value; + if (attributeData.TryGetNamedArgument("CallOnChanged", out value)) + callOnChanged = value; + return true; + } + + return false; + } } } diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs index 83311270c..81b93ea4e 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -70,4 +70,23 @@ namespace CommunityToolkit.Mvvm.ComponentModel; [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class ObservablePropertyAttribute : Attribute { + /// + /// Indicates whether to generate property On{PropertyName}Changing partial method. + /// + public bool GenerateOnChanging { get; set; } = true; + + /// + /// Indicates whether to generate property On{PropertyName}Changed partial method. + /// + public bool GenerateOnChanged { get; set; } = true; + + /// + /// Indicates whether to call property On{PropertyName}Changing method. + /// + public bool CallOnChanging { get; set; } = true; + + /// + /// Indicates whether to call property On{PropertyName}Changed method. + /// + public bool CallOnChanged { get; set; } = true; } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index d91dfe2b2..faf04a318 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -153,6 +153,450 @@ public string Name VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + [TestMethod] + public void ObservablePropertyWithoutGenerateOnChanging() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty(GenerateOnChanging = false)] + private string name; + } + """; + +#if NET6_0_OR_GREATER + 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 string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanged(string 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 OnNameChanged(string? oldValue, string newValue); + } + } + """; +#else + 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 string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanged(string 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 OnNameChanged(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithoutGenerateOnChanged() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty(GenerateOnChanged = false)] + private string name; + } + """; + +#if NET6_0_OR_GREATER + 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 string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanging(string 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 OnNameChanging(string? oldValue, string newValue); + } + } + """; +#else + 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 string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanging(string 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 OnNameChanging(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithoutGenerateOnChanging_WithoutCallOnChanging() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty(GenerateOnChanging = false, CallOnChanging = false)] + private string name; + } + """; + +#if NET6_0_OR_GREATER + 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 string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanged(string 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 OnNameChanged(string? oldValue, string newValue); + } + } + """; +#else + 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 string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanged(string 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 OnNameChanged(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithoutGenerateOnChanged_WithoutCallOnChanged() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty(GenerateOnChanged = false, CallOnChanged = false)] + private string name; + } + """; + +#if NET6_0_OR_GREATER + 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 string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanging(string 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 OnNameChanging(string? oldValue, string newValue); + } + } + """; +#else + 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 string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// 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 OnNameChanging(string 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 OnNameChanging(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + [TestMethod] public void ObservablePropertyWithNonNullableValueType_DoesNotEmitMemberNotNullAttribute() {