diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
index c02e123ca..59e5b33fe 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
@@ -17,6 +17,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
/// The sequence of commands to notify.
/// Whether or not the generated property also broadcasts changes.
/// Whether or not the generated property also validates its value.
+/// Whether the old property value is being directly referenced.
+/// Indicates whether the property is of a reference type.
/// The sequence of forwarded attributes for the generated property.
internal sealed record PropertyInfo(
string TypeNameWithNullabilityAnnotations,
@@ -27,4 +29,6 @@ internal sealed record PropertyInfo(
EquatableArray NotifiedCommandNames,
bool NotifyPropertyChangedRecipients,
bool NotifyDataErrorInfo,
+ bool IsOldPropertyValueDirectlyReferenced,
+ bool IsReferenceType,
EquatableArray ForwardedAttributes);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
index 031a9d473..d84c3f96e 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
@@ -111,6 +111,8 @@ public static bool TryGetInfo(
bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false;
bool hasOrInheritsClassLevelNotifyDataErrorInfo = false;
bool hasAnyValidationAttributes = false;
+ bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName);
+ bool isReferenceType = fieldSymbol.Type.IsReferenceType;
// Track the property changing event for the property, if the type supports it
if (shouldInvokeOnPropertyChanging)
@@ -263,6 +265,8 @@ public static bool TryGetInfo(
notifiedCommandNames.ToImmutable(),
notifyRecipients,
notifyDataErrorInfo,
+ isOldPropertyValueDirectlyReferenced,
+ isReferenceType,
forwardedAttributes.ToImmutable());
diagnostics = builder.ToImmutable();
@@ -637,6 +641,38 @@ private static bool TryGetNotifyDataErrorInfo(
return false;
}
+ ///
+ /// Checks whether the generated code has to directly reference the old property value.
+ ///
+ /// The input instance to process.
+ /// The name of the property being generated.
+ /// Whether the generated code needs direct access to the old property value.
+ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbol, string propertyName)
+ {
+ // Check OnChanging( oldValue, newValue) first
+ foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changing"))
+ {
+ // No need to be too specific as we're not expecting false positives (which also wouldn't really
+ // cause any problems anyway, just produce slightly worse codegen). Just checking the number of
+ // parameters is good enough, and keeps the code very simple and cheap to run.
+ if (symbol is IMethodSymbol { Parameters.Length: 2 })
+ {
+ return true;
+ }
+ }
+
+ // Do the same for OnChanged( oldValue, newValue)
+ foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changed"))
+ {
+ if (symbol is IMethodSymbol { Parameters.Length: 2 })
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
/// Gets a instance with the cached args for property changing notifications.
///
@@ -683,10 +719,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
string name => IdentifierName(name)
};
- if (propertyInfo.NotifyPropertyChangedRecipients)
+ if (propertyInfo.NotifyPropertyChangedRecipients || propertyInfo.IsOldPropertyValueDirectlyReferenced)
{
- // If broadcasting changes are required, also store the old value.
- // This code generates a statement as follows:
+ // Store the old value for later. This code generates a statement as follows:
//
// __oldValue = ;
setterStatements.Add(
@@ -705,6 +740,23 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
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.
+ ArgumentSyntax oldPropertyValueArgument = propertyInfo.IsOldPropertyValueDirectlyReferenced switch
+ {
+ true => Argument(IdentifierName("__oldValue")),
+ 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")))));
+
// Gather the statements to notify dependent properties
foreach (string propertyName in propertyInfo.PropertyChangingNames)
{
@@ -751,6 +803,14 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
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")))));
+
// Gather the statements to notify dependent properties
foreach (string propertyName in propertyInfo.PropertyChangedNames)
{
@@ -872,6 +932,8 @@ public static ImmutableArray GetOnPropertyChangeMethods
// Construct the generated method as follows:
//
// /// 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("...", "...")]
// partial void OnChanging( value);
MemberDeclarationSyntax onPropertyChangingDeclaration =
@@ -884,12 +946,56 @@ public static ImmutableArray GetOnPropertyChangeMethods
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
- .WithOpenBracketToken(Token(TriviaList(Comment($"/// Executes the logic for when is changing.")), SyntaxKind.OpenBracketToken, TriviaList())))
+ .WithOpenBracketToken(Token(TriviaList(
+ 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));
+
+ // 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.IsReferenceType switch
+ {
+ true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?")
+ => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"),
+ _ => parameterType
+ };
+
+ // Construct the generated method as follows:
+ //
+ // /// 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("...", "...")]
+ // partial void OnChanging( oldValue, newValue);
+ MemberDeclarationSyntax onPropertyChanging2Declaration =
+ MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changing"))
+ .AddModifiers(Token(SyntaxKind.PartialKeyword))
+ .AddParameterListParameters(
+ Parameter(Identifier("oldValue")).WithType(oldValueTypeSyntax),
+ Parameter(Identifier("newValue")).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($"/// Executes the logic for when is changing."),
+ 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));
// Construct the generated method as follows:
//
// /// Executes the logic for when ust changed.
+ // /// The new property value that was set.
+ // /// This method is invoked right after the value of is changed.
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// partial void OnChanged( value);
MemberDeclarationSyntax onPropertyChangedDeclaration =
@@ -902,10 +1008,44 @@ public static ImmutableArray GetOnPropertyChangeMethods
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
- .WithOpenBracketToken(Token(TriviaList(Comment($"/// Executes the logic for when just changed.")), SyntaxKind.OpenBracketToken, TriviaList())))
+ .WithOpenBracketToken(Token(TriviaList(
+ 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));
+
+ // Construct the generated method as follows:
+ //
+ // /// Executes the logic for when ust 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("...", "...")]
+ // partial void OnChanged( oldValue, newValue);
+ MemberDeclarationSyntax onPropertyChanged2Declaration =
+ MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier($"On{propertyInfo.PropertyName}Changed"))
+ .AddModifiers(Token(SyntaxKind.PartialKeyword))
+ .AddParameterListParameters(
+ Parameter(Identifier("oldValue")).WithType(oldValueTypeSyntax),
+ Parameter(Identifier("newValue")).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($"/// Executes the logic for when just changed."),
+ 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));
- return ImmutableArray.Create(onPropertyChangingDeclaration, onPropertyChangedDeclaration);
+ return ImmutableArray.Create(
+ onPropertyChangingDeclaration,
+ onPropertyChanging2Declaration,
+ onPropertyChangedDeclaration,
+ onPropertyChanged2Declaration);
}
///
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems
index ab60a1c18..881fdb625 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems
@@ -10,6 +10,7 @@
+
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
new file mode 100644
index 000000000..8cf4b9daa
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
@@ -0,0 +1,143 @@
+// 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;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests;
+
+[TestClass]
+public class Test_SourceGeneratorsCodegen
+{
+ [TestMethod]
+ public void ObservablePropertyWithPartialMethodWithPreviousValuesNotUsed_DoesNotGenerateFieldReadAndMarksOldValueAsNullable()
+ {
+ string source = """
+ using System.ComponentModel;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ #nullable enable
+
+ namespace MyApp;
+
+ partial class MyViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ private string name = null!;
+ }
+ """;
+
+ string result = """
+ //
+ #pragma warning disable
+ #nullable enable
+ namespace MyApp
+ {
+ partial class MyViewModel
+ {
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
+ [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", "8.1.0.0")]
+ 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", "8.1.0.0")]
+ 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", "8.1.0.0")]
+ 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", "8.1.0.0")]
+ partial void OnNameChanged(string? oldValue, string newValue);
+ }
+ }
+ """;
+
+ VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result));
+ }
+
+ ///
+ /// 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)
+ {
+ // Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded
+ Type observableObjectType = typeof(ObservableObject);
+ Type validationAttributeType = typeof(ValidationAttribute);
+
+ // Get all assembly references for the loaded assemblies (easy way to pull in all necessary dependencies)
+ IEnumerable references =
+ from assembly in AppDomain.CurrentDomain.GetAssemblies()
+ where !assembly.IsDynamic
+ let reference = MetadataReference.CreateFromFile(assembly.Location)
+ select reference;
+
+ SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp11));
+
+ // Create a syntax tree with the input source
+ CSharpCompilation compilation = CSharpCompilation.Create(
+ "original",
+ new SyntaxTree[] { syntaxTree },
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(generators).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);
+
+ // Run all source generators on the input source code
+ _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics);
+
+ // Ensure that no diagnostics were generated
+ CollectionAssert.AreEquivalent(Array.Empty(), diagnostics);
+
+ foreach ((string filename, string text) in results)
+ {
+ SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filename);
+
+ Assert.AreEqual(text, generatedTree.ToString());
+ }
+
+ GC.KeepAlive(observableObjectType);
+ GC.KeepAlive(validationAttributeType);
+ }
+}
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index 6e15ce95e..fcb059fb5 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -7,14 +7,14 @@
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using CommunityToolkit.Mvvm.ComponentModel;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers;
-using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests;
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
index 44adc66aa..e9d5606d2 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs
@@ -438,6 +438,60 @@ public void Test_OnPropertyChangingAndChangedPartialMethods()
Assert.AreEqual(99, model.NumberChangedValue);
}
+ [TestMethod]
+ public void Test_OnPropertyChangingAndChangedPartialMethods_WithPreviousValues()
+ {
+ ViewModelWithImplementedUpdateMethods2 model = new();
+
+ Assert.AreEqual(null, model.Name);
+ Assert.AreEqual(0, model.Number);
+
+ CollectionAssert.AreEqual(Array.Empty<(string, string)>(), model.OnNameChangingValues);
+ CollectionAssert.AreEqual(Array.Empty<(string, string)>(), model.OnNameChangedValues);
+ CollectionAssert.AreEqual(Array.Empty<(int, int)>(), model.OnNumberChangingValues);
+ CollectionAssert.AreEqual(Array.Empty<(int, int)>(), model.OnNumberChangedValues);
+
+ model.Name = "Bob";
+
+ CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangingValues);
+ CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangedValues);
+
+ Assert.AreEqual("Bob", model.Name);
+
+ CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangingValues);
+ CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangedValues);
+
+ model.Name = "Alice";
+
+ CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangingValues);
+ CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangedValues);
+
+ Assert.AreEqual("Alice", model.Name);
+
+ CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangingValues);
+ CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangedValues);
+
+ model.Number = 42;
+
+ CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangingValues);
+ CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangedValues);
+
+ Assert.AreEqual(42, model.Number);
+
+ CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangingValues);
+ CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangedValues);
+
+ model.Number = 77;
+
+ CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangingValues);
+ CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangedValues);
+
+ Assert.AreEqual(77, model.Number);
+
+ CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangingValues);
+ CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangedValues);
+ }
+
[TestMethod]
public void Test_OnPropertyChangingAndChangedPartialMethodWithAdditionalValidation()
{
@@ -1253,6 +1307,43 @@ partial void OnNumberChanged(int value)
}
}
+ public partial class ViewModelWithImplementedUpdateMethods2 : ObservableObject
+ {
+ [ObservableProperty]
+ public string? name;
+
+ [ObservableProperty]
+ public int number;
+
+ public List<(string? Old, string? New)> OnNameChangingValues { get; } = new();
+
+ public List<(string? Old, string? New)> OnNameChangedValues { get; } = new();
+
+ public List<(int Old, int New)> OnNumberChangingValues { get; } = new();
+
+ public List<(int Old, int New)> OnNumberChangedValues { get; } = new();
+
+ partial void OnNameChanging(string? oldValue, string? newValue)
+ {
+ OnNameChangingValues.Add((oldValue, newValue));
+ }
+
+ partial void OnNameChanged(string? oldValue, string? newValue)
+ {
+ OnNameChangedValues.Add((oldValue, newValue));
+ }
+
+ partial void OnNumberChanging(int oldValue, int newValue)
+ {
+ OnNumberChangingValues.Add((oldValue, newValue));
+ }
+
+ partial void OnNumberChanged(int oldValue, int newValue)
+ {
+ OnNumberChangedValues.Add((oldValue, newValue));
+ }
+ }
+
public partial class ViewModelWithImplementedUpdateMethodAndAdditionalValidation : ObservableObject
{
private int step;