diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 855dce1fb..c6bf051a4 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -109,7 +109,6 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); using ImmutableArrayBuilder propertyChangedNames = ImmutableArrayBuilder.Rent(); - using ImmutableArrayBuilder propertyChangingNames = ImmutableArrayBuilder.Rent(); using ImmutableArrayBuilder notifiedCommandNames = ImmutableArrayBuilder.Rent(); using ImmutableArrayBuilder forwardedAttributes = ImmutableArrayBuilder.Rent(); @@ -131,12 +130,6 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); - // 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); @@ -296,12 +289,24 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Prepare the effective property changing/changed names. For the property changing names, + // there are two possible cases: if the mode is disabled, then there are no names to report + // at all. If the mode is enabled, then the list is just the same as for property changed. + ImmutableArray effectivePropertyChangedNames = propertyChangedNames.ToImmutable(); + ImmutableArray effectivePropertyChangingNames = shouldInvokeOnPropertyChanging switch + { + true => effectivePropertyChangedNames, + false => ImmutableArray.Empty + }; + + token.ThrowIfCancellationRequested(); + propertyInfo = new PropertyInfo( typeNameWithNullabilityAnnotations, fieldName, propertyName, - propertyChangingNames.ToImmutable(), - propertyChangedNames.ToImmutable(), + effectivePropertyChangingNames, + effectivePropertyChangedNames, notifiedCommandNames.ToImmutable(), notifyRecipients, notifyDataErrorInfo, diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs index 1900b33fe..09178f9f5 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs @@ -46,8 +46,14 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// get => name; /// set /// { -/// if (SetProperty(ref name, value)) +/// if (!EqualityComparer<string>.Default.Equals(name, value)) /// { +/// OnPropertyChanging(nameof(Name)); +/// OnPropertyChanged(nameof(FullName)); +/// +/// name = value; +/// +/// OnPropertyChanged(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); /// } /// } @@ -58,8 +64,14 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// get => surname; /// set /// { -/// if (SetProperty(ref surname, value)) +/// if (!EqualityComparer<string>.Default.Equals(name, value)) /// { +/// OnPropertyChanging(nameof(Surname)); +/// OnPropertyChanged(nameof(FullName)); +/// +/// surname = value; +/// +/// OnPropertyChanged(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); /// } /// } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index d19c52247..b47183c85 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -2367,13 +2367,379 @@ public double Object VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void ObservableProperty_NotifyPropertyChangedFor_WithNotifyPropertyChanging() + { + // Using integers for properties to avoid needing conditional code in the expected results for nullability attributes. + // This test and the one below are only validating the generation related to INotifyPropertyChanging, and nothing else. + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int firstName; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int lastName; + + public string FullName => ""; + } + """; + + 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 int FirstName + { + get => firstName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(firstName, value)) + { + OnFirstNameChanging(value); + OnFirstNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FirstName); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FullName); + firstName = value; + OnFirstNameChanged(value); + OnFirstNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FirstName); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public int LastName + { + get => lastName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(lastName, value)) + { + OnLastNameChanging(value); + OnLastNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.LastName); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FullName); + lastName = value; + OnLastNameChanged(value); + OnLastNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.LastName); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// 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 OnFirstNameChanging(int 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 OnFirstNameChanging(int oldValue, int 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 OnFirstNameChanged(int 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 OnFirstNameChanged(int oldValue, int newValue); + /// 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 OnLastNameChanging(int 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 OnLastNameChanging(int oldValue, int 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 OnLastNameChanged(int 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 OnLastNameChanged(int oldValue, int newValue); + } + } + """; + + string changingArgs = """ + // + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Mvvm.ComponentModel.__Internals + { + /// + /// A helper type providing cached, reusable instances + /// for all properties generated with . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [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 __KnownINotifyPropertyChangingArgs + { + /// The cached instance for all "FirstName" generated properties. + [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 global::System.ComponentModel.PropertyChangingEventArgs FirstName = new global::System.ComponentModel.PropertyChangingEventArgs("FirstName"); + /// The cached instance for all "FullName" generated properties. + [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 global::System.ComponentModel.PropertyChangingEventArgs FullName = new global::System.ComponentModel.PropertyChangingEventArgs("FullName"); + /// The cached instance for all "LastName" generated properties. + [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 global::System.ComponentModel.PropertyChangingEventArgs LastName = new global::System.ComponentModel.PropertyChangingEventArgs("LastName"); + } + } + """; + + string changedArgs = """ + // + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Mvvm.ComponentModel.__Internals + { + /// + /// A helper type providing cached, reusable instances + /// for all properties generated with . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [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 __KnownINotifyPropertyChangedArgs + { + /// The cached instance for all "FirstName" generated properties. + [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 global::System.ComponentModel.PropertyChangedEventArgs FirstName = new global::System.ComponentModel.PropertyChangedEventArgs("FirstName"); + /// The cached instance for all "FullName" generated properties. + [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 global::System.ComponentModel.PropertyChangedEventArgs FullName = new global::System.ComponentModel.PropertyChangedEventArgs("FullName"); + /// The cached instance for all "LastName" generated properties. + [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 global::System.ComponentModel.PropertyChangedEventArgs LastName = new global::System.ComponentModel.PropertyChangedEventArgs("LastName"); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result), ("__KnownINotifyPropertyChangingArgs.g.cs", changingArgs), ("__KnownINotifyPropertyChangedArgs.g.cs", changedArgs)); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void ObservableProperty_NotifyPropertyChangedFor_WithoutNotifyPropertyChanging() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + abstract class BaseViewModel + { + } + + [INotifyPropertyChanged(IncludeAdditionalHelperMethods = false)] + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int firstName; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int lastName; + + public string FullName => ""; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public int FirstName + { + get => firstName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(firstName, value)) + { + OnFirstNameChanging(value); + OnFirstNameChanging(default, value); + firstName = value; + OnFirstNameChanged(value); + OnFirstNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FirstName); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public int LastName + { + get => lastName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(lastName, value)) + { + OnLastNameChanging(value); + OnLastNameChanging(default, value); + lastName = value; + OnLastNameChanged(value); + OnLastNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.LastName); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// 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", "8.2.0.0")] + partial void OnFirstNameChanging(int 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", "8.2.0.0")] + partial void OnFirstNameChanging(int oldValue, int 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", "8.2.0.0")] + partial void OnFirstNameChanged(int 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", "8.2.0.0")] + partial void OnFirstNameChanged(int oldValue, int newValue); + /// 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", "8.2.0.0")] + partial void OnLastNameChanging(int 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", "8.2.0.0")] + partial void OnLastNameChanging(int oldValue, int 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", "8.2.0.0")] + partial void OnLastNameChanged(int 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", "8.2.0.0")] + partial void OnLastNameChanged(int oldValue, int newValue); + } + } + """; + + string changedArgs = """ + // + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Mvvm.ComponentModel.__Internals + { + /// + /// A helper type providing cached, reusable instances + /// for all properties generated with . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [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 __KnownINotifyPropertyChangedArgs + { + /// The cached instance for all "FirstName" generated properties. + [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 global::System.ComponentModel.PropertyChangedEventArgs FirstName = new global::System.ComponentModel.PropertyChangedEventArgs("FirstName"); + /// The cached instance for all "FullName" generated properties. + [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 global::System.ComponentModel.PropertyChangedEventArgs FullName = new global::System.ComponentModel.PropertyChangedEventArgs("FullName"); + /// The cached instance for all "LastName" generated properties. + [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 global::System.ComponentModel.PropertyChangedEventArgs LastName = new global::System.ComponentModel.PropertyChangedEventArgs("LastName"); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result), ("__KnownINotifyPropertyChangingArgs.g.cs", null), ("__KnownINotifyPropertyChangedArgs.g.cs", changedArgs)); + } + /// /// Generates the requested sources /// /// The input source to process. /// The generators to apply to the input syntax tree. /// The source files to compare. - private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, params (string Filename, string Text)[] results) + private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, params (string Filename, string? Text)[] results) { VerifyGenerateSources(source, generators, LanguageVersion.CSharp10, results); } @@ -2385,7 +2751,7 @@ private static void VerifyGenerateSources(string source, IIncrementalGenerator[] /// The generators to apply to the input syntax tree. /// The language version to use. /// The source files to compare. - private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, LanguageVersion languageVersion, params (string Filename, string Text)[] results) + private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, LanguageVersion languageVersion, params (string Filename, string? Text)[] results) { // Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded Type observableObjectType = typeof(ObservableObject); @@ -2415,22 +2781,30 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() // Ensure that no diagnostics were generated CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); - foreach ((string filename, string text) in results) + foreach ((string filename, string? text) in results) { - string filePath = filename; + if (text is not null) + { + string filePath = filename; - // Update the assembly version using the version from the assembly of the input generators. - // This allows the tests to not need updates whenever the version of the MVVM Toolkit changes. - string expectedText = text.Replace("", $"\"{generators[0].GetType().Assembly.GetName().Version}\""); + // Update the assembly version using the version from the assembly of the input generators. + // This allows the tests to not need updates whenever the version of the MVVM Toolkit changes. + string expectedText = text.Replace("", $"\"{generators[0].GetType().Assembly.GetName().Version}\""); #if !ROSLYN_4_3_1_OR_GREATER - // Adjust the filenames for the legacy Roslyn 4.0 - filePath = filePath.Replace('`', '_'); + // Adjust the filenames for the legacy Roslyn 4.0 + filePath = filePath.Replace('`', '_'); #endif - SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filePath); + SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filePath); - Assert.AreEqual(expectedText, generatedTree.ToString()); + Assert.AreEqual(expectedText, generatedTree.ToString()); + } + else + { + // If the text is null, verify that the file was not generated at all + Assert.IsFalse(outputCompilation.SyntaxTrees.Any(tree => Path.GetFileName(tree.FilePath) == filename)); + } } GC.KeepAlive(observableObjectType); diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 5a557206f..aac13f796 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -1040,6 +1040,39 @@ public void Test_ObservableProperty_ModelWithObservablePropertyWithUnderscoreAnd Assert.IsTrue(model.IsReadOnly); } + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndPropertyChanging() + { + ModelWithDependentPropertyAndPropertyChanging model = new(); + + List changingArgs = new(); + List changedArgs = new(); + + model.PropertyChanging += (s, e) => changingArgs.Add(e.PropertyName); + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Bob"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changingArgs); + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndNoPropertyChanging() + { + ModelWithDependentPropertyAndNoPropertyChanging model = new(); + + List changedArgs = new(); + + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Alice"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndNoPropertyChanging.Name), nameof(ModelWithDependentPropertyAndNoPropertyChanging.FullName) }, changedArgs); + } + #if NET6_0_OR_GREATER [TestMethod] public void Test_ObservableProperty_MemberNotNullAttributeIsPresent() @@ -1745,4 +1778,23 @@ public enum NegativeEnum Problem = -1, OK = 0 } + + private sealed partial class ModelWithDependentPropertyAndPropertyChanging : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private string? name; + + public string? FullName => ""; + } + + [INotifyPropertyChanged(IncludeAdditionalHelperMethods = false)] + private sealed partial class ModelWithDependentPropertyAndNoPropertyChanging + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private string? name; + + public string? FullName => ""; + } }