diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index d029295bd..52b7f44fe 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -834,9 +834,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf { using ImmutableArrayBuilder setterStatements = ImmutableArrayBuilder.Rent(); - // Get the property type syntax - TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); - string getterFieldIdentifierName; ExpressionSyntax getterFieldExpression; ExpressionSyntax setterFieldExpression; @@ -872,7 +869,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // __oldValue = ; setterStatements.Add( LocalDeclarationStatement( - VariableDeclaration(propertyType) + VariableDeclaration(GetMaybeNullPropertyType(propertyInfo)) .AddVariables( VariableDeclarator(Identifier("__oldValue")) .WithInitializer(EqualsValueClause(setterFieldExpression))))); @@ -1001,6 +998,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName)))))); } + // Get the property type syntax + TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); + // Generate the inner setter block as follows: // // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) @@ -1128,17 +1128,8 @@ public static ImmutableArray GetOnPropertyChangeMethods Comment($"/// This method is invoked right before the value of is changed.")), SyntaxKind.OpenBracketToken, TriviaList()))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - // Prepare the nullable type for the previous property value. This is needed because if the type is a reference - // type, the previous value might be null even if the property type is not nullable, as the first invocation would - // happen when the property is first set to some value that is not null (but the backing field would still be so). - // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability - // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. - TypeSyntax oldValueTypeSyntax = propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch - { - true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") - => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), - _ => parameterType - }; + // Get the type for the 'oldValue' parameter (which can be null on first invocation) + TypeSyntax oldValueTypeSyntax = GetMaybeNullPropertyType(propertyInfo); // Construct the generated method as follows: // @@ -1224,6 +1215,26 @@ public static ImmutableArray GetOnPropertyChangeMethods onPropertyChanged2Declaration); } + /// + /// Gets the for the type of a given property, when it can possibly be . + /// + /// The input instance to process. + /// The type of a given property, when it can possibly be + private static TypeSyntax GetMaybeNullPropertyType(PropertyInfo propertyInfo) + { + // Prepare the nullable type for the previous property value. This is needed because if the type is a reference + // type, the previous value might be null even if the property type is not nullable, as the first invocation would + // happen when the property is first set to some value that is not null (but the backing field would still be so). + // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability + // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. + return propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch + { + true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") + => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), + _ => IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations) + }; + } + /// /// Gets a instance with the cached args of a specified type. /// diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index c3e48fba6..a1e954d85 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -2301,7 +2301,7 @@ partial class MyViewModel : ObservableObject [ObservableProperty] double @object; - partial void OnObjectChanged(object oldValue, object NewValue) + partial void OnObjectChanged(double oldValue, double NewValue) { } } @@ -3018,6 +3018,284 @@ public string Name VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + [TestMethod] + public void ObservableProperty_NotNullableProperty_OfReferenceType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + string name; + + partial void OnNameChanged(string? oldValue, string NewValue) + { + } + } + """; + +#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)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, 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); + /// 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)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, 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); + /// 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 ObservableProperty_NotNullableProperty_OfUnconstrainedGenericType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + T value; + + partial void OnValueChanged(T? oldValue, T NewValue) + { + } + } + """; + +#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 T Value + { + get => value; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("value")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// 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 OnValueChanging(T 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 OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T 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 OnValueChanged(T? oldValue, T 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 T Value + { + get => value; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// 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 OnValueChanging(T 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 OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T 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 OnValueChanged(T? oldValue, T newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + /// /// Generates the requested sources ///