// 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 ObservablePropertyWithNonNullableReferenceType_EmitsMemberNotNullAttribute() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] 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); /// 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 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 ObservablePropertyWithNonNullableValueType_DoesNotEmitMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] private Guid id; } """; 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 global::System.Guid Id { get => id; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(id, value)) { OnIdChanging(value); OnIdChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Id); id = value; OnIdChanged(value); OnIdChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Id); } } } /// 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 OnIdChanging(global::System.Guid 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 OnIdChanging(global::System.Guid oldValue, global::System.Guid 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 OnIdChanged(global::System.Guid 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 OnIdChanged(global::System.Guid oldValue, global::System.Guid newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNonNullableUnconstrainedGenericType_EmitsMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] private T content; } """; #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 Content { get => content; [global::System.Diagnostics.CodeAnalysis.MemberNotNull("content")] set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(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 Content { get => content; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(T? oldValue, T newValue); } } """; #endif VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); // Also test with C# 8 to double check that using a nullable unconstrained type parameter doesn't cause issues, but just a (suppressed) warning VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.CSharp8, ("MyApp.MyViewModel`1.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNonNullableConstrainedNotNullGenericType_EmitsMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject where T : notnull { [ObservableProperty] private T content; } """; #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 Content { get => content; [global::System.Diagnostics.CodeAnalysis.MemberNotNull("content")] set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(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 Content { get => content; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(T? oldValue, T newValue); } } """; #endif VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNonNullableConstrainedReferenceTypeGenericType_EmitsMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject where T : class { [ObservableProperty] private T content; } """; #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 Content { get => content; [global::System.Diagnostics.CodeAnalysis.MemberNotNull("content")] set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(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 Content { get => content; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(T? oldValue, T newValue); } } """; #endif VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNonNullableConstrainedValueTypeGenericType_DoesNotEmitMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject where T : struct { [ObservableProperty] private T content; } """; 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 Content { get => content; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(T oldValue, T newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNullableReferenceType_DoesNotEmitMemberNotNullAttribute() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] private string? name; } """; 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); /// 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); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNullableValueType_DoesNotEmitMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] private Guid? id; } """; 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 global::System.Guid? Id { get => id; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(id, value)) { OnIdChanging(value); OnIdChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Id); id = value; OnIdChanged(value); OnIdChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Id); } } } /// 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 OnIdChanging(global::System.Guid? 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 OnIdChanging(global::System.Guid? oldValue, global::System.Guid? 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 OnIdChanged(global::System.Guid? 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 OnIdChanged(global::System.Guid? oldValue, global::System.Guid? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNullableUnconstrainedGenericType_DoesNotEmitMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] private T? content; } """; 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? Content { get => content; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(T? oldValue, T? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNullableConstrainedReferenceTypeGenericType_DoesNotEmitMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject where T : class { [ObservableProperty] private T? content; } """; 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? Content { get => content; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(T? oldValue, T? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } [TestMethod] public void ObservablePropertyWithNullableConstrainedValueTypeGenericType_DoesNotEmitMemberNotNullAttribute() { string source = """ using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject where T : struct { [ObservableProperty] private T? content; } """; 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? Content { get => content; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) { OnContentChanging(value); OnContentChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); content = value; OnContentChanged(value); OnContentChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); } } } /// 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 OnContentChanging(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 OnContentChanging(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 OnContentChanged(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 OnContentChanged(T? oldValue, T? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } // See https://github.com/CommunityToolkit/dotnet/issues/601 [TestMethod] public void ObservablePropertyWithForwardedAttributesWithNumberLiterals_PreservesType() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { const double MyDouble = 3.14; const float MyFloat = 3.14f; [ObservableProperty] [property: DefaultValue(0.0)] [property: DefaultValue(1.24)] [property: DefaultValue(0.0f)] [property: DefaultValue(0.0f)] [property: DefaultValue(MyDouble)] [property: DefaultValue(MyFloat)] private object? a; } public class DefaultValueAttribute : Attribute { public DefaultValueAttribute(object value) { } } """; 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] [global::MyApp.DefaultValueAttribute(0D)] [global::MyApp.DefaultValueAttribute(1.24D)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(3.14D)] [global::MyApp.DefaultValueAttribute(3.14F)] public object? A { get => a; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) { OnAChanging(value); OnAChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); a = value; OnAChanged(value); OnAChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); } } } /// 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 OnAChanging(object? 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 OnAChanging(object? oldValue, object? 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 OnAChanged(object? 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 OnAChanged(object? oldValue, object? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } [TestMethod] public void RelayCommandMethodWithForwardedAttributesWithNumberLiterals_PreservesType() { string source = """ using CommunityToolkit.Mvvm.Input; #nullable enable namespace MyApp; partial class MyViewModel { const double MyDouble = 3.14; const float MyFloat = 3.14f; [RelayCommand] [field: DefaultValue(0.0)] [field: DefaultValue(1.24)] [field: DefaultValue(0.0f)] [field: DefaultValue(0.0f)] [field: DefaultValue(MyDouble)] [field: DefaultValue(MyFloat)] [property: DefaultValue(0.0)] [property: DefaultValue(1.24)] [property: DefaultValue(0.0f)] [property: DefaultValue(0.0f)] [property: DefaultValue(MyDouble)] [property: DefaultValue(MyFloat)] private void Test() { } } public class DefaultValueAttribute : Attribute { public DefaultValueAttribute(object value) { } } """; string result = """ // #pragma warning disable #nullable enable namespace MyApp { /// partial class MyViewModel { /// The backing field for . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::MyApp.DefaultValueAttribute(0D)] [global::MyApp.DefaultValueAttribute(1.24D)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(3.14D)] [global::MyApp.DefaultValueAttribute(3.14F)] private global::CommunityToolkit.Mvvm.Input.RelayCommand? testCommand; /// Gets an instance wrapping . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [global::MyApp.DefaultValueAttribute(0D)] [global::MyApp.DefaultValueAttribute(1.24D)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(0F)] [global::MyApp.DefaultValueAttribute(3.14D)] [global::MyApp.DefaultValueAttribute(3.14F)] public global::CommunityToolkit.Mvvm.Input.IRelayCommand TestCommand => testCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test)); } } """; VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result)); } // See https://github.com/CommunityToolkit/dotnet/issues/681 [TestMethod] public void ObservablePropertyWithForwardedAttributesWithEnumWithNegativeValue_WorksCorrectly() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] [property: DefaultValue(NegativeEnum1.Problem)] [property: DefaultValue(NegativeEnum2.Problem)] [property: DefaultValue(NegativeEnum3.Problem)] [property: DefaultValue(NegativeEnum4.Problem)] private object? a; } public class DefaultValueAttribute : Attribute { public DefaultValueAttribute(object value) { } } public enum NegativeEnum1 { Problem = -1073741824, OK = 0 } public enum NegativeEnum2 : long { Problem = long.MinValue, OK = 0 } public enum NegativeEnum3 : short { Problem = -1234, OK = 0 } public enum NegativeEnum4 : sbyte { Problem = -1, OK = 0 } """; 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] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] public object? A { get => a; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) { OnAChanging(value); OnAChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); a = value; OnAChanged(value); OnAChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); } } } /// 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 OnAChanging(object? 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 OnAChanging(object? oldValue, object? 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 OnAChanged(object? 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 OnAChanged(object? oldValue, object? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } [TestMethod] public void ObservablePropertyWithForwardedAttributesWithEnumWithCombinedValues_WorksCorrectly() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] [property: DefaultValue(NegativeEnum1.A)] [property: DefaultValue(NegativeEnum1.B)] [property: DefaultValue((NegativeEnum1)42)] [property: DefaultValue((NegativeEnum1)OtherClass.MyNumber)] [property: DefaultValue(OtherClass.MyEnumValue)] [property: DefaultValue(NegativeEnum2.A)] [property: DefaultValue(NegativeEnum2.B)] [property: DefaultValue((NegativeEnum2)42)] [property: DefaultValue((NegativeEnum2)OtherClass.MyNumber)] [property: DefaultValue((NegativeEnum2)(int)OtherClass.MyEnumValue)] [property: DefaultValue(NegativeEnum3.A)] [property: DefaultValue(NegativeEnum3.B)] [property: DefaultValue((NegativeEnum3)42)] [property: DefaultValue((NegativeEnum3)OtherClass.MyNumber)] [property: DefaultValue((NegativeEnum3)(int)OtherClass.MyEnumValue)] [property: DefaultValue(NegativeEnum4.A)] [property: DefaultValue(NegativeEnum4.B)] [property: DefaultValue((NegativeEnum4)42)] [property: DefaultValue((NegativeEnum4)unchecked((sbyte)OtherClass.MyNumber))] [property: DefaultValue((NegativeEnum4)(int)OtherClass.MyEnumValue)] private object? a; } public static class OtherClass { public const int MyNumber = 456; public const NegativeEnum1 MyEnumValue = NegativeEnum1.C; } public class DefaultValueAttribute : Attribute { public DefaultValueAttribute(object value) { } } public enum NegativeEnum1 { A = 0, B = -1073741824, C = 123 } public enum NegativeEnum2 : long { A = 0, B = long.MinValue } public enum NegativeEnum3 : short { A = 1, B = -1234 } public enum NegativeEnum4 : sbyte { A = 1, B = -1 } """; 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] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)0)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)42)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)456)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)123)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)0)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)42)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)456)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)123)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)1)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)42)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)456)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)123)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)1)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)42)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-56)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)123)] public object? A { get => a; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) { OnAChanging(value); OnAChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); a = value; OnAChanged(value); OnAChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); } } } /// 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 OnAChanging(object? 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 OnAChanging(object? oldValue, object? 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 OnAChanged(object? 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 OnAChanged(object? oldValue, object? newValue); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } // See https://github.com/CommunityToolkit/dotnet/issues/632 [TestMethod] public void RelayCommandMethodWithPartialDeclarations_TriggersCorrectly() { string source = """ using CommunityToolkit.Mvvm.Input; #nullable enable namespace MyApp; partial class MyViewModel { [RelayCommand] private partial void Test1() { } private partial void Test1(); private partial void Test2() { } [RelayCommand] private partial void Test2(); } """; string result1 = """ // #pragma warning disable #nullable enable namespace MyApp { /// partial class MyViewModel { /// The backing field for . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] private global::CommunityToolkit.Mvvm.Input.RelayCommand? test1Command; /// Gets an instance wrapping . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test1Command => test1Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test1)); } } """; string result2 = """ // #pragma warning disable #nullable enable namespace MyApp { /// partial class MyViewModel { /// The backing field for . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] private global::CommunityToolkit.Mvvm.Input.RelayCommand? test2Command; /// Gets an instance wrapping . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test2Command => test2Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test2)); } } """; VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test1.g.cs", result1), ("MyApp.MyViewModel.Test2.g.cs", result2)); } // See https://github.com/CommunityToolkit/dotnet/issues/632 [TestMethod] public void RelayCommandMethodWithForwardedAttributesOverPartialDeclarations_MergesAttributes() { string source = """ using CommunityToolkit.Mvvm.Input; #nullable enable namespace MyApp; partial class MyViewModel { [RelayCommand] [field: Value(0)] [property: Value(1)] private partial void Test1() { } [field: Value(2)] [property: Value(3)] private partial void Test1(); [field: Value(0)] [property: Value(1)] private partial void Test2() { } [RelayCommand] [field: Value(2)] [property: Value(3)] private partial void Test2(); } public class ValueAttribute : Attribute { public ValueAttribute(object value) { } } """; string result1 = """ // #pragma warning disable #nullable enable namespace MyApp { /// partial class MyViewModel { /// The backing field for . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::MyApp.ValueAttribute(2)] [global::MyApp.ValueAttribute(0)] private global::CommunityToolkit.Mvvm.Input.RelayCommand? test1Command; /// Gets an instance wrapping . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [global::MyApp.ValueAttribute(3)] [global::MyApp.ValueAttribute(1)] public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test1Command => test1Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test1)); } } """; string result2 = """ // #pragma warning disable #nullable enable namespace MyApp { /// partial class MyViewModel { /// The backing field for . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::MyApp.ValueAttribute(2)] [global::MyApp.ValueAttribute(0)] private global::CommunityToolkit.Mvvm.Input.RelayCommand? test2Command; /// Gets an instance wrapping . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [global::MyApp.ValueAttribute(3)] [global::MyApp.ValueAttribute(1)] public global::CommunityToolkit.Mvvm.Input.IRelayCommand Test2Command => test2Command ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test2)); } } """; VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test1.g.cs", result1), ("MyApp.MyViewModel.Test2.g.cs", result2)); } // See https://github.com/CommunityToolkit/dotnet/issues/681 [TestMethod] public void RelayCommandMethodWithForwardedAttributesWithEnumValues_WorksCorrectly() { string source = """ using CommunityToolkit.Mvvm.Input; #nullable enable namespace MyApp; partial class MyViewModel { [RelayCommand] [field: DefaultValue(NegativeEnum1.Problem)] [field: DefaultValue(NegativeEnum2.Problem)] [field: DefaultValue(NegativeEnum3.Problem)] [field: DefaultValue(NegativeEnum4.Problem)] [property: DefaultValue(NegativeEnum1.Problem)] [property: DefaultValue(NegativeEnum2.Problem)] [property: DefaultValue(NegativeEnum3.Problem)] [property: DefaultValue(NegativeEnum4.Problem)] private void Test() { } } public class DefaultValueAttribute : Attribute { public DefaultValueAttribute(object value) { } } public enum NegativeEnum1 { Problem = -1073741824, OK = 0 } public enum NegativeEnum2 : long { Problem = long.MinValue, OK = 0 } public enum NegativeEnum3 : short { Problem = -1234, OK = 0 } public enum NegativeEnum4 : sbyte { Problem = -1, OK = 0 } """; string result = """ // #pragma warning disable #nullable enable namespace MyApp { /// partial class MyViewModel { /// The backing field for . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] private global::CommunityToolkit.Mvvm.Input.RelayCommand? testCommand; /// Gets an instance wrapping . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum1)-1073741824)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum2)-9223372036854775808)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum3)-1234)] [global::MyApp.DefaultValueAttribute((global::MyApp.NegativeEnum4)-1)] public global::CommunityToolkit.Mvvm.Input.IRelayCommand TestCommand => testCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test)); } } """; VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result)); } [TestMethod] public void ObservablePropertyWithinGenericAndNestedTypes() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable namespace MyApp; partial class Foo { partial class MyViewModel : ObservableObject { [ObservableProperty] private string? a; } } """; string result = """ // #pragma warning disable #nullable enable namespace MyApp { /// partial class Foo { /// partial class MyViewModel { /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public string? A { get => a; set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) { OnAChanging(value); OnAChanging(default, value); OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); a = value; OnAChanged(value); OnAChanged(default, value); OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); } } } /// 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 OnAChanging(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 OnAChanging(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 OnAChanged(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 OnAChanged(string? oldValue, string? newValue); } } } """; #if ROSLYN_4_3_1_OR_GREATER VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.Foo+MyViewModel`1.g.cs", result)); #else VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.Foo.MyViewModel_1.g.cs", result)); #endif } [TestMethod] public void ObservableProperty_ValidateGeneratedCachedArguments() { string source = """ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] private string? name; } """; 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); /// 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); } } """; 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 "Name" 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 Name = new global::System.ComponentModel.PropertyChangingEventArgs("Name"); } } """; 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 "Name" 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 Name = new global::System.ComponentModel.PropertyChangedEventArgs("Name"); } } """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result), ("__KnownINotifyPropertyChangingArgs.g.cs", changingArgs), ("__KnownINotifyPropertyChangedArgs.g.cs", changedArgs)); } [TestMethod] public void RelayCommandMethodInTopLevelTypeWithNoNamespace_PreservesInheritdoc() { string source = """ using CommunityToolkit.Mvvm.Input; partial class MyViewModel { [RelayCommand] private void Test() { } } """; string result = """ // #pragma warning disable #nullable enable /// partial class MyViewModel { /// The backing field for . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] private global::CommunityToolkit.Mvvm.Input.RelayCommand? testCommand; /// Gets an instance wrapping . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", )] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public global::CommunityToolkit.Mvvm.Input.IRelayCommand TestCommand => testCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test)); } """; VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyViewModel.Test.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) { VerifyGenerateSources(source, generators, LanguageVersion.CSharp10, results); } /// /// Generates the requested sources /// /// The input source to process. /// 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) { // 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)); // 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) { 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}\""); #if !ROSLYN_4_3_1_OR_GREATER // Adjust the filenames for the legacy Roslyn 4.0 filePath = filePath.Replace('`', '_'); #endif SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filePath); Assert.AreEqual(expectedText, generatedTree.ToString()); } GC.KeepAlive(observableObjectType); GC.KeepAlive(validationAttributeType); } }