diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln
index dc557355a..7d7065f8b 100644
--- a/dotnet Community Toolkit.sln
+++ b/dotnet Community Toolkit.sln
@@ -91,6 +91,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.CodeF
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4120", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj", "{98572004-D29A-486E-8053-6D409557CE44}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.Roslyn4120.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj", "{87BF1537-935A-414D-8318-458F61A6E562}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -525,6 +527,26 @@ Global
{98572004-D29A-486E-8053-6D409557CE44}.Release|x64.Build.0 = Release|Any CPU
{98572004-D29A-486E-8053-6D409557CE44}.Release|x86.ActiveCfg = Release|Any CPU
{98572004-D29A-486E-8053-6D409557CE44}.Release|x86.Build.0 = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.Build.0 = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.Build.0 = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.Build.0 = Debug|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.Build.0 = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.ActiveCfg = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.Build.0 = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.Build.0 = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.ActiveCfg = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.Build.0 = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.ActiveCfg = Release|Any CPU
+ {87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -548,6 +570,7 @@ Global
{ECFE93AA-4B98-4292-B3FA-9430D513B4F9} = {B30036C4-D514-4E5B-A323-587A061772CE}
{4FCD501C-1BB5-465C-AD19-356DAB6600C6} = {B30036C4-D514-4E5B-A323-587A061772CE}
{C342302D-A263-42D6-B8EE-01DEF8192690} = {B30036C4-D514-4E5B-A323-587A061772CE}
+ {87BF1537-935A-414D-8318-458F61A6E562} = {B30036C4-D514-4E5B-A323-587A061772CE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345}
@@ -556,6 +579,7 @@ Global
tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{4fcd501c-1bb5-465c-ad19-356dab6600c6}*SharedItemsImports = 5
tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{5b44f7f1-dca2-4776-924e-a266f7bbf753}*SharedItemsImports = 5
src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{5e7f1212-a54b-40ca-98c5-1ff5cd1a1638}*SharedItemsImports = 13
+ tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{87bf1537-935a-414d-8318-458f61a6e562}*SharedItemsImports = 5
src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{98572004-d29a-486e-8053-6d409557ce44}*SharedItemsImports = 5
src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{a2ebda90-b720-430d-83f5-c6bcc355232c}*SharedItemsImports = 13
tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{ad9c3223-8e37-4fd4-a0d4-a45119551d3a}*SharedItemsImports = 5
diff --git a/global.json b/global.json
index 1880a952c..00b67caef 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.403",
+ "version": "9.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
index 730713ab0..b0bf76a5a 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
@@ -92,4 +92,7 @@ MVVMTK0047 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
MVVMTK0048 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0048
MVVMTK0049 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0049
MVVMTK0050 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0050
-MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0050
+MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0051
+MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0052
+MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053
+MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
index cc4351664..f02990ae1 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
@@ -41,6 +41,7 @@
+
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
index 3505d05f7..e127859d7 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
@@ -71,7 +71,7 @@ static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? contain
/// The instance to process.
/// The instance for the current run.
/// Whether is valid.
- public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel semanticModel)
+ public static bool IsCandidateValidForCompilation(MemberDeclarationSyntax node, SemanticModel semanticModel)
{
// At least C# 8 is always required
if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
@@ -90,6 +90,35 @@ public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel
return true;
}
+ ///
+ /// Performs additional checks before running the core generation logic.
+ ///
+ /// The input instance to process.
+ /// Whether is valid.
+ public static bool IsCandidateSymbolValid(ISymbol memberSymbol)
+ {
+#if ROSLYN_4_12_0_OR_GREATER
+ // We only need additional checks for properties (Roslyn already validates things for fields in our scenarios)
+ if (memberSymbol is IPropertySymbol propertySymbol)
+ {
+ // Ensure that the property declaration is a partial definition with no implementation
+ if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null })
+ {
+ return false;
+ }
+
+ // Also ignore all properties that have an invalid declaration
+ if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly || propertySymbol.Type.IsRefLikeType)
+ {
+ return false;
+ }
+ }
+#endif
+
+ // We assume all other cases are supported (other failure cases will be detected later)
+ return true;
+ }
+
///
/// Gets the candidate after the initial filtering.
///
@@ -140,13 +169,11 @@ public static bool TryGetInfo(
return false;
}
- using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent();
-
// Validate the target type
if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging))
{
propertyInfo = null;
- diagnostics = builder.ToImmutable();
+ diagnostics = ImmutableArray.Empty;
return false;
}
@@ -168,7 +195,7 @@ public static bool TryGetInfo(
if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration))
{
propertyInfo = null;
- diagnostics = builder.ToImmutable();
+ diagnostics = ImmutableArray.Empty;
// If the generated property would collide, skip generating it entirely. This makes sure that
// users only get the helpful diagnostic about the collision, and not the normal compiler error
@@ -182,7 +209,7 @@ public static bool TryGetInfo(
if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol)))
{
propertyInfo = null;
- diagnostics = builder.ToImmutable();
+ diagnostics = ImmutableArray.Empty;
return false;
}
@@ -232,6 +259,8 @@ public static bool TryGetInfo(
token.ThrowIfCancellationRequested();
+ using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent();
+
// Gather attributes info
foreach (AttributeData attributeData in memberSymbol.GetAttributes())
{
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
index 9957f3bbe..05bee8c83 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
@@ -9,7 +9,6 @@
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
using CommunityToolkit.Mvvm.SourceGenerators.Models;
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace CommunityToolkit.Mvvm.SourceGenerators;
@@ -38,6 +37,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return default;
}
+ // Validate the symbol as well before doing any work
+ if (!Execute.IsCandidateSymbolValid(context.TargetSymbol))
+ {
+ return default;
+ }
+
+ token.ThrowIfCancellationRequested();
+
// Get the hierarchy info for the target symbol, and try to gather the property info
HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs
new file mode 100644
index 000000000..8b144049e
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs
@@ -0,0 +1,103 @@
+// 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.
+
+#if ROSLYN_4_12_0_OR_GREATER
+
+using System.Collections.Immutable;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used on an invalid partial property declaration.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(
+ InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition,
+ InvalidObservablePropertyDeclarationReturnsByRef,
+ InvalidObservablePropertyDeclarationReturnsRefLikeType);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ // This generator is intentionally also analyzing generated code, because Roslyn will interpret properties
+ // that have '[GeneratedCode]' on them as being generated (and the same will apply to all partial parts).
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static context =>
+ {
+ // Get the [ObservableProperty] and [GeneratedCode] symbols
+ if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol ||
+ context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute") is not { } generatedCodeAttributeSymbol)
+ {
+ return;
+ }
+
+ context.RegisterSymbolAction(context =>
+ {
+ // Ensure that we have some target property to analyze (also skip implementation parts)
+ if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol)
+ {
+ return;
+ }
+
+ // If the property is not using [ObservableProperty], there's nothing to do
+ if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute))
+ {
+ return;
+ }
+
+ // Emit an error if the property is not a partial definition with no implementation...
+ if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null })
+ {
+ // ...But only if it wasn't actually generated by the [ObservableProperty] generator.
+ bool isImplementationAllowed =
+ propertySymbol is { IsPartialDefinition: true, PartialImplementationPart: IPropertySymbol implementationPartSymbol } &&
+ implementationPartSymbol.TryGetAttributeWithType(generatedCodeAttributeSymbol, out AttributeData? generatedCodeAttributeData) &&
+ generatedCodeAttributeData.TryGetConstructorArgument(0, out string? toolName) &&
+ toolName == typeof(ObservablePropertyGenerator).FullName;
+
+ // Emit the diagnostic only for cases that were not valid generator outputs
+ if (!isImplementationAllowed)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition,
+ observablePropertyAttribute.GetLocation(),
+ propertySymbol.ContainingType,
+ propertySymbol.Name));
+ }
+ }
+
+ // Emit an error if the property returns a value by ref
+ if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidObservablePropertyDeclarationReturnsByRef,
+ observablePropertyAttribute.GetLocation(),
+ propertySymbol.ContainingType,
+ propertySymbol.Name));
+ }
+
+ // Emit an error if the property type is a ref struct
+ if (propertySymbol.Type.IsRefLikeType)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidObservablePropertyDeclarationReturnsRefLikeType,
+ observablePropertyAttribute.GetLocation(),
+ propertySymbol.ContainingType,
+ propertySymbol.Name));
+ }
+ }, SymbolKind.Property);
+ });
+ }
+}
+
+#endif
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs
index 0b65550d0..e304690fa 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs
@@ -60,7 +60,9 @@ public override void Initialize(AnalysisContext context)
InvalidPropertyDeclarationForObservableProperty,
observablePropertyAttribute.GetLocation(),
propertySymbol.ContainingType,
- propertySymbol));
+ propertySymbol.Name));
+
+ return;
}
}
}, SymbolKind.Property);
@@ -91,6 +93,14 @@ internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarati
return false;
}
+ // Static properties are not supported
+ if (property.Modifiers.Any(SyntaxKind.StaticKeyword))
+ {
+ containingTypeNode = null;
+
+ return false;
+ }
+
// The accessors must be a get and a set (with any accessibility)
if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) ||
accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration))
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs
index 29255adb5..2760ab023 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs
@@ -51,7 +51,7 @@ public override void Initialize(AnalysisContext context)
UnsupportedRoslynVersionForObservablePartialPropertySupport,
propertySymbol.Locations.FirstOrDefault(),
propertySymbol.ContainingType,
- propertySymbol));
+ propertySymbol.Name));
}
}, SymbolKind.Property);
});
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
index adbb28771..36835788a 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
@@ -689,7 +689,7 @@ internal static class DiagnosticDescriptors
/// Format: "Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)".
///
///
- public static readonly DiagnosticDescriptor CSharpLanguageVersionIsNotPreviewForObservableProperty = new(
+ public static readonly DiagnosticDescriptor CSharpLanguageVersionIsNotPreviewForObservableProperty = new DiagnosticDescriptor(
id: "MVVMTK0041",
title: "C# language version is not 'preview'",
messageFormat: """Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)""",
@@ -705,7 +705,7 @@ internal static class DiagnosticDescriptors
/// Format: "The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)".
///
///
- public static readonly DiagnosticDescriptor UseObservablePropertyOnPartialProperty = new(
+ public static readonly DiagnosticDescriptor UseObservablePropertyOnPartialProperty = new DiagnosticDescriptor(
id: UseObservablePropertyOnPartialPropertyId,
title: "Prefer using [ObservableProperty] on partial properties",
messageFormat: """The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)""",
@@ -718,17 +718,17 @@ internal static class DiagnosticDescriptors
///
/// Gets a indicating when [ObservableProperty] is applied to a property with an invalid declaration.
///
- /// Format: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)".
+ /// Format: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be an instance (non static) partial property with a getter and a setter that is not init-only)".
///
///
public static readonly DiagnosticDescriptor InvalidPropertyDeclarationForObservableProperty = new DiagnosticDescriptor(
id: "MVVMTK0043",
title: "Invalid property declaration for [ObservableProperty]",
- messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)",
+ messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be an instance (non static) partial property with a getter and a setter that is not init-only)",
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
- description: "Properties annotated with [ObservableProperty] must be partial properties with a getter and a setter that is not init-only.",
+ description: "Properties annotated with [ObservableProperty] must be instance (non static) partial properties with a getter and a setter that is not init-only.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0043");
///
@@ -753,7 +753,7 @@ internal static class DiagnosticDescriptors
/// Format: "The field {0}.{1} using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and a partial property should be used instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)".
///
///
- public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatible = new(
+ public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatible = new DiagnosticDescriptor(
id: WinRTObservablePropertyOnFieldsIsNotAotCompatibleId,
title: "Using [ObservableProperty] on fields is not AOT compatible for WinRT",
messageFormat: """The field {0}.{1} using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and a partial property should be used instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)""",
@@ -769,7 +769,7 @@ internal static class DiagnosticDescriptors
/// Format: "The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty], which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)".
///
///
- public static readonly DiagnosticDescriptor WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible = new(
+ public static readonly DiagnosticDescriptor WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible = new DiagnosticDescriptor(
id: "MVVMTK0046",
title: "Using [RelayCommand] is not compatible with [GeneratedBindableCustomProperty]",
messageFormat: """The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty], which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)""",
@@ -785,7 +785,7 @@ internal static class DiagnosticDescriptors
/// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)".
///
///
- public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField = new(
+ public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField = new DiagnosticDescriptor(
id: "MVVMTK0047",
title: "Using [GeneratedBindableCustomProperty] is not compatible with [ObservableProperty] on fields",
messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""",
@@ -801,7 +801,7 @@ internal static class DiagnosticDescriptors
/// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1}: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)".
///
///
- public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand = new(
+ public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand = new DiagnosticDescriptor(
id: "MVVMTK0048",
title: "Using [GeneratedBindableCustomProperty] is not compatible with [RelayCommand]",
messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1}: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""",
@@ -849,7 +849,7 @@ internal static class DiagnosticDescriptors
/// Format: "This project produced one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, but it can't enable partial properties and the associated code fixer because 'LangVersion' is not set to 'preview' (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings)".
///
///
- public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo = new(
+ public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo = new DiagnosticDescriptor(
id: "MVVMTK0051",
title: "Using [ObservableProperty] with WinRT and AOT requires 'LangVersion=preview'",
messageFormat: """This project produced one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, but it can't enable partial properties and the associated code fixer because 'LangVersion' is not set to 'preview' (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings)""",
@@ -859,4 +859,52 @@ internal static class DiagnosticDescriptors
description: "This project producing one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, should set 'LangVersion' to 'preview' to enable partial properties and the associated code fixer (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings).",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0051",
customTags: WellKnownDiagnosticTags.CompilationEnd);
+
+ ///
+ /// Gets a for when [ObservableProperty] is used on a property that is not an incomplete partial definition.
+ ///
+ /// Format: "The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)".
+ ///
+ ///
+ public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition = new DiagnosticDescriptor(
+ id: "MVVMTK0052",
+ title: "Using [ObservableProperty] on an invalid property declaration (not incomplete partial definition)",
+ messageFormat: """The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)""",
+ category: typeof(ObservablePropertyGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "A property using [ObservableProperty] is not an incomplete partial definition part ([ObservableProperty] must be used on partial property definitions with no implementation part).",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0052");
+
+ ///
+ /// Gets a for when [ObservableProperty] is used on a property that returns a ref value.
+ ///
+ /// Format: "The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)".
+ ///
+ ///
+ public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsByRef = new DiagnosticDescriptor(
+ id: "MVVMTK0053",
+ title: "Using [ObservableProperty] on a property that returns byref",
+ messageFormat: """The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)""",
+ category: typeof(ObservablePropertyGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "A property using [ObservableProperty] returns a value by reference ([ObservableProperty] must be used on properties returning a type by value).",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0053");
+
+ ///
+ /// Gets a for when [ObservableProperty] is used on a property that returns a byref-like value.
+ ///
+ /// Format: "The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)".
+ ///
+ ///
+ public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsRefLikeType = new DiagnosticDescriptor(
+ id: "MVVMTK0054",
+ title: "Using [ObservableProperty] on a property that returns byref-like",
+ messageFormat: """The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)""",
+ category: typeof(ObservablePropertyGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "A property using [ObservableProperty] returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type).",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0054");
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs
index 2f82c8b3a..774024c9f 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs
@@ -4,6 +4,8 @@
using Microsoft.CodeAnalysis;
+#pragma warning disable IDE0090 // Use 'new SuppressionDescriptor(...)'
+
namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
///
@@ -14,7 +16,7 @@ internal static class SuppressionDescriptors
///
/// Gets a for a field using [ObservableProperty] with an attribute list targeting a property.
///
- public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new(
+ public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new SuppressionDescriptor(
id: "MVVMTKSPR0001",
suppressedDiagnosticId: "CS0657",
justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties");
@@ -22,7 +24,7 @@ internal static class SuppressionDescriptors
///
/// Gets a for a field using [ObservableProperty] with an attribute list targeting a get or set accessor.
///
- public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new(
+ public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new SuppressionDescriptor(
id: "MVVMTKSPR0001",
suppressedDiagnosticId: "CS0658",
justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties");
@@ -30,7 +32,7 @@ internal static class SuppressionDescriptors
///
/// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property.
///
- public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new(
+ public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new SuppressionDescriptor(
id: "MVVMTKSPR0002",
suppressedDiagnosticId: "CS0657",
justification: "Methods using [RelayCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties");
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs
index 14f7498af..096e6456e 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs
@@ -6,6 +6,7 @@
// more info in ThirdPartyNotices.txt in the root of the project.
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
@@ -53,6 +54,29 @@ properties.Value.Value is T argumentValue &&
return null;
}
+ ///
+ /// Tries to get a constructor argument at a given index from the input instance.
+ ///
+ /// The type of constructor argument to retrieve.
+ /// The target instance to get the argument from.
+ /// The index of the argument to try to retrieve.
+ /// The resulting argument, if it was found.
+ /// Whether or not an argument of type at position was found.
+ public static bool TryGetConstructorArgument(this AttributeData attributeData, int index, [NotNullWhen(true)] out T? result)
+ {
+ if (attributeData.ConstructorArguments.Length > index &&
+ attributeData.ConstructorArguments[index].Value is T argument)
+ {
+ result = argument;
+
+ return true;
+ }
+
+ result = default;
+
+ return false;
+ }
+
///
/// Gets a given named argument value from an instance, or a fallback value.
///
diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj
new file mode 100644
index 000000000..cfdec7b22
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net472;net7.0;net8.0
+ preview
+ true
+ $(DefineConstants);ROSLYN_4_12_0_OR_GREATER
+
+
+ $(NoWarn);MVVMTK0042
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs
new file mode 100644
index 000000000..ddeb5ebbf
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs
@@ -0,0 +1,1737 @@
+// 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.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+#if NET6_0_OR_GREATER
+using System.Runtime.CompilerServices;
+#endif
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.ExternalAssembly;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using CommunityToolkit.Mvvm.Messaging.Messages;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#pragma warning disable MVVMTK0032, MVVMTK0033, MVVMTK0034
+
+namespace CommunityToolkit.Mvvm.UnitTests;
+
+// Note: this class is a copy of 'Test_ObservablePropertyAttribute', but using partial properties.
+// The two implementations should be kept in sync for all tests, for parity, whenever possible.
+
+[TestClass]
+public partial class Test_ObservablePropertyAttribute_PartialProperties
+{
+ [TestMethod]
+ public void Test_ObservablePropertyAttribute_Events()
+ {
+ SampleModel model = new();
+
+ (PropertyChangingEventArgs, int) changing = default;
+ (PropertyChangedEventArgs, int) changed = default;
+
+ model.PropertyChanging += (s, e) =>
+ {
+ Assert.IsNull(changing.Item1);
+ Assert.IsNull(changed.Item1);
+ Assert.AreSame(model, s);
+ Assert.IsNotNull(s);
+ Assert.IsNotNull(e);
+
+ changing = (e, model.Data);
+ };
+
+ model.PropertyChanged += (s, e) =>
+ {
+ Assert.IsNotNull(changing.Item1);
+ Assert.IsNull(changed.Item1);
+ Assert.AreSame(model, s);
+ Assert.IsNotNull(s);
+ Assert.IsNotNull(e);
+
+ changed = (e, model.Data);
+ };
+
+ model.Data = 42;
+
+ Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data));
+ Assert.AreEqual(changing.Item2, 0);
+ Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data));
+ Assert.AreEqual(changed.Item2, 42);
+ }
+
+ // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225
+ [TestMethod]
+ public void Test_ObservablePropertyAttributeWithinRegion_Events()
+ {
+ SampleModel model = new();
+
+ (PropertyChangingEventArgs, int) changing = default;
+ (PropertyChangedEventArgs, int) changed = default;
+
+ model.PropertyChanging += (s, e) =>
+ {
+ Assert.IsNull(changing.Item1);
+ Assert.IsNull(changed.Item1);
+ Assert.AreSame(model, s);
+ Assert.IsNotNull(s);
+ Assert.IsNotNull(e);
+
+ changing = (e, model.Counter);
+ };
+
+ model.PropertyChanged += (s, e) =>
+ {
+ Assert.IsNotNull(changing.Item1);
+ Assert.IsNull(changed.Item1);
+ Assert.AreSame(model, s);
+ Assert.IsNotNull(s);
+ Assert.IsNotNull(e);
+
+ changed = (e, model.Counter);
+ };
+
+ model.Counter = 42;
+
+ Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Counter));
+ Assert.AreEqual(changing.Item2, 0);
+ Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Counter));
+ Assert.AreEqual(changed.Item2, 42);
+ }
+
+ // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225
+ [TestMethod]
+ public void Test_ObservablePropertyAttributeRightBelowRegion_Events()
+ {
+ SampleModel model = new();
+
+ (PropertyChangingEventArgs, string?) changing = default;
+ (PropertyChangedEventArgs, string?) changed = default;
+
+ model.PropertyChanging += (s, e) =>
+ {
+ Assert.IsNull(changing.Item1);
+ Assert.IsNull(changed.Item1);
+ Assert.AreSame(model, s);
+ Assert.IsNotNull(s);
+ Assert.IsNotNull(e);
+
+ changing = (e, model.Name);
+ };
+
+ model.PropertyChanged += (s, e) =>
+ {
+ Assert.IsNotNull(changing.Item1);
+ Assert.IsNull(changed.Item1);
+ Assert.AreSame(model, s);
+ Assert.IsNotNull(s);
+ Assert.IsNotNull(e);
+
+ changed = (e, model.Name);
+ };
+
+ model.Name = "Bob";
+
+ Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Name));
+ Assert.AreEqual(changing.Item2, null);
+ Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Name));
+ Assert.AreEqual(changed.Item2, "Bob");
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedForAttribute_Events()
+ {
+ DependentPropertyModel model = new();
+
+ List propertyNames = new();
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+
+ model.Name = "Bob";
+ model.Surname = "Ross";
+
+ CollectionAssert.AreEqual(new[] { nameof(model.Name), nameof(model.FullName), nameof(model.Alias), nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames);
+ }
+
+ [TestMethod]
+ public void Test_ValidationAttributes()
+ {
+ PropertyInfo nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name))!;
+
+ Assert.IsNotNull(nameProperty.GetCustomAttribute());
+ Assert.IsNotNull(nameProperty.GetCustomAttribute());
+ Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 1);
+ Assert.IsNotNull(nameProperty.GetCustomAttribute());
+ Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100);
+
+ PropertyInfo ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age))!;
+
+ Assert.IsNotNull(ageProperty.GetCustomAttribute());
+ Assert.AreEqual(ageProperty.GetCustomAttribute()!.Minimum, 0);
+ Assert.AreEqual(ageProperty.GetCustomAttribute()!.Maximum, 120);
+
+ PropertyInfo emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email))!;
+
+ Assert.IsNotNull(emailProperty.GetCustomAttribute());
+
+ PropertyInfo comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat))!;
+
+ TestValidationAttribute testAttribute = comboProperty.GetCustomAttribute()!;
+
+ Assert.IsNotNull(testAttribute);
+ Assert.IsNull(testAttribute.O);
+ Assert.AreEqual(testAttribute.T, typeof(SampleModel));
+ Assert.AreEqual(testAttribute.Flag, true);
+ Assert.AreEqual(testAttribute.D, 6.28);
+ CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" });
+
+ object[]? nestedArray = (object[]?)testAttribute.NestedArray;
+
+ Assert.IsNotNull(nestedArray);
+ Assert.AreEqual(nestedArray!.Length, 3);
+ Assert.AreEqual(nestedArray[0], 1);
+ Assert.AreEqual(nestedArray[1], "Hello");
+ Assert.IsTrue(nestedArray[2] is int[]);
+ CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 });
+
+ Assert.AreEqual(testAttribute.Animal, Animal.Llama);
+ }
+
+ // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216
+ [TestMethod]
+ public void Test_ObservablePropertyWithValueNamedField()
+ {
+ ModelWithValueProperty model = new();
+
+ List propertyNames = new();
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+
+ model.Value = "Hello world";
+
+ Assert.AreEqual(model.Value, "Hello world");
+
+ CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames);
+ }
+
+ // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216
+ [TestMethod]
+ public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributes()
+ {
+ ModelWithValuePropertyWithValidation model = new();
+
+ List propertyNames = new();
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+
+ bool errorsChanged = false;
+
+ model.ErrorsChanged += (s, e) => errorsChanged = true;
+
+ model.Value = "Hello world";
+
+ Assert.AreEqual(model.Value, "Hello world");
+
+ // The [NotifyDataErrorInfo] attribute wasn't used, so the property shouldn't be validated
+ Assert.IsFalse(errorsChanged);
+
+ CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames);
+ }
+
+ [TestMethod]
+ public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation()
+ {
+ ModelWithValuePropertyWithAutomaticValidation model = new();
+
+ List propertyNames = new();
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+
+ List errors = new();
+
+ model.ErrorsChanged += (s, e) => errors.Add(e);
+
+ model.Value = "Bo";
+
+ Assert.IsTrue(model.HasErrors);
+ Assert.AreEqual(errors.Count, 1);
+ Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidation.Value));
+
+ model.Value = "Hello world";
+
+ Assert.IsFalse(model.HasErrors);
+ Assert.AreEqual(errors.Count, 2);
+ Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidation.Value));
+ }
+
+ [TestMethod]
+ public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation_WithClassLevelAttribute()
+ {
+ ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute model = new();
+
+ List propertyNames = new();
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+
+ List errors = new();
+
+ model.ErrorsChanged += (s, e) => errors.Add(e);
+
+ model.Value = "Bo";
+
+ Assert.IsTrue(model.HasErrors);
+ Assert.AreEqual(errors.Count, 1);
+ Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute.Value));
+
+ model.Value = "Hello world";
+
+ Assert.IsFalse(model.HasErrors);
+ Assert.AreEqual(errors.Count, 2);
+ Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute.Value));
+ }
+
+ [TestMethod]
+ public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation_InheritingClassLevelAttribute()
+ {
+ ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute model = new();
+
+ List propertyNames = new();
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+
+ List errors = new();
+
+ model.ErrorsChanged += (s, e) => errors.Add(e);
+
+ model.Value2 = "Bo";
+
+ Assert.IsTrue(model.HasErrors);
+ Assert.AreEqual(errors.Count, 1);
+ Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute.Value2));
+
+ model.Value2 = "Hello world";
+
+ Assert.IsFalse(model.HasErrors);
+ Assert.AreEqual(errors.Count, 2);
+ Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute.Value2));
+ }
+
+ // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4184
+ [TestMethod]
+ public void Test_GeneratedPropertiesWithValidationAttributesOverFields()
+ {
+ ViewModelWithValidatableGeneratedProperties model = new();
+
+ List propertyNames = new();
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+
+ // Assign these fields directly to bypass the validation that is executed in the generated setters.
+ // We only need those generated properties to be there to check whether they are correctly detected.
+ model.First = "A";
+ model.Last = "This is a very long name that exceeds the maximum length of 60 for this property";
+
+ Assert.IsFalse(model.HasErrors);
+
+ model.RunValidation();
+
+ Assert.IsTrue(model.HasErrors);
+
+ ValidationResult[] validationErrors = model.GetErrors().ToArray();
+
+ Assert.AreEqual(validationErrors.Length, 2);
+
+ CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.First) }, validationErrors[0].MemberNames.ToArray());
+ CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.Last) }, validationErrors[1].MemberNames.ToArray());
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedFor()
+ {
+ DependentPropertyModel model = new();
+
+ List propertyNames = new();
+ int canExecuteRequests = 0;
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+ model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++;
+
+ model.Surname = "Ross";
+
+ Assert.AreEqual(1, canExecuteRequests);
+
+ CollectionAssert.AreEqual(new[] { nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames);
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedFor_GeneratedCommand()
+ {
+ DependentPropertyModel2 model = new();
+
+ List propertyNames = new();
+ int canExecuteRequests = 0;
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+ model.TestFromMethodCommand.CanExecuteChanged += (s, e) => canExecuteRequests++;
+
+ model.Text = "Ross";
+
+ Assert.AreEqual(1, canExecuteRequests);
+
+ CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames);
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedFor_IRelayCommandProperty()
+ {
+ DependentPropertyModel3 model = new();
+
+ List propertyNames = new();
+ int canExecuteRequests = 0;
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+ model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++;
+
+ model.Text = "Ross";
+
+ Assert.AreEqual(1, canExecuteRequests);
+
+ CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames);
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedFor_IAsyncRelayCommandOfTProperty()
+ {
+ DependentPropertyModel4 model = new();
+
+ List propertyNames = new();
+ int canExecuteRequests = 0;
+
+ model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
+ model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++;
+
+ model.Text = "Ross";
+
+ Assert.AreEqual(1, canExecuteRequests);
+
+ CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames);
+ }
+
+ [TestMethod]
+ public void Test_OnPropertyChangingAndChangedPartialMethods()
+ {
+ ViewModelWithImplementedUpdateMethods model = new();
+
+ model.Name = nameof(Test_OnPropertyChangingAndChangedPartialMethods);
+
+ Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangingValue);
+ Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangedValue);
+
+ model.Number = 99;
+
+ 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()
+ {
+ ViewModelWithImplementedUpdateMethodAndAdditionalValidation model = new();
+
+ // The actual validation is performed inside the model itself.
+ // This test validates that the order with which methods/events are generated is:
+ // - OnChanging(value);
+ // - OnPropertyChanging();
+ // - field = value;
+ // - OnChanged(value);
+ // - OnPropertyChanged();
+ model.Name = "B";
+
+ Assert.AreEqual("B", model.Name);
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedRecipients_WithObservableObject()
+ {
+ Test_NotifyPropertyChangedRecipients_Test(
+ factory: static messenger => new BroadcastingViewModel(messenger),
+ setter: static (model, value) => model.Name = value,
+ propertyName: nameof(BroadcastingViewModel.Name));
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedRecipients_WithObservableRecipientAttribute()
+ {
+ Test_NotifyPropertyChangedRecipients_Test(
+ factory: static messenger => new BroadcastingViewModelWithAttribute(messenger),
+ setter: static (model, value) => model.Name = value,
+ propertyName: nameof(BroadcastingViewModelWithAttribute.Name));
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttribute()
+ {
+ Test_NotifyPropertyChangedRecipients_Test(
+ factory: static messenger => new BroadcastingViewModelWithInheritedAttribute(messenger),
+ setter: static (model, value) => model.Name2 = value,
+ propertyName: nameof(BroadcastingViewModelWithInheritedAttribute.Name2));
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedRecipients_WithObservableObject_WithClassLevelAttribute()
+ {
+ Test_NotifyPropertyChangedRecipients_Test(
+ factory: static messenger => new BroadcastingViewModelWithClassLevelAttribute(messenger),
+ setter: static (model, value) => model.Name = value,
+ propertyName: nameof(BroadcastingViewModelWithClassLevelAttribute.Name));
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedRecipients_WithObservableRecipientAttribute_WithClassLevelAttribute()
+ {
+ Test_NotifyPropertyChangedRecipients_Test(
+ factory: static messenger => new BroadcastingViewModelWithAttributeAndClassLevelAttribute(messenger),
+ setter: static (model, value) => model.Name = value,
+ propertyName: nameof(BroadcastingViewModelWithAttributeAndClassLevelAttribute.Name));
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttribute_WithClassLevelAttribute()
+ {
+ Test_NotifyPropertyChangedRecipients_Test(
+ factory: static messenger => new BroadcastingViewModelWithInheritedClassLevelAttribute(messenger),
+ setter: static (model, value) => model.Name2 = value,
+ propertyName: nameof(BroadcastingViewModelWithInheritedClassLevelAttribute.Name2));
+ }
+
+ [TestMethod]
+ public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttributeAndClassLevelAttribute()
+ {
+ Test_NotifyPropertyChangedRecipients_Test(
+ factory: static messenger => new BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute(messenger),
+ setter: static (model, value) => model.Name2 = value,
+ propertyName: nameof(BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute.Name2));
+ }
+
+ private void Test_NotifyPropertyChangedRecipients_Test(Func factory, Action setter, string propertyName)
+ where T : notnull
+ {
+ IMessenger messenger = new StrongReferenceMessenger();
+
+ T model = factory(messenger);
+
+ List<(object Sender, PropertyChangedMessage Message)> messages = new();
+
+ messenger.Register>(model, (r, m) => messages.Add((r, m)));
+
+ setter(model, "Bob");
+
+ Assert.AreEqual(1, messages.Count);
+ Assert.AreSame(model, messages[0].Sender);
+ Assert.AreEqual(null, messages[0].Message.OldValue);
+ Assert.AreEqual("Bob", messages[0].Message.NewValue);
+ Assert.AreEqual(propertyName, messages[0].Message.PropertyName);
+
+ setter(model, "Ross");
+
+ Assert.AreEqual(2, messages.Count);
+ Assert.AreSame(model, messages[1].Sender);
+ Assert.AreEqual("Bob", messages[1].Message.OldValue);
+ Assert.AreEqual("Ross", messages[1].Message.NewValue);
+ Assert.AreEqual(propertyName, messages[0].Message.PropertyName);
+ }
+
+ [TestMethod]
+ public void Test_ObservableProperty_ObservableRecipientDoesNotBroadcastByDefault()
+ {
+ IMessenger messenger = new StrongReferenceMessenger();
+ RecipientWithNonBroadcastingProperty model = new(messenger);
+
+ List<(object Sender, PropertyChangedMessage Message)> messages = new();
+
+ messenger.Register>(model, (r, m) => messages.Add((r, m)));
+
+ model.Name = "Bob";
+ model.Name = "Alice";
+ model.Name = null;
+
+ // The [NotifyPropertyChangedRecipients] attribute wasn't used, so no messages should have been sent
+ Assert.AreEqual(messages.Count, 0);
+ }
+
+#if NET6_0_OR_GREATER
+ // See https://github.com/CommunityToolkit/dotnet/issues/155
+ [TestMethod]
+ public void Test_ObservableProperty_NullabilityAnnotations_Simple()
+ {
+ // List?
+ NullabilityInfoContext context = new();
+ NullabilityInfo info = context.Create(typeof(NullableRepro).GetProperty(nameof(NullableRepro.NullableList))!);
+
+ Assert.AreEqual(typeof(List), info.Type);
+ Assert.AreEqual(NullabilityState.Nullable, info.ReadState);
+ Assert.AreEqual(NullabilityState.Nullable, info.WriteState);
+ Assert.AreEqual(1, info.GenericTypeArguments.Length);
+
+ NullabilityInfo elementInfo = info.GenericTypeArguments[0];
+
+ Assert.AreEqual(typeof(string), elementInfo.Type);
+ Assert.AreEqual(NullabilityState.Nullable, elementInfo.ReadState);
+ Assert.AreEqual(NullabilityState.Nullable, elementInfo.WriteState);
+ }
+
+ // See https://github.com/CommunityToolkit/dotnet/issues/155
+ [TestMethod]
+ public void Test_ObservableProperty_NullabilityAnnotations_Complex()
+ {
+ // Foo.Bar