diff --git a/dotnet.slnx b/dotnet.slnx
index 7cf15708b..812c7e3fb 100644
--- a/dotnet.slnx
+++ b/dotnet.slnx
@@ -7,6 +7,7 @@
+
@@ -30,6 +31,7 @@
+
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 000000000..6e1bf280b
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems
index 0b0abb069..fb8bfe9b7 100644
--- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems
+++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems
@@ -12,10 +12,11 @@
+
-
\ No newline at end of file
+
diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/MakeObservableValidatorTypePartialCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/MakeObservableValidatorTypePartialCodeFixer.cs
new file mode 100644
index 000000000..220c90512
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.CodeFixers/MakeObservableValidatorTypePartialCodeFixer.cs
@@ -0,0 +1,67 @@
+// 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.Collections.Immutable;
+using System.Composition;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Editing;
+using Microsoft.CodeAnalysis.Text;
+
+namespace CommunityToolkit.Mvvm.CodeFixers;
+
+///
+/// A code fixer that makes types partial for generated ObservableValidator validation support.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp)]
+[Shared]
+public sealed class MakeObservableValidatorTypePartialCodeFixer : CodeFixProvider
+{
+ ///
+ public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.ObservableValidatorTypeMustBePartialId);
+
+ ///
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ Diagnostic diagnostic = context.Diagnostics[0];
+ TextSpan diagnosticSpan = context.Span;
+
+ SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ if (root?.FindNode(diagnosticSpan).FirstAncestorOrSelf() is TypeDeclarationSyntax typeDeclaration)
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Make type partial",
+ createChangedDocument: token => MakeTypePartialAsync(context.Document, root, typeDeclaration),
+ equivalenceKey: "Make type partial"),
+ diagnostic);
+ }
+ }
+
+ ///
+ /// Applies the code fix to a target type declaration and returns an updated document.
+ ///
+ /// The original document being fixed.
+ /// The original tree root belonging to the current document.
+ /// The to update.
+ /// An updated document with the applied code fix.
+ private static Task MakeTypePartialAsync(Document document, SyntaxNode root, TypeDeclarationSyntax typeDeclaration)
+ {
+ SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
+ TypeDeclarationSyntax updatedTypeDeclaration = (TypeDeclarationSyntax)generator.WithModifiers(typeDeclaration, generator.GetModifiers(typeDeclaration).WithPartial(true));
+
+ return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(typeDeclaration, updatedTypeDeclaration)));
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md
index f2b7fad65..5f1457151 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -1,2 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+MVVMTK0057 | CommunityToolkit.Mvvm.SourceGenerators.ObservableValidatorValidationGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0057
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
index c3294780f..81e390625 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
@@ -28,15 +28,17 @@
+
+
-
-
+
+
@@ -59,6 +61,7 @@
+
@@ -113,4 +116,4 @@
-
\ No newline at end of file
+
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
index 1fee5b622..07a05384b 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
@@ -24,10 +24,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
/// The sequence of commands to notify.
/// Whether or not the generated property also broadcasts changes.
/// Whether or not the generated property also validates its value.
+/// Whether or not the generated property has validation attributes on the effective generated property.
/// Whether the old property value is being directly referenced.
/// Indicates whether the property is of a reference type or an unconstrained type parameter.
/// Indicates whether to include nullability annotations on the setter.
-/// Indicates whether to annotate the setter as requiring unreferenced code.
/// The sequence of forwarded attributes for the generated property.
internal sealed record PropertyInfo(
SyntaxKind AnnotatedMemberKind,
@@ -43,8 +43,8 @@ internal sealed record PropertyInfo(
EquatableArray NotifiedCommandNames,
bool NotifyPropertyChangedRecipients,
bool NotifyDataErrorInfo,
+ bool HasValidationAttributes,
bool IsOldPropertyValueDirectlyReferenced,
bool IsReferenceTypeOrUnconstrainedTypeParameter,
bool IncludeMemberNotNullOnSetAccessor,
- bool IncludeRequiresUnreferencedCodeOnSetAccessor,
EquatableArray ForwardedAttributes);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyValidationInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyValidationInfo.cs
new file mode 100644
index 000000000..8238958e7
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyValidationInfo.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
+
+///
+/// A model with gathered info on a locally declared validatable property.
+///
+/// The name of the property to validate.
+/// Whether the property has a DisplayAttribute .
+internal sealed record PropertyValidationInfo(string PropertyName, bool HasDisplayAttribute);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ValidationInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ValidationInfo.cs
index 7d5b7b5d9..de7ec1ce1 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ValidationInfo.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ValidationInfo.cs
@@ -3,13 +3,14 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
+using CommunityToolkit.Mvvm.SourceGenerators.Models;
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
///
/// A model with gathered info on all validatable properties in a given type.
///
-/// The filename hint for the current type.
+/// The hierarchy for the current type.
/// The fully qualified type name of the target type.
-/// The name of validatable properties.
-internal sealed record ValidationInfo(string FilenameHint, string TypeName, EquatableArray PropertyNames);
+/// The locally declared validatable properties for the current type.
+internal sealed record ValidationInfo(HierarchyInfo Hierarchy, string TypeName, EquatableArray Properties);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ValidationTypeInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ValidationTypeInfo.cs
new file mode 100644
index 000000000..208491b23
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ValidationTypeInfo.cs
@@ -0,0 +1,14 @@
+// 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 CommunityToolkit.Mvvm.SourceGenerators.Models;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
+
+///
+/// A model with identity information for a type requiring generated validation hooks.
+///
+/// The hierarchy for the target type.
+/// The fully qualified type name for the target type.
+internal sealed record ValidationTypeInfo(HierarchyInfo Hierarchy, string TypeName);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
index 2b2756ec4..75deb4ffb 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
@@ -327,6 +327,7 @@ public static bool TryGetInfo(
memberSyntax,
memberSymbol,
semanticModel,
+ ref hasAnyValidationAttributes,
in forwardedAttributes,
in builder,
token);
@@ -359,13 +360,6 @@ public static bool TryGetInfo(
token.ThrowIfCancellationRequested();
- // We should generate [RequiresUnreferencedCode] on the setter if [NotifyDataErrorInfo] was used and the attribute is available
- bool includeRequiresUnreferencedCodeOnSetAccessor =
- notifyDataErrorInfo &&
- semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute");
-
- token.ThrowIfCancellationRequested();
-
// Prepare the effective property changing/changed names. For the property changing names,
// there are two possible cases: if the mode is disabled, then there are no names to report
// at all. If the mode is enabled, then the list is just the same as for property changed.
@@ -411,10 +405,10 @@ public static bool TryGetInfo(
notifiedCommandNames.ToImmutable(),
notifyRecipients,
notifyDataErrorInfo,
+ hasAnyValidationAttributes,
isOldPropertyValueDirectlyReferenced,
isReferenceTypeOrUnconstrainedTypeParameter,
includeMemberNotNullOnSetAccessor,
- includeRequiresUnreferencedCodeOnSetAccessor,
forwardedAttributes.ToImmutable());
diagnostics = builder.ToImmutable();
@@ -885,6 +879,7 @@ private static void GetNullabilityInfo(
/// The instance to process.
/// The input instance to process.
/// The instance for the current run.
+ /// Tracks whether the effective generated property has validation attributes.
/// The collection of forwarded attributes to add new ones to.
/// The current collection of gathered diagnostics.
/// The cancellation token for the current operation.
@@ -892,6 +887,7 @@ private static void GatherLegacyForwardedAttributes(
MemberDeclarationSyntax memberSyntax,
ISymbol memberSymbol,
SemanticModel semanticModel,
+ ref bool hasAnyValidationAttributes,
in ImmutableArrayBuilder forwardedAttributes,
in ImmutableArrayBuilder diagnostics,
CancellationToken token)
@@ -965,6 +961,12 @@ private static void GatherLegacyForwardedAttributes(
continue;
}
+ if (targetIdentifier.IsKind(SyntaxKind.PropertyKeyword) &&
+ attributeTypeSymbol.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"))
+ {
+ hasAnyValidationAttributes = true;
+ }
+
IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty();
// Try to extract the forwarded attribute
@@ -1376,19 +1378,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.FieldName)))))));
}
- // Add the [RequiresUnreferencedCode] attribute if needed:
- //
- // [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- //
- if (propertyInfo.IncludeRequiresUnreferencedCodeOnSetAccessor)
- {
- setAccessor = setAccessor.AddAttributeLists(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode"))
- .AddArgumentListArguments(
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered.")))))));
- }
-
// Also add any forwarded attributes
setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes);
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs
deleted file mode 100644
index 0b82211ba..000000000
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs
+++ /dev/null
@@ -1,305 +0,0 @@
-// 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.Immutable;
-using System.Linq;
-using System.Threading;
-using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
-using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
-using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
-
-namespace CommunityToolkit.Mvvm.SourceGenerators;
-
-///
-partial class ObservableValidatorValidateAllPropertiesGenerator
-{
- ///
- /// A container for all the logic for .
- ///
- private static class Execute
- {
- ///
- /// Checks whether a given type inherits from ObservableValidator .
- ///
- /// The input instance to check.
- /// Whether inherits from ObservableValidator .
- public static bool IsObservableValidator(INamedTypeSymbol typeSymbol)
- {
- return typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
- }
-
- ///
- /// Gets the instance from an input symbol.
- ///
- /// The input instance to inspect.
- /// The cancellation token for the current operation.
- /// The resulting instance for .
- public static ValidationInfo GetInfo(INamedTypeSymbol typeSymbol, CancellationToken token)
- {
- using ImmutableArrayBuilder propertyNames = ImmutableArrayBuilder.Rent();
-
- foreach (ISymbol memberSymbol in typeSymbol.GetAllMembers())
- {
- if (memberSymbol is { IsStatic: true } or not (IPropertySymbol { IsIndexer: false } or IFieldSymbol))
- {
- continue;
- }
-
- token.ThrowIfCancellationRequested();
-
- ImmutableArray attributes = memberSymbol.GetAttributes();
-
- // Also include fields that are annotated with [ObservableProperty]. This is necessary because
- // all generators run in an undefined order and looking at the same original compilation, so the
- // current one wouldn't be able to see generated properties from other generators directly.
- if (memberSymbol is IFieldSymbol &&
- !attributes.Any(static a => a.AttributeClass?.HasFullyQualifiedMetadataName(
- "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") == true))
- {
- continue;
- }
-
- // Skip the current member if there are no validation attributes applied to it
- if (!attributes.Any(a => a.AttributeClass?.InheritsFromFullyQualifiedMetadataName(
- "System.ComponentModel.DataAnnotations.ValidationAttribute") == true))
- {
- continue;
- }
-
- // Get the target property name either directly or matching the generated one
- string propertyName = memberSymbol switch
- {
- IPropertySymbol propertySymbol => propertySymbol.Name,
- IFieldSymbol fieldSymbol => ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol),
- _ => throw new InvalidOperationException("Invalid symbol type")
- };
-
- propertyNames.Add(propertyName);
- }
-
- token.ThrowIfCancellationRequested();
-
- return new(
- typeSymbol.GetFullyQualifiedMetadataName(),
- typeSymbol.GetFullyQualifiedName(),
- propertyNames.ToImmutable());
- }
-
- ///
- /// Gets the head instance.
- ///
- /// Indicates whether [DynamicallyAccessedMembers] should be generated.
- /// The head instance with the type attributes.
- public static CompilationUnitSyntax GetSyntax(bool isDynamicallyAccessedMembersAttributeAvailable)
- {
- using ImmutableArrayBuilder attributes = ImmutableArrayBuilder.Rent();
-
- // Prepare the base attributes with are always present:
- //
- // ///
- // /// A helper type with generated validation stubs for types deriving from .
- // ///
- // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
- // [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")]
- attributes.Add(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")).AddArgumentListArguments(
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidateAllPropertiesGenerator).FullName))),
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidateAllPropertiesGenerator).Assembly.GetName().Version.ToString()))))))
- .WithOpenBracketToken(Token(TriviaList(
- Comment("/// "),
- Comment("/// A helper type with generated validation stubs for types deriving from ."),
- Comment("/// ")), SyntaxKind.OpenBracketToken, TriviaList())));
- attributes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))));
- attributes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))));
- attributes.Add(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
- AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))));
- attributes.Add(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
- AttributeArgument(LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- Literal("This type is not intended to be used directly by user code")))))));
-
- if (isDynamicallyAccessedMembersAttributeAvailable)
- {
- // Conditionally add the attribute to inform trimming, if the type is available:
- //
- // [global::System.CodeDom.Compiler.DynamicallyAccessedMembersAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)]
- attributes.Add(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute")).AddArgumentListArguments(
- AttributeArgument(ParseExpression("global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods"))))));
- }
-
- // This code produces a compilation unit as follows:
- //
- // //
- // #pragma warning disable
- // namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
- // {
- //
- // internal static partial class __ObservableValidatorExtensions
- // {
- // }
- // }
- return
- CompilationUnit().AddMembers(
- NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
- Comment("// "),
- Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
- ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(
- Token(SyntaxKind.InternalKeyword),
- Token(SyntaxKind.StaticKeyword),
- Token(SyntaxKind.PartialKeyword))
- .AddAttributeLists(attributes.ToArray())))
- .NormalizeWhitespace();
- }
-
- ///
- /// Gets the instance for the input recipient.
- ///
- /// The input instance to process.
- /// The generated instance for .
- public static CompilationUnitSyntax GetSyntax(ValidationInfo validationInfo)
- {
- // Create a static factory method creating a delegate that can be used to validate all properties in a given class.
- // This pattern is used so that the library doesn't have to use MakeGenericType(...) at runtime, nor use unsafe casts
- // over the created delegate to be able to cache it as an Action instance. This pattern enables the same
- // functionality and with almost identical performance (not noticeable in this context anyway), but while preserving
- // full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed.
- // Note that we're deliberately creating a new delegate instance here and not using code that could see the C# compiler
- // create a static class to cache a reusable delegate, because each generated method will only be called at most once,
- // as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the produced code is minimal,
- // and that there will be no unnecessary static fields and objects being created and possibly never collected.
- // This code will produce a syntax tree as follows:
- //
- // //
- // #pragma warning disable
- // namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
- // {
- // ///
- // partial class __ObservableValidatorExtensions
- // {
- // ///
- // /// Creates a validation stub for objects.
- // ///
- // /// Dummy parameter, only used to disambiguate the method signature.
- // /// A validation stub for objects.
- // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- // [global::System.Obsolete("This method is not intended to be called directly by user code")]
- // public static global::System.Action CreateAllPropertiesValidator( _)
- // {
- // static void ValidateAllProperties(object obj)
- // {
- // var instance = ()obj;
- //
- // }
- //
- // return ValidateAllProperties;
- // }
- // }
- // }
- return
- CompilationUnit().AddMembers(
- NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
- Comment("// "),
- Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
- ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(
- Token(TriviaList(Comment("/// ")), SyntaxKind.PartialKeyword, TriviaList())).AddMembers(
- MethodDeclaration(
- GenericName("global::System.Action").AddTypeArgumentListArguments(PredefinedType(Token(SyntaxKind.ObjectKeyword))),
- Identifier("CreateAllPropertiesValidator")).AddAttributeLists(
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
- AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never")))))
- .WithOpenBracketToken(Token(TriviaList(
- Comment("/// "),
- Comment($"/// Creates a validation stub for objects."),
- Comment("/// "),
- Comment("/// Dummy parameter, only used to disambiguate the method signature."),
- Comment($"/// A validation stub for objects. ")), SyntaxKind.OpenBracketToken, TriviaList())),
- AttributeList(SingletonSeparatedList(
- Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
- AttributeArgument(LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- Literal("This method is not intended to be called directly by user code"))))))).AddModifiers(
- Token(SyntaxKind.PublicKeyword),
- Token(SyntaxKind.StaticKeyword)).AddParameterListParameters(
- Parameter(Identifier("_")).WithType(IdentifierName(validationInfo.TypeName)))
- .WithBody(Block(
- LocalFunctionStatement(
- PredefinedType(Token(SyntaxKind.VoidKeyword)),
- Identifier("ValidateAllProperties"))
- .AddModifiers(Token(SyntaxKind.StaticKeyword))
- .AddParameterListParameters(
- Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))))
- .WithBody(Block(
- LocalDeclarationStatement(
- VariableDeclaration(IdentifierName("var")) // Cannot use Token(SyntaxKind.VarKeyword) here (throws an ArgumentException)
- .AddVariables(
- VariableDeclarator(Identifier("instance"))
- .WithInitializer(EqualsValueClause(
- CastExpression(
- IdentifierName(validationInfo.TypeName),
- IdentifierName("obj")))))))
- .AddStatements(EnumerateValidationStatements(validationInfo).ToArray())),
- ReturnStatement(IdentifierName("ValidateAllProperties")))))))
- .NormalizeWhitespace();
- }
-
- ///
- /// Gets a sequence of statements to validate declared properties.
- ///
- /// The input instance to process.
- /// The sequence of instances to validate declared properties.
- private static ImmutableArray EnumerateValidationStatements(ValidationInfo validationInfo)
- {
- using ImmutableArrayBuilder statements = ImmutableArrayBuilder.Rent();
-
- // This loop produces a sequence of statements as follows:
- //
- // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.));
- // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.));
- // ...
- // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.));
- foreach (string propertyName in validationInfo.PropertyNames)
- {
- statements.Add(
- ExpressionStatement(
- InvocationExpression(
- MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- IdentifierName("__ObservableValidatorHelper"),
- IdentifierName("ValidateProperty")))
- .AddArgumentListArguments(
- Argument(IdentifierName("instance")),
- Argument(
- MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- IdentifierName("instance"),
- IdentifierName(propertyName))),
- Argument(
- InvocationExpression(IdentifierName("nameof"))
- .AddArgumentListArguments(Argument(
- MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- IdentifierName("instance"),
- IdentifierName(propertyName))))))));
- }
-
- return statements.ToImmutable();
- }
- }
-}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs
deleted file mode 100644
index 4ec9bc6f0..000000000
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-// 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.Linq;
-using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
-using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-namespace CommunityToolkit.Mvvm.SourceGenerators;
-
-///
-/// A source generator for property validation without relying on compiled LINQ expressions.
-///
-[Generator(LanguageNames.CSharp)]
-public sealed partial class ObservableValidatorValidateAllPropertiesGenerator : IIncrementalGenerator
-{
- ///
- public void Initialize(IncrementalGeneratorInitializationContext context)
- {
- // Get the types that inherit from ObservableValidator and gather their info
- IncrementalValuesProvider validationInfo =
- context.SyntaxProvider
- .CreateSyntaxProvider(
- static (node, _) => node is ClassDeclarationSyntax classDeclaration && classDeclaration.HasOrPotentiallyHasBaseTypes(),
- static (context, token) =>
- {
- if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
- {
- return default;
- }
-
- INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node, token)!;
-
- // Skip generating code for abstract types, as that would never be used. The methods that are generated by
- // this generator are retrieved through reflection using the type of the invoking instance as discriminator,
- // which means a type that is abstract could never be used (since it couldn't be instantiated).
- if (typeSymbol is not { IsAbstract: false, IsGenericType: false })
- {
- return default;
- }
-
- // Just like in IMessengerRegisterAllGenerator, only select the first declaration for this type symbol
- if (!context.Node.IsFirstSyntaxDeclarationForSymbol(typeSymbol))
- {
- return default;
- }
-
- token.ThrowIfCancellationRequested();
-
- // Only select types inheriting from ObservableValidator
- if (!Execute.IsObservableValidator(typeSymbol))
- {
- return default;
- }
-
- token.ThrowIfCancellationRequested();
-
- return Execute.GetInfo(typeSymbol, token);
- })
- .Where(static item => item is not null)!;
-
- // Check whether the header file is needed
- IncrementalValueProvider isHeaderFileNeeded =
- validationInfo
- .Collect()
- .Select(static (item, _) => item.Length > 0);
-
- // Check whether [DynamicallyAccessedMembers] is available
- IncrementalValueProvider isDynamicallyAccessedMembersAttributeAvailable =
- context.CompilationProvider
- .Select(static (item, _) => item.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute"));
-
- // Gather the conditional flag and attribute availability
- IncrementalValueProvider<(bool IsHeaderFileNeeded, bool IsDynamicallyAccessedMembersAttributeAvailable)> headerFileInfo =
- isHeaderFileNeeded.Combine(isDynamicallyAccessedMembersAttributeAvailable);
-
- // Generate the header file with the attributes
- context.RegisterConditionalImplementationSourceOutput(headerFileInfo, static (context, item) =>
- {
- CompilationUnitSyntax compilationUnit = Execute.GetSyntax(item);
-
- context.AddSource("__ObservableValidatorExtensions.g.cs", compilationUnit);
- });
-
- // Generate the class with all validation methods
- context.RegisterImplementationSourceOutput(validationInfo, static (context, item) =>
- {
- CompilationUnitSyntax compilationUnit = Execute.GetSyntax(item);
-
- context.AddSource($"{item.FilenameHint}.g.cs", compilationUnit);
- });
- }
-}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidationGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidationGenerator.Execute.cs
new file mode 100644
index 000000000..8596dbbec
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidationGenerator.Execute.cs
@@ -0,0 +1,359 @@
+// 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.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
+using CommunityToolkit.Mvvm.SourceGenerators.Models;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+partial class ObservableValidatorValidationGenerator
+{
+ ///
+ /// A container for all the logic for .
+ ///
+ private static class Execute
+ {
+ ///
+ /// Checks whether a given type inherits from ObservableValidator .
+ ///
+ /// The input instance to check.
+ /// Whether inherits from ObservableValidator .
+ public static bool IsObservableValidator(INamedTypeSymbol typeSymbol)
+ {
+ return typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
+ }
+
+ ///
+ /// Gets the instance from an input symbol.
+ ///
+ /// The input instance to inspect.
+ /// The cancellation token for the current operation.
+ /// The resulting instance for , if available.
+ public static ValidationInfo? GetInfo(INamedTypeSymbol typeSymbol, CancellationToken token)
+ {
+ if (!typeSymbol.IsTypeHierarchyPartial())
+ {
+ return default;
+ }
+
+ using ImmutableArrayBuilder properties = ImmutableArrayBuilder.Rent();
+
+ foreach (ISymbol memberSymbol in typeSymbol.GetMembers())
+ {
+ if (memberSymbol is not IPropertySymbol propertySymbol ||
+ propertySymbol.IsStatic ||
+ propertySymbol.IsIndexer ||
+ !propertySymbol.CanBeReferencedByName)
+ {
+ continue;
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ if (!propertySymbol.GetAttributes().Any(static a => a.AttributeClass?.InheritsFromFullyQualifiedMetadataName(
+ "System.ComponentModel.DataAnnotations.ValidationAttribute") == true))
+ {
+ continue;
+ }
+
+ properties.Add(new(propertySymbol.Name, HasDisplayAttribute(propertySymbol.GetAttributes())));
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ if (properties.WrittenSpan.IsEmpty)
+ {
+ return default;
+ }
+
+ return new(
+ HierarchyInfo.From(typeSymbol),
+ typeSymbol.GetFullyQualifiedName(),
+ properties.ToImmutable());
+ }
+
+ ///
+ /// Gets the validation info for a field-backed observable property, if applicable.
+ ///
+ /// The source member declaration for the annotated field.
+ /// The field symbol annotated with [ObservableProperty] .
+ /// The semantic model for the current run.
+ /// The analyzer config options in use.
+ /// The cancellation token for the current operation.
+ /// The validation info for , if available.
+ public static (ValidationTypeInfo Left, PropertyValidationInfo Right) GetGeneratedObservablePropertyValidationInfo(
+ MemberDeclarationSyntax memberSyntax,
+ IFieldSymbol fieldSymbol,
+ SemanticModel semanticModel,
+ AnalyzerConfigOptions options,
+ CancellationToken token)
+ {
+ if (!fieldSymbol.ContainingType.IsTypeHierarchyPartial() ||
+ !IsObservableValidator(fieldSymbol.ContainingType) ||
+ !ObservablePropertyGenerator.Execute.TryGetInfo(memberSyntax, fieldSymbol, semanticModel, options, token, out PropertyInfo? propertyInfo, out _ ) ||
+ propertyInfo is not { HasValidationAttributes: true })
+ {
+ return default;
+ }
+
+ return (
+ new ValidationTypeInfo(HierarchyInfo.From(fieldSymbol.ContainingType), fieldSymbol.ContainingType.GetFullyQualifiedName()),
+ new PropertyValidationInfo(propertyInfo.PropertyName, HasDisplayAttribute(propertyInfo.ForwardedAttributes)));
+ }
+
+ ///
+ /// Gets the instance for the input recipient.
+ ///
+ /// The input instance to process.
+ /// The generated instance for .
+ public static CompilationUnitSyntax GetSyntax(ValidationInfo validationInfo)
+ {
+ ImmutableArray memberDeclarations = GetMemberDeclarations(validationInfo);
+
+ return validationInfo.Hierarchy.GetCompilationUnit(memberDeclarations)
+ .AddUsings(
+ UsingDirective(ParseName("System.Collections.Generic")),
+ UsingDirective(ParseName("System.ComponentModel.DataAnnotations")),
+ UsingDirective(ParseName("System.Reflection")))
+ .NormalizeWhitespace();
+ }
+
+ ///
+ /// Gets the compatibility header instance.
+ ///
+ /// Indicates whether [DynamicallyAccessedMembers] should be generated.
+ /// The header instance with the compatibility type.
+ public static CompilationUnitSyntax GetHeaderSyntax(bool isDynamicallyAccessedMembersAttributeAvailable)
+ {
+ using ImmutableArrayBuilder attributes = ImmutableArrayBuilder.Rent();
+
+ attributes.Add(
+ AttributeList(SingletonSeparatedList(
+ Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")).AddArgumentListArguments(
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidationGenerator).FullName))),
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidationGenerator).Assembly.GetName().Version!.ToString()))))))
+ .WithOpenBracketToken(Token(TriviaList(
+ Comment("/// "),
+ Comment("/// A compatibility helper type for validation-related generated code."),
+ Comment("/// ")), SyntaxKind.OpenBracketToken, TriviaList())));
+ attributes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))));
+ attributes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))));
+ attributes.Add(
+ AttributeList(SingletonSeparatedList(
+ Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
+ AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))));
+ attributes.Add(
+ AttributeList(SingletonSeparatedList(
+ Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
+ AttributeArgument(LiteralExpression(
+ SyntaxKind.StringLiteralExpression,
+ Literal("This type is not intended to be used directly by user code")))))));
+
+ if (isDynamicallyAccessedMembersAttributeAvailable)
+ {
+ attributes.Add(
+ AttributeList(SingletonSeparatedList(
+ Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute")).AddArgumentListArguments(
+ AttributeArgument(ParseExpression("global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods"))))));
+ }
+
+ return
+ CompilationUnit().AddMembers(
+ NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
+ Comment("// "),
+ Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
+ ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(
+ Token(SyntaxKind.InternalKeyword),
+ Token(SyntaxKind.StaticKeyword),
+ Token(SyntaxKind.PartialKeyword))
+ .AddAttributeLists(attributes.ToArray())))
+ .NormalizeWhitespace();
+ }
+
+ ///
+ /// Creates the generated members for a target type.
+ ///
+ /// The validation info for the target type.
+ /// The generated members for .
+ private static ImmutableArray GetMemberDeclarations(ValidationInfo validationInfo)
+ {
+ using ImmutableArrayBuilder members = ImmutableArrayBuilder.Rent();
+
+ foreach (PropertyValidationInfo propertyInfo in validationInfo.Properties)
+ {
+ string validationHelperName = GetValidationAttributesHelperName(propertyInfo.PropertyName);
+
+ members.Add(ParseMemberDeclaration($$"""
+ private static readonly IEnumerable {{validationHelperName}} =
+ typeof({{validationInfo.TypeName}}).GetProperty(nameof({{propertyInfo.PropertyName}}))!.GetCustomAttributes();
+ """)!);
+
+ if (propertyInfo.HasDisplayAttribute)
+ {
+ string displayHelperName = GetDisplayAttributeHelperName(propertyInfo.PropertyName);
+
+ members.Add(ParseMemberDeclaration($$"""
+ private static readonly DisplayAttribute {{displayHelperName}} =
+ typeof({{validationInfo.TypeName}}).GetProperty(nameof({{propertyInfo.PropertyName}}))!.GetCustomAttribute()!;
+ """)!);
+ }
+ }
+
+ members.Add(ParseMemberDeclaration(GetTryValidatePropertyCoreSource(validationInfo))!);
+ members.Add(ParseMemberDeclaration(GetValidateAllPropertiesCoreSource(validationInfo))!);
+
+ return members.ToImmutable();
+ }
+
+ ///
+ /// Creates the source for the generated TryValidatePropertyCore override.
+ ///
+ /// The validation info for the current type.
+ /// The generated member source.
+ private static string GetTryValidatePropertyCoreSource(ValidationInfo validationInfo)
+ {
+ StringBuilder builder = new();
+
+ builder.AppendLine("/// ");
+ builder.AppendLine("protected override ValidationStatus TryValidatePropertyCore(object? value, string propertyName, ICollection errors)");
+ builder.AppendLine("{");
+ builder.AppendLine(" return (propertyName) switch");
+ builder.AppendLine(" {");
+
+ foreach (PropertyValidationInfo propertyInfo in validationInfo.Properties)
+ {
+ string validationHelperName = GetValidationAttributesHelperName(propertyInfo.PropertyName);
+ string displayNameExpression = propertyInfo.HasDisplayAttribute
+ ? $"{GetDisplayAttributeHelperName(propertyInfo.PropertyName)}.GetName() ?? {SymbolDisplay.FormatLiteral(propertyInfo.PropertyName, true)}"
+ : SymbolDisplay.FormatLiteral(propertyInfo.PropertyName, true);
+
+ builder.AppendLine($" nameof({propertyInfo.PropertyName}) => TryValidateValue(value, nameof({propertyInfo.PropertyName}), {displayNameExpression}, {validationHelperName}, errors),");
+ }
+
+ builder.AppendLine(" _ => base.TryValidatePropertyCore(value, propertyName, errors)");
+ builder.AppendLine(" };");
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
+
+ ///
+ /// Creates the source for the generated ValidateAllPropertiesCore override.
+ ///
+ /// The validation info for the current type.
+ /// The generated member source.
+ private static string GetValidateAllPropertiesCoreSource(ValidationInfo validationInfo)
+ {
+ StringBuilder builder = new();
+
+ builder.AppendLine("/// ");
+ builder.AppendLine("protected override void ValidateAllPropertiesCore()");
+ builder.AppendLine("{");
+
+ foreach (PropertyValidationInfo propertyInfo in validationInfo.Properties)
+ {
+ builder.AppendLine($" ValidateProperty({propertyInfo.PropertyName}, nameof({propertyInfo.PropertyName}));");
+ }
+
+ builder.AppendLine(" base.ValidateAllPropertiesCore();");
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
+
+ ///
+ /// Creates a stable helper name for a given property.
+ ///
+ /// The property name to process.
+ /// A stable helper name for .
+ private static string GetValidationAttributesHelperName(string propertyName)
+ {
+ return GetHelperName(propertyName, "ValidationAttributes");
+ }
+
+ ///
+ /// Creates a stable display helper name for a given property.
+ ///
+ /// The property name to process.
+ /// A stable display helper name for .
+ private static string GetDisplayAttributeHelperName(string propertyName)
+ {
+ return GetHelperName(propertyName, "DisplayAttribute");
+ }
+
+ ///
+ /// Creates a stable helper name for a given property and suffix.
+ ///
+ /// The property name to process.
+ /// The suffix to append.
+ /// A stable helper name for .
+ private static string GetHelperName(string propertyName, string suffix)
+ {
+ StringBuilder builder = new("__");
+
+ foreach (char c in propertyName)
+ {
+ builder.Append(char.IsLetterOrDigit(c) ? c : '_');
+ }
+
+ if (builder.Length == 2 || !char.IsLetter(builder[2]) && builder[2] != '_')
+ {
+ builder.Insert(2, '_');
+ }
+
+ builder.Append(suffix);
+
+ return builder.ToString();
+ }
+
+ ///
+ /// Checks whether a property has a DisplayAttribute declared.
+ ///
+ /// The attributes declared on the property.
+ /// Whether the property has a display attribute.
+ private static bool HasDisplayAttribute(ImmutableArray attributes)
+ {
+ foreach (AttributeData attributeData in attributes)
+ {
+ if (attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks whether a generated property has a forwarded DisplayAttribute .
+ ///
+ /// The forwarded property attributes.
+ /// Whether the property has a display attribute.
+ private static bool HasDisplayAttribute(EquatableArray attributes)
+ {
+ foreach (AttributeInfo attributeInfo in attributes)
+ {
+ if (attributeInfo.TypeName == "global::System.ComponentModel.DataAnnotations.DisplayAttribute")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidationGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidationGenerator.cs
new file mode 100644
index 000000000..5c7335812
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidationGenerator.cs
@@ -0,0 +1,118 @@
+// 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.Linq;
+using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+/// A source generator for hook-based ObservableValidator validation.
+///
+[Generator(LanguageNames.CSharp)]
+public sealed partial class ObservableValidatorValidationGenerator : IIncrementalGenerator
+{
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ IncrementalValuesProvider explicitValidationInfo =
+ context.SyntaxProvider
+ .CreateSyntaxProvider(
+ static (node, _) => node is TypeDeclarationSyntax typeDeclaration && typeDeclaration.HasOrPotentiallyHasBaseTypes(),
+ static (context, token) =>
+ {
+ if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
+ {
+ return default;
+ }
+
+ INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node, token)!;
+
+ if (!context.Node.IsFirstSyntaxDeclarationForSymbol(typeSymbol))
+ {
+ return default;
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ if (!Execute.IsObservableValidator(typeSymbol))
+ {
+ return default;
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ return Execute.GetInfo(typeSymbol, token);
+ })
+ .Where(static item => item is not null)!;
+
+ IncrementalValuesProvider<(ValidationTypeInfo Left, PropertyValidationInfo Right)> explicitValidationMembers =
+ explicitValidationInfo.SelectMany(static (item, _) =>
+ item.Properties.Select(property => (new ValidationTypeInfo(item.Hierarchy, item.TypeName), property)));
+
+ IncrementalValuesProvider<(ValidationTypeInfo Left, PropertyValidationInfo Right)> generatedPropertyValidationMembers =
+ context.ForAttributeWithMetadataNameAndOptions(
+ "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute",
+ ObservablePropertyGenerator.Execute.IsCandidatePropertyDeclaration,
+ static (context, token) =>
+ {
+ MemberDeclarationSyntax memberSyntax = ObservablePropertyGenerator.Execute.GetCandidateMemberDeclaration(context.TargetNode);
+
+ if (!ObservablePropertyGenerator.Execute.IsCandidateValidForCompilation(memberSyntax, context.SemanticModel) ||
+ !ObservablePropertyGenerator.Execute.IsCandidateSymbolValid(context.TargetSymbol) ||
+ context.TargetSymbol is not IFieldSymbol fieldSymbol)
+ {
+ return default;
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ return Execute.GetGeneratedObservablePropertyValidationInfo(memberSyntax, fieldSymbol, context.SemanticModel, context.GlobalOptions, token);
+ })
+ .Where(static item => item.Left is not null)!;
+
+ IncrementalValuesProvider<(ValidationTypeInfo Left, PropertyValidationInfo Right)> validationMembers =
+ explicitValidationMembers
+ .Collect()
+ .Combine(generatedPropertyValidationMembers.Collect())
+ .SelectMany(static (item, _) => item.Left.Concat(item.Right));
+
+ IncrementalValuesProvider validationInfo =
+ validationMembers
+ .GroupBy(static item => item.Left, static item => item.Right)
+ .Select(static (item, _) => new ValidationInfo(item.Key.Hierarchy, item.Key.TypeName, item.Right));
+
+ IncrementalValueProvider isHeaderFileNeeded =
+ validationInfo
+ .Collect()
+ .Select(static (item, _) => item.Length > 0);
+
+ IncrementalValueProvider isDynamicallyAccessedMembersAttributeAvailable =
+ context.CompilationProvider
+ .Select(static (item, _) => item.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute"));
+
+ IncrementalValueProvider<(bool Condition, bool State)> headerFileInfo =
+ isHeaderFileNeeded
+ .Combine(isDynamicallyAccessedMembersAttributeAvailable)
+ .Select(static (item, _) => (item.Left, item.Right));
+
+ context.RegisterConditionalImplementationSourceOutput(headerFileInfo, static (context, item) =>
+ {
+ CompilationUnitSyntax compilationUnit = Execute.GetHeaderSyntax(item);
+
+ context.AddSource("__ObservableValidatorExtensions.g.cs", compilationUnit);
+ });
+
+ context.RegisterSourceOutput(validationInfo, static (context, item) =>
+ {
+ CompilationUnitSyntax compilationUnit = Execute.GetSyntax(item);
+
+ context.AddSource($"{item.Hierarchy.FilenameHint}.ObservableValidator.g.cs", compilationUnit);
+ });
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ObservableValidatorValidationGeneratorPartialTypeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ObservableValidatorValidationGeneratorPartialTypeAnalyzer.cs
new file mode 100644
index 000000000..3363bb75e
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ObservableValidatorValidationGeneratorPartialTypeAnalyzer.cs
@@ -0,0 +1,99 @@
+// 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.Collections.Concurrent;
+using System.Collections.Immutable;
+using System.Linq;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Text;
+using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+/// A diagnostic analyzer that reports types requiring generated ObservableValidator hooks that are not partial.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ObservableValidatorValidationGeneratorPartialTypeAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(ObservableValidatorTypeMustBePartial);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static context =>
+ {
+ ConcurrentDictionary<(SyntaxTree SyntaxTree, TextSpan Span), byte> reportedLocations = new();
+
+ context.RegisterSymbolAction(context =>
+ {
+ if (context.Symbol is not INamedTypeSymbol typeSymbol ||
+ typeSymbol.TypeKind != TypeKind.Class)
+ {
+ return;
+ }
+
+ if (!typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator") ||
+ !HasLocalValidatableProperties(typeSymbol))
+ {
+ return;
+ }
+
+ foreach (var typeDeclaration in typeSymbol.GetNonPartialTypeDeclarationNodes())
+ {
+ if (reportedLocations.TryAdd((typeDeclaration.SyntaxTree, typeDeclaration.Span), 0))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ObservableValidatorTypeMustBePartial,
+ typeDeclaration.Identifier.GetLocation(),
+ typeDeclaration.Identifier.ValueText));
+ }
+ }
+ }, SymbolKind.NamedType);
+ });
+ }
+
+ ///
+ /// Checks whether a target type has any locally declared validatable members.
+ ///
+ /// The target instance to inspect.
+ /// Whether has locally declared validatable members.
+ private static bool HasLocalValidatableProperties(INamedTypeSymbol typeSymbol)
+ {
+ foreach (ISymbol memberSymbol in typeSymbol.GetMembers())
+ {
+ if (memberSymbol is IPropertySymbol propertySymbol)
+ {
+ if (propertySymbol.IsStatic ||
+ propertySymbol.IsIndexer ||
+ !propertySymbol.CanBeReferencedByName)
+ {
+ continue;
+ }
+
+ if (propertySymbol.GetAttributes().Any(static a => a.AttributeClass?.InheritsFromFullyQualifiedMetadataName(
+ "System.ComponentModel.DataAnnotations.ValidationAttribute") == true))
+ {
+ return true;
+ }
+ }
+ else if (memberSymbol is IFieldSymbol fieldSymbol &&
+ fieldSymbol.GetAttributes().Any(static a => a.AttributeClass?.HasFullyQualifiedMetadataName(
+ "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") == true) &&
+ fieldSymbol.GetAttributes().Any(static a => a.AttributeClass?.InheritsFromFullyQualifiedMetadataName(
+ "System.ComponentModel.DataAnnotations.ValidationAttribute") == true))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
index 7c297892d..e0a7d07e1 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
@@ -49,6 +49,11 @@ internal static class DiagnosticDescriptors
///
public const string UseObservablePropertyOnSemiAutoPropertyId = "MVVMTK0056";
+ ///
+ /// The diagnostic id for .
+ ///
+ public const string ObservableValidatorTypeMustBePartialId = "MVVMTK0057";
+
///
/// Gets a indicating when a duplicate declaration of would happen.
///
@@ -944,4 +949,20 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "Semi-auto properties should be converted to partial properties using [ObservableProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0056");
+
+ ///
+ /// Gets a indicating when a type requiring generated validation hooks is not partial.
+ ///
+ /// Format: "The type {0} must be partial to enable generated validation support for ObservableValidator" .
+ ///
+ ///
+ public static readonly DiagnosticDescriptor ObservableValidatorTypeMustBePartial = new DiagnosticDescriptor(
+ id: ObservableValidatorTypeMustBePartialId,
+ title: "ObservableValidator type must be partial",
+ messageFormat: "The type {0} must be partial to enable generated validation support for ObservableValidator",
+ category: typeof(ObservableValidatorValidationGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Types deriving from ObservableValidator and declaring validatable properties must be partial, and all containing types must be partial as well, so the source generators can emit validation hooks.",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0057");
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs
index b79f743b3..4fee3743b 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs
@@ -3,9 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Immutable;
using System.Linq;
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
@@ -174,6 +177,40 @@ public static bool HasOrInheritsAttributeWithType(this ITypeSymbol typeSymbol, I
return false;
}
+ ///
+ /// Gets all non-partial declarations for a target type and its containing types.
+ ///
+ /// The target instance.
+ /// The sequence of all non-partial declarations for and its containing types.
+ public static ImmutableArray GetNonPartialTypeDeclarationNodes(this INamedTypeSymbol typeSymbol)
+ {
+ using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent();
+
+ for (INamedTypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.ContainingType)
+ {
+ foreach (SyntaxReference syntaxReference in currentType.DeclaringSyntaxReferences)
+ {
+ if (syntaxReference.GetSyntax() is TypeDeclarationSyntax typeDeclaration &&
+ !typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
+ {
+ builder.Add(typeDeclaration);
+ }
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
+ ///
+ /// Checks whether a target type and all its containing types are partial across all declarations.
+ ///
+ /// The target instance.
+ /// Whether and all its containing types are partial.
+ public static bool IsTypeHierarchyPartial(this INamedTypeSymbol typeSymbol)
+ {
+ return GetNonPartialTypeDeclarationNodes(typeSymbol).IsEmpty;
+ }
+
///
/// Checks whether or not a given inherits a specified attribute.
/// If the type has no base type, this method will automatically handle that and return .
diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs
index 6c83c9125..cc071bcaf 100644
--- a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs
+++ b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs
@@ -9,44 +9,39 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
using System.Runtime.CompilerServices;
namespace CommunityToolkit.Mvvm.ComponentModel;
///
-/// A base class for objects implementing the interface. This class
-/// also inherits from , so it can be used for observable items too.
+/// A base class for observable objects implementing the interface.
///
+///
+/// This type stores validation state and exposes helper APIs to validate individual properties or all
+/// properties in an instance. The actual validation logic can be provided either by derived types or by
+/// source-generated code overriding the available validation hooks.
+///
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
{
///
- /// The instance used to track compiled delegates to validate entities.
+ /// The cached for .
///
- private static readonly ConditionalWeakTable> EntityValidatorMap = new();
+ private static readonly PropertyChangedEventArgs HasErrorsChangedEventArgs = new(nameof(HasErrors));
///
- /// The instance used to track display names for properties to validate.
+ /// The optional instance to use when creating a .
///
- ///
- /// This is necessary because we want to reuse the same instance for all validations, but
- /// with the same behavior with respect to formatted names that new instances would have provided. The issue is that the
- /// property is not refreshed when we set ,
- /// so we need to replicate the same logic to retrieve the right display name for properties to validate and update that
- /// property manually right before passing the context to and proceed with the normal functionality.
- ///
- private static readonly ConditionalWeakTable> DisplayNamesMap = new();
+ private readonly IServiceProvider? validationServiceProvider;
///
- /// The cached for .
+ /// The optional copied items to use when creating a .
///
- private static readonly PropertyChangedEventArgs HasErrorsChangedEventArgs = new(nameof(HasErrors));
+ private readonly Dictionary? validationItems;
///
/// The instance currently in use.
///
- private readonly ValidationContext validationContext;
+ private ValidationContext? validationContext;
///
/// The instance used to store previous validation results.
@@ -65,53 +60,54 @@ public abstract class ObservableValidator : ObservableObject, INotifyDataErrorIn
///
/// Initializes a new instance of the class.
- /// This constructor will create a new that will
- /// be used to validate all properties, which will reference the current instance
- /// and no additional services or validation properties and settings.
+ /// This constructor will lazily create a new when validation runs.
+ /// The created context will reference the current instance and no additional services or validation items.
///
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected ObservableValidator()
{
- this.validationContext = new ValidationContext(this);
}
///
/// Initializes a new instance of the class.
- /// This constructor will create a new that will
- /// be used to validate all properties, which will reference the current instance.
+ /// This constructor will lazily create a new when validation runs.
+ /// The created context will reference the current instance and expose the specified validation items.
///
/// A set of key/value pairs to make available to consumers.
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected ObservableValidator(IDictionary? items)
{
- this.validationContext = new ValidationContext(this, items);
+ if (items is not null)
+ {
+ this.validationItems = new Dictionary(items);
+ }
}
///
/// Initializes a new instance of the class.
- /// This constructor will create a new that will
- /// be used to validate all properties, which will reference the current instance.
+ /// This constructor will lazily create a new when validation runs.
+ /// The created context will reference the current instance and expose the specified services and validation items.
///
/// An instance to make available during validation.
/// A set of key/value pairs to make available to consumers.
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected ObservableValidator(IServiceProvider? serviceProvider, IDictionary? items)
{
- this.validationContext = new ValidationContext(this, serviceProvider, items);
+ this.validationServiceProvider = serviceProvider;
+
+ if (items is not null)
+ {
+ this.validationItems = new Dictionary(items);
+ }
}
///
/// Initializes a new instance of the class.
- /// This constructor will store the input instance,
- /// and it will use it to validate all properties for the current viewmodel.
+ /// This constructor stores the input instance for later reuse.
///
///
/// The instance to use to validate properties.
///
- /// This instance will be passed to all
- /// calls executed by the current viewmodel, and its property will be updated every time
- /// before the call is made to set the name of the property being validated. The property name will not be reset after that, so the
- /// value of will always indicate the name of the last property that was validated, if any.
+ /// This instance will be reused by the validation helpers in this type. Its
+ /// and properties will be updated before validating a property, and they will
+ /// keep the values from the last validation operation.
///
///
/// Thrown if is .
@@ -145,7 +141,6 @@ protected ObservableValidator(ValidationContext validationContext)
/// are not raised if the current and new value for the target property are the same.
///
/// Thrown if is .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool SetProperty([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, bool validate, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(propertyName);
@@ -174,7 +169,6 @@ protected bool SetProperty([NotNullIfNotNull(nameof(newValue))] ref T field,
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
/// Thrown if or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool SetProperty([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, IEqualityComparer comparer, bool validate, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(comparer);
@@ -211,7 +205,6 @@ protected bool SetProperty([NotNullIfNotNull(nameof(newValue))] ref T field,
/// are not raised if the current and new value for the target property are the same.
///
/// Thrown if or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool SetProperty(T oldValue, T newValue, Action callback, bool validate, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(callback);
@@ -242,7 +235,6 @@ protected bool SetProperty(T oldValue, T newValue, Action callback, bool v
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
/// Thrown if , or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, bool validate, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(comparer);
@@ -277,7 +269,6 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer compa
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
/// Thrown if , or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, bool validate, [CallerMemberName] string propertyName = null!)
where TModel : class
{
@@ -315,7 +306,6 @@ protected bool SetProperty(T oldValue, T newValue, TModel model, Acti
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
/// Thrown if , , or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, bool validate, [CallerMemberName] string propertyName = null!)
where TModel : class
{
@@ -345,7 +335,6 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer<
/// (optional) The name of the property that changed.
/// Whether the validation was successful and the property value changed as well.
/// Thrown if is .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool TrySetProperty(ref T field, T newValue, out IReadOnlyCollection errors, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(propertyName);
@@ -366,7 +355,6 @@ protected bool TrySetProperty(ref T field, T newValue, out IReadOnlyCollectio
/// (optional) The name of the property that changed.
/// Whether the validation was successful and the property value changed as well.
/// Thrown if or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool TrySetProperty(ref T field, T newValue, IEqualityComparer comparer, out IReadOnlyCollection errors, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(comparer);
@@ -388,7 +376,6 @@ protected bool TrySetProperty(ref T field, T newValue, IEqualityComparer c
/// (optional) The name of the property that changed.
/// Whether the validation was successful and the property value changed as well.
/// Thrown if or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool TrySetProperty(T oldValue, T newValue, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(callback);
@@ -411,7 +398,6 @@ protected bool TrySetProperty(T oldValue, T newValue, Action callback, out
/// (optional) The name of the property that changed.
/// Whether the validation was successful and the property value changed as well.
/// Thrown if , or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool TrySetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(comparer);
@@ -436,7 +422,6 @@ protected bool TrySetProperty(T oldValue, T newValue, IEqualityComparer co
/// (optional) The name of the property that changed.
/// Whether the validation was successful and the property value changed as well.
/// Thrown if , or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool TrySetProperty(T oldValue, T newValue, TModel model, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string propertyName = null!)
where TModel : class
{
@@ -463,7 +448,6 @@ protected bool TrySetProperty(T oldValue, T newValue, TModel model, A
/// (optional) The name of the property that changed.
/// Whether the validation was successful and the property value changed as well.
/// Thrown if , , or are .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
protected bool TrySetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string propertyName = null!)
where TModel : class
{
@@ -530,104 +514,83 @@ IEnumerable GetAllErrors()
IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) => GetErrors(propertyName);
///
- /// Validates all the properties in the current instance and updates all the tracked errors.
- /// If any changes are detected, the event will be raised.
+ /// Validates all properties in the current instance and updates the tracked errors.
///
///
- /// Only public instance properties (excluding custom indexers) that have at least one
- /// applied to them will be validated. All other
- /// members in the current instance will be ignored. None of the processed properties
- /// will be modified - they will only be used to retrieve their values and validate them.
+ /// This method delegates to .
///
- [RequiresUnreferencedCode(
- "This method requires the generated CommunityToolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions type not to be removed to use the fast path. " +
- "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " +
- "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type. " +
- "Additionally, due to the usage of validation APIs, the type of the current instance cannot be statically discovered.")]
protected void ValidateAllProperties()
{
- // Fast path that tries to create a delegate from a generated type-specific method. This
- // is used to make this method more AOT-friendly and faster, as there is no dynamic code.
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- static Action GetValidationAction(Type type)
- {
- if (type.Assembly.GetType("CommunityToolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType &&
- extensionsType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo methodInfo)
- {
- return (Action)methodInfo.Invoke(null, new object?[] { null })!;
- }
-
- return GetValidationActionFallback(type);
- }
+ ValidateAllPropertiesCore();
+ }
- // Fallback method to create the delegate with a compiled LINQ expression
- static Action GetValidationActionFallback(Type type)
- {
- // Get the collection of all properties to validate
- (string Name, MethodInfo GetMethod)[] validatableProperties = (
- from property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
- where property.GetIndexParameters().Length == 0 &&
- property.GetCustomAttributes(true).Any()
- let getMethod = property.GetMethod
- where getMethod is not null
- select (property.Name, getMethod)).ToArray();
-
- // Short path if there are no properties to validate
- if (validatableProperties.Length == 0)
- {
- return static _ => { };
- }
+ ///
+ /// Validates all properties in the current instance.
+ ///
+ ///
+ /// The default implementation does nothing.
+ /// Derived types can override this method to validate all relevant properties and update the tracked errors.
+ ///
+ protected virtual void ValidateAllPropertiesCore() {}
- // MyViewModel inst0 = (MyViewModel)arg0;
- ParameterExpression arg0 = Expression.Parameter(typeof(object));
- UnaryExpression inst0 = Expression.Convert(arg0, type);
-
- // Get a reference to ValidateProperty(object, string)
- MethodInfo validateMethod = typeof(ObservableValidator).GetMethod(nameof(ValidateProperty), BindingFlags.Instance | BindingFlags.NonPublic)!;
-
- // We want a single compiled LINQ expression that validates all properties in the
- // actual type of the executing viewmodel at once. We do this by creating a block
- // expression with the unrolled invocations of all properties to validate.
- // Essentially, the body will contain the following code:
- // ===============================================================================
- // {
- // inst0.ValidateProperty(inst0.Property0, nameof(MyViewModel.Property0));
- // inst0.ValidateProperty(inst0.Property1, nameof(MyViewModel.Property1));
- // ...
- // inst0.ValidateProperty(inst0.PropertyN, nameof(MyViewModel.PropertyN));
- // }
- // ===============================================================================
- // We also add an explicit object conversion to represent boxing, if a given property
- // is a value type. It will just be a no-op if the value is a reference type already.
- // Note that this generated code is technically accessing a protected method from
- // ObservableValidator externally, but that is fine because IL doesn't really have
- // a concept of member visibility, that's purely a C# build-time feature.
- BlockExpression body = Expression.Block(
- from property in validatableProperties
- select Expression.Call(inst0, validateMethod, new Expression[]
- {
- Expression.Convert(Expression.Call(inst0, property.GetMethod), typeof(object)),
- Expression.Constant(property.Name)
- }));
+ ///
+ /// Tries to validate a property with the specified name and value, adding any errors to the target collection.
+ ///
+ /// The value to validate.
+ /// The name of the property to validate.
+ /// The target collection for validation errors.
+ ///
+ /// A value indicating whether validation succeeded, failed, or was not handled.
+ ///
+ ///
+ /// The default implementation returns .
+ /// Derived types can override this method to provide property-specific validation logic.
+ ///
+ protected virtual ValidationStatus TryValidatePropertyCore(object? value, string propertyName, ICollection errors)
+ {
+ return ValidationStatus.Unhandled;
+ }
- return Expression.Lambda>(body, arg0).Compile();
+ ///
+ /// Validates a property value through .
+ ///
+ /// The value to validate.
+ /// The name of the property to validate.
+ /// The display name to use for validation messages.
+ /// The explicit validation attributes to use.
+ /// The target collection for validation errors.
+ /// if the property is valid, otherwise .
+ ///
+ /// This helper reuses a shared instance, updating its
+ /// and properties for the target property.
+ ///
+ protected ValidationStatus TryValidateValue(object? value, string propertyName, string displayName, IEnumerable validationAttributes, ICollection errors)
+ {
+ ArgumentNullException.ThrowIfNull(propertyName);
+ ArgumentNullException.ThrowIfNull(displayName);
+ ArgumentNullException.ThrowIfNull(validationAttributes);
+ ArgumentNullException.ThrowIfNull(errors);
+ if (displayName.Length == 0)
+ {
+ throw new ArgumentException("The display name cannot be empty.", nameof(displayName));
}
- // Get or compute the cached list of properties to validate. Here we're using a static lambda to ensure the
- // delegate is cached by the C# compiler, see the related issue at https://github.com/dotnet/roslyn/issues/5835.
- EntityValidatorMap.GetValue(
- GetType(),
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] static (t) => GetValidationAction(t))(this);
+ ValidationContext updatedContext = GetOrCreateUpdatedValidationContext(propertyName, displayName);
+
+ return Validator.TryValidateValue(value!, updatedContext, errors, validationAttributes)
+ ? ValidationStatus.Success
+ : ValidationStatus.Error;
}
///
- /// Validates a property with a specified name and a given input value.
- /// If any changes are detected, the event will be raised.
+ /// Validates a property with a specified name and value and updates the tracked errors for that property.
///
/// The value to test for the specified property.
/// The name of the property to validate.
/// Thrown when is .
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
+ ///
+ /// Thrown when the validation request is not handled by .
+ ///
protected internal void ValidateProperty(object? value, [CallerMemberName] string propertyName = null!)
{
ArgumentNullException.ThrowIfNull(propertyName);
@@ -653,11 +616,7 @@ protected internal void ValidateProperty(object? value, [CallerMemberName] strin
errorsChanged = true;
}
- // Validate the property, by adding new errors to the existing list
- this.validationContext.MemberName = propertyName;
- this.validationContext.DisplayName = GetDisplayNameForProperty(propertyName);
-
- bool isValid = Validator.TryValidateProperty(value, this.validationContext, propertyErrors);
+ ValidationStatus isValid = TryValidatePropertyCore(value, propertyName, propertyErrors);
// Update the shared counter for the number of errors, and raise the
// property changed event if necessary. We decrement the number of total
@@ -665,7 +624,7 @@ protected internal void ValidateProperty(object? value, [CallerMemberName] strin
// validation, and we increment it if the validation failed after being
// correct before. The property changed event is raised whenever the
// number of total errors is either decremented to 0, or incremented to 1.
- if (isValid)
+ if (isValid is ValidationStatus.Success)
{
if (errorsChanged)
{
@@ -677,6 +636,10 @@ protected internal void ValidateProperty(object? value, [CallerMemberName] strin
}
}
}
+ else if (isValid is ValidationStatus.Unhandled)
+ {
+ throw new InvalidOperationException($"The requested property {propertyName} was not handled");
+ }
else if (!errorsChanged)
{
this.totalErrors++;
@@ -690,44 +653,37 @@ protected internal void ValidateProperty(object? value, [CallerMemberName] strin
// Only raise the event once if needed. This happens either when the target property
// had existing errors and is now valid, or if the validation has failed and there are
// new errors to broadcast, regardless of the previous validation state for the property.
- if (errorsChanged || !isValid)
+ if (errorsChanged || isValid is not ValidationStatus.Success)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
}
///
- /// Tries to validate a property with a specified name and a given input value, and returns
- /// the computed errors, if any. If the property is valid, it is assumed that its value is
- /// about to be set in the current object. Otherwise, no observable local state is modified.
+ /// Tries to validate a property with a specified name and value and returns the computed errors, if any.
///
/// The value to test for the specified property.
/// The name of the property to validate.
/// The resulting validation errors, if any.
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
private bool TryValidateProperty(object? value, string propertyName, out IReadOnlyCollection errors)
{
// Add the cached errors list for later use.
- if (!this.errors.TryGetValue(propertyName!, out List? propertyErrors))
+ if (!this.errors.TryGetValue(propertyName, out List? propertyErrors))
{
propertyErrors = new List();
- this.errors.Add(propertyName!, propertyErrors);
+ this.errors.Add(propertyName, propertyErrors);
}
bool hasErrors = propertyErrors.Count > 0;
List localErrors = new();
- // Validate the property, by adding new errors to the local list
- this.validationContext.MemberName = propertyName;
- this.validationContext.DisplayName = GetDisplayNameForProperty(propertyName!);
-
- bool isValid = Validator.TryValidateProperty(value, this.validationContext, localErrors);
+ ValidationStatus isValid = TryValidatePropertyCore(value, propertyName, localErrors);
// We only modify the state if the property is valid and it wasn't so before. In this case, we
// clear the cached list of errors (which is visible to consumers) and raise the necessary events.
- if (isValid && hasErrors)
+ if ((isValid is ValidationStatus.Success) && hasErrors)
{
propertyErrors.Clear();
@@ -743,7 +699,7 @@ private bool TryValidateProperty(object? value, string propertyName, out IReadOn
errors = localErrors;
- return isValid;
+ return isValid is ValidationStatus.Success;
}
///
@@ -781,7 +737,7 @@ private void ClearAllErrors()
/// The name of the property to clear errors for.
private void ClearErrorsForProperty(string propertyName)
{
- if (!this.errors.TryGetValue(propertyName!, out List? propertyErrors) ||
+ if (!this.errors.TryGetValue(propertyName, out List? propertyErrors) ||
propertyErrors.Count == 0)
{
return;
@@ -800,33 +756,41 @@ private void ClearErrorsForProperty(string propertyName)
}
///
- /// Gets the display name for a given property. It could be a custom name or just the property name.
+ /// Lazily creates or updates the shared validation context for a target property.
///
/// The target property name being validated.
- /// The display name for the property.
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- private string GetDisplayNameForProperty(string propertyName)
+ /// The display name to expose for the target property.
+ /// The shared instance to use.
+ private ValidationContext GetOrCreateUpdatedValidationContext(string propertyName, string displayName)
{
- static Dictionary GetDisplayNames(Type type)
- {
- Dictionary displayNames = new();
-
- foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
- {
- if (property.GetCustomAttribute() is DisplayAttribute attribute &&
- attribute.GetName() is string displayName)
- {
- displayNames.Add(property.Name, displayName);
- }
- }
+#pragma warning disable IL2026 // The created ValidationContext object is used in a way that never calls reflection.
+ ValidationContext context = this.validationContext ??= new ValidationContext(this, this.validationServiceProvider, this.validationItems);
+#pragma warning restore IL2026
- return displayNames;
- }
+ context.MemberName = propertyName;
+ context.DisplayName = displayName;
- // This method replicates the logic of DisplayName and GetDisplayName from the
- // ValidationContext class. See the original source in the BCL for more details.
- _ = DisplayNamesMap.GetValue(GetType(), static t => GetDisplayNames(t)).TryGetValue(propertyName, out string? displayName);
+ return context;
+ }
- return displayName ?? propertyName;
+ ///
+ /// Indicates the outcome of a property validation request.
+ ///
+ protected enum ValidationStatus
+ {
+ ///
+ /// Validation succeeded.
+ ///
+ Success,
+
+ ///
+ /// The requested property was not handled.
+ ///
+ Unhandled,
+
+ ///
+ /// Validation failed.
+ ///
+ Error
}
}
diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs b/src/CommunityToolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs
index cddca2c73..a4a598e4d 100644
--- a/src/CommunityToolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs
+++ b/src/CommunityToolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs
@@ -4,7 +4,6 @@
using System;
using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
namespace CommunityToolkit.Mvvm.ComponentModel.__Internals;
@@ -24,10 +23,6 @@ public static class __ObservableValidatorHelper
/// The name of the property to validate.
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is not intended to be called directly by user code")]
- [UnconditionalSuppressMessage(
- "ReflectionAnalysis",
- "IL2026:RequiresUnreferencedCode",
- Justification = "This helper is called by generated code from public APIs that have the proper annotations already (and we don't want generated code to produce warnings that developers cannot fix).")]
public static void ValidateProperty(ObservableValidator instance, object? value, string propertyName)
{
instance.ValidateProperty(value, propertyName);
diff --git a/tests/CommunityToolkit.Mvvm.Aot.UnitTests/CommunityToolkit.Mvvm.Aot.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Aot.UnitTests/CommunityToolkit.Mvvm.Aot.UnitTests.csproj
new file mode 100644
index 000000000..27b3b8be3
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.Aot.UnitTests/CommunityToolkit.Mvvm.Aot.UnitTests.csproj
@@ -0,0 +1,41 @@
+
+
+
+ net10.0
+ 14.0
+ true
+ $(DefineConstants);ROSLYN_4_12_0_OR_GREATER;ROSLYN_5_0_0_OR_GREATER
+
+
+ $(NoWarn);MVVMTK0042
+
+ true
+ true
+ exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Mvvm.Aot.UnitTests/TestAot.ps1 b/tests/CommunityToolkit.Mvvm.Aot.UnitTests/TestAot.ps1
new file mode 100644
index 000000000..6b234e615
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.Aot.UnitTests/TestAot.ps1
@@ -0,0 +1,2 @@
+dotnet publish ./CommunityToolkit.Mvvm.Aot.UnitTests.csproj -r win-x64 -c Release -f net10.0
+./bin/Release/net10.0/win-x64/publish/CommunityToolkit.Mvvm.Aot.UnitTests.exe
diff --git a/tests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests.csproj
index 07a20826d..94e0272e6 100644
--- a/tests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests.csproj
+++ b/tests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests/CommunityToolkit.Mvvm.Roslyn5000.UnitTests.csproj
@@ -1,7 +1,7 @@
- net472;net8.0;net9.0
+ net472;net8.0;net9.0;net10.0
14.0
true
$(DefineConstants);ROSLYN_4_12_0_OR_GREATER;ROSLYN_5_0_0_OR_GREATER
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_MakeObservableValidatorTypePartialCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_MakeObservableValidatorTypePartialCodeFixer.cs
new file mode 100644
index 000000000..f3573658b
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_MakeObservableValidatorTypePartialCodeFixer.cs
@@ -0,0 +1,64 @@
+// 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.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using CSharpCodeFixTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixTest<
+ CommunityToolkit.Mvvm.SourceGenerators.ObservableValidatorValidationGeneratorPartialTypeAnalyzer,
+ CommunityToolkit.Mvvm.CodeFixers.MakeObservableValidatorTypePartialCodeFixer,
+ Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
+using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<
+ CommunityToolkit.Mvvm.SourceGenerators.ObservableValidatorValidationGeneratorPartialTypeAnalyzer,
+ CommunityToolkit.Mvvm.CodeFixers.MakeObservableValidatorTypePartialCodeFixer,
+ Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests;
+
+[TestClass]
+public class MakeObservableValidatorTypePartialCodeFixer
+{
+ [TestMethod]
+ public async Task TopLevelType()
+ {
+ string original = """
+ using System.ComponentModel.DataAnnotations;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ class SampleViewModel : ObservableValidator
+ {
+ [Required]
+ public string Name { get; set; }
+ }
+ """;
+
+ string @fixed = """
+ using System.ComponentModel.DataAnnotations;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ partial class SampleViewModel : ObservableValidator
+ {
+ [Required]
+ public string Name { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new()
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.Add(CSharpCodeFixVerifier.Diagnostic("MVVMTK0057").WithSpan(6, 7, 6, 22).WithArguments("SampleViewModel"));
+
+ await test.RunAsync();
+ }
+}
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs
index 314f49e8c..a32224693 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs
@@ -1107,7 +1107,6 @@ partial class MyViewModel : ObservableValidator
}
""";
-#if NET6_0_OR_GREATER
string result = """
//
#pragma warning disable
@@ -1123,7 +1122,6 @@ partial class MyViewModel
public partial string Name
{
get => field;
- [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
set
{
if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value))
@@ -1165,64 +1163,6 @@ public partial string Name
}
}
""";
-#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 partial string Name
- {
- get => field;
- set
- {
- if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value))
- {
- OnNameChanging(value);
- OnNameChanging(default, value);
- OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name);
- field = value;
- ValidateProperty(value, "Name");
- 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() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result));
}
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
index d91dfe2b2..6ef52be80 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs
@@ -2788,7 +2788,7 @@ internal static class __KnownINotifyPropertyChangedArgs
}
[TestMethod]
- public void ObservableProperty_NotifyDataErrorInfo_EmitsTrimAnnotationsWhenNeeded()
+ public void ObservableProperty_NotifyDataErrorInfo_WorksCorrectly()
{
string source = """
using System.ComponentModel.DataAnnotations;
@@ -2807,66 +2807,6 @@ partial class MyViewModel : ObservableValidator
}
""";
-#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]
- [global::System.ComponentModel.DataAnnotations.RequiredAttribute()]
- public string? Name
- {
- get => name;
- [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- 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;
- ValidateProperty(value, "Name");
- 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
@@ -2924,13 +2864,12 @@ public string? Name
}
}
""";
-#endif
VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result));
}
[TestMethod]
- public void ObservableProperty_NotifyDataErrorInfo_EmitsTrimAnnotationsWhenNeeded_AppendToNullableAttribute()
+ public void ObservableProperty_NotifyDataErrorInfo_AppendToNullableAttribute_WorksCorrectly()
{
string source = """
using System.ComponentModel.DataAnnotations;
@@ -2967,7 +2906,6 @@ public string Name
{
get => name;
[global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")]
- [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
set
{
if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value))
@@ -3448,6 +3386,144 @@ public object? A
VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result));
}
+ [TestMethod]
+ public void ObservableValidator_GeneratesValidationHooksForObservablePropertyFields()
+ {
+ string source = """
+ using System.ComponentModel.DataAnnotations;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ #nullable enable
+
+ namespace MyApp;
+
+ partial class MyViewModel : ObservableValidator
+ {
+ [ObservableProperty]
+ [Required]
+ private string? name;
+ }
+ """;
+
+ Type observableObjectType = typeof(ObservableObject);
+ Type validationAttributeType = typeof(ValidationAttribute);
+
+ 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.CSharp10));
+
+ CSharpCompilation compilation = CSharpCompilation.Create(
+ "original",
+ new SyntaxTree[] { syntaxTree },
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new IIncrementalGenerator[]
+ {
+ new ObservablePropertyGenerator(),
+ new ObservableValidatorValidationGenerator()
+ }).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);
+
+ _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics);
+
+ CollectionAssert.AreEquivalent(Array.Empty(), diagnostics);
+
+ SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == "MyApp.MyViewModel.ObservableValidator.g.cs");
+ string generatedText = generatedTree.ToString();
+
+ StringAssert.Contains(generatedText, "using System.Collections.Generic;");
+ StringAssert.Contains(generatedText, "using System.ComponentModel.DataAnnotations;");
+ StringAssert.Contains(generatedText, "using System.Reflection;");
+ StringAssert.Contains(generatedText, "private static readonly IEnumerable __NameValidationAttributes =");
+ StringAssert.Contains(generatedText, "typeof(global::MyApp.MyViewModel).GetProperty(nameof(Name))!.GetCustomAttributes();");
+ StringAssert.Contains(generatedText, "protected override void ValidateAllPropertiesCore()");
+ StringAssert.Contains(generatedText, "ValidateProperty(Name, nameof(Name));");
+ StringAssert.Contains(generatedText, "protected override ValidationStatus TryValidatePropertyCore(object? value, string propertyName, ICollection errors)");
+ StringAssert.Contains(generatedText, "nameof(Name) => TryValidateValue(value, nameof(Name), \"Name\", __NameValidationAttributes, errors),");
+ Assert.DoesNotContain(generatedText, "GetProperty(\"Name\")!");
+ Assert.DoesNotContain(generatedText, "PropertyInfo __");
+ Assert.DoesNotContain(generatedText, "__GetDisplayName");
+ Assert.DoesNotContain(generatedText, "__GetValidationAttributes");
+
+ GC.KeepAlive(observableObjectType);
+ GC.KeepAlive(validationAttributeType);
+ }
+
+ [TestMethod]
+ public void ObservableValidator_UsesDeclaredDisplayNamesInValidationHooks()
+ {
+ string source = """
+ using System.ComponentModel.DataAnnotations;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ #nullable enable
+
+ namespace MyApp;
+
+ partial class MyViewModel : ObservableValidator
+ {
+ private string? userName;
+
+ [Required]
+ [Display(Name = "User Name")]
+ public string? UserName
+ {
+ get => this.userName;
+ set => SetProperty(ref this.userName, value, validate: true);
+ }
+
+ [ObservableProperty]
+ [Required]
+ [Display(Name = "Email Address")]
+ private string? email;
+ }
+ """;
+
+ Type observableObjectType = typeof(ObservableObject);
+ Type validationAttributeType = typeof(ValidationAttribute);
+
+ 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.CSharp10));
+
+ CSharpCompilation compilation = CSharpCompilation.Create(
+ "original",
+ new SyntaxTree[] { syntaxTree },
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new IIncrementalGenerator[]
+ {
+ new ObservablePropertyGenerator(),
+ new ObservableValidatorValidationGenerator()
+ }).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);
+
+ _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics);
+
+ CollectionAssert.AreEquivalent(Array.Empty(), diagnostics);
+
+ SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == "MyApp.MyViewModel.ObservableValidator.g.cs");
+ string generatedText = generatedTree.ToString();
+
+ StringAssert.Contains(generatedText, "private static readonly DisplayAttribute __UserNameDisplayAttribute =");
+ StringAssert.Contains(generatedText, "typeof(global::MyApp.MyViewModel).GetProperty(nameof(UserName))!.GetCustomAttribute()!;");
+ StringAssert.Contains(generatedText, "private static readonly DisplayAttribute __EmailDisplayAttribute =");
+ StringAssert.Contains(generatedText, "typeof(global::MyApp.MyViewModel).GetProperty(nameof(Email))!.GetCustomAttribute();");
+ StringAssert.Contains(generatedText, "nameof(UserName) => TryValidateValue(value, nameof(UserName), __UserNameDisplayAttribute.GetName() ?? \"UserName\", __UserNameValidationAttributes, errors),");
+ StringAssert.Contains(generatedText, "nameof(Email) => TryValidateValue(value, nameof(Email), __EmailDisplayAttribute.GetName() ?? \"Email\", __EmailValidationAttributes, errors),");
+
+ GC.KeepAlive(observableObjectType);
+ GC.KeepAlive(validationAttributeType);
+ }
+
///
/// Generates the requested sources
///
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index 69da07125..1b75cc266 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -327,7 +327,7 @@ public partial class {|MVVMTK0008:SampleViewModel|}
}
[TestMethod]
- public async Task UnsupportedCSharpLanguageVersion_FromObservableValidatorValidateAllPropertiesGenerator()
+ public async Task UnsupportedCSharpLanguageVersion_FromObservableValidatorValidationGenerator()
{
string source = """
using System.ComponentModel.DataAnnotations;
@@ -346,6 +346,49 @@ public partial class SampleViewModel : ObservableValidator
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp7_3);
}
+ [TestMethod]
+ public async Task ObservableValidatorValidationGeneratorRequiresPartialType()
+ {
+ string source = """
+ using System.ComponentModel.DataAnnotations;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp
+ {
+ public class {|MVVMTK0057:SampleViewModel|} : ObservableValidator
+ {
+ [Required]
+ public string Name { get; set; }
+ }
+ }
+ """;
+
+ await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8);
+ }
+
+ [TestMethod]
+ public async Task ObservableValidatorValidationGeneratorRequiresContainingTypesToBePartial()
+ {
+ string source = """
+ using System.ComponentModel.DataAnnotations;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp
+ {
+ public class {|MVVMTK0057:Outer|}
+ {
+ public partial class Inner : ObservableValidator
+ {
+ [Required]
+ public string Name { get; set; }
+ }
+ }
+ }
+ """;
+
+ await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8);
+ }
+
[TestMethod]
public async Task UnsupportedCSharpLanguageVersion_FromRelayCommandGenerator()
{
@@ -2507,7 +2550,7 @@ internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration();
+ IMessenger weakMessenger = Activator.CreateInstance();
+
+ Assert.IsNotNull(strongMessenger);
+ Assert.IsNotNull(weakMessenger);
+ }
+
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs
index 559746e2e..c5cb29766 100644
--- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs
+++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs
@@ -17,7 +17,7 @@
namespace CommunityToolkit.Mvvm.UnitTests;
[TestClass]
-public class Test_ObservableValidator
+public sealed partial class Test_ObservableValidator
{
[TestMethod]
public void Test_ObservableValidator_HasErrors()
@@ -483,6 +483,31 @@ public void Test_ObservableValidator_ValidationWithFormattedDisplayName()
Assert.AreEqual($"SECOND: {nameof(ValidationWithDisplayName.AnotherRequiredField)}.", allErrors[1].ErrorMessage);
}
+ [TestMethod]
+ public void Test_ObservableValidator_ValidationWithLocalizedDisplayName()
+ {
+ ValidationWithLocalizedDisplayName? model = new();
+
+ Assert.IsTrue(model.HasErrors);
+
+ ValidationResult error = model.GetErrors(nameof(ValidationWithLocalizedDisplayName.UserName)).Cast().Single();
+
+ Assert.AreEqual(nameof(ValidationWithLocalizedDisplayName.UserName), error.MemberNames.Single());
+ Assert.AreEqual("LOCALIZED: Localized User Name.", error.ErrorMessage);
+ }
+
+ [TestMethod]
+ public void Test_EnsureConstructorsArePreserved()
+ {
+ // DynamicallyAccessedMembers on test methods do not seem to preserve constructors.
+ // Therefore, this method calls them
+ ObservableValidatorBase baseValidator = Activator.CreateInstance();
+ ObservableValidatorDerived derivedValidator = Activator.CreateInstance();
+
+ Assert.IsNotNull(baseValidator);
+ Assert.IsNotNull(derivedValidator);
+ }
+
// See: https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4272
[TestMethod]
[DataRow(typeof(ObservableValidatorBase))]
@@ -622,7 +647,7 @@ public void Test_ObservableValidator_HasErrors_IncludeNonAutogenerateAttribute()
Assert.IsFalse(displayAttribute.AutoGenerateField);
}
- public class Person : ObservableValidator
+ public partial class Person : ObservableValidator
{
private string? name;
@@ -655,7 +680,7 @@ public int Age
}
}
- public class PersonWithDeferredValidation : ObservableValidator
+ public partial class PersonWithDeferredValidation : ObservableValidator
{
[MinLength(4)]
[MaxLength(20)]
@@ -678,7 +703,7 @@ public class PersonWithDeferredValidation : ObservableValidator
/// Test model for linked properties, to test instance.
/// See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3665 for the original request for this feature.
///
- public class ComparableModel : ObservableValidator
+ public partial class ComparableModel : ObservableValidator
{
private int a;
@@ -731,7 +756,7 @@ protected override ValidationResult IsValid(object? value, ValidationContext val
/// Test model for custom validation properties.
/// See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3729 for the original request for this feature.
///
- public class CustomValidationModel : ObservableValidator
+ public partial class CustomValidationModel : ObservableValidator
{
public CustomValidationModel(IDictionary items)
: base(items)
@@ -777,7 +802,7 @@ public bool Validate(string name)
/// Test model for custom validation with an injected service.
/// See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3750 for the original request for this feature.
///
- public class ValidationWithServiceModel : ObservableValidator
+ public partial class ValidationWithServiceModel : ObservableValidator
{
private readonly IFancyService service;
@@ -813,7 +838,7 @@ public static ValidationResult ValidateName(string name, ValidationContext conte
/// Test model for validation with a formatted display name string on each property.
/// This is issue #1 from https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3763.
///
- public class ValidationWithDisplayName : ObservableValidator
+ public partial class ValidationWithDisplayName : ObservableValidator
{
public ValidationWithDisplayName()
{
@@ -839,6 +864,29 @@ public string? AnotherRequiredField
}
}
+ public partial class ValidationWithLocalizedDisplayName : ObservableValidator
+ {
+ public ValidationWithLocalizedDisplayName()
+ {
+ ValidateAllProperties();
+ }
+
+ private string? userName;
+
+ [Required(AllowEmptyStrings = false, ErrorMessage = "LOCALIZED: {0}.")]
+ [Display(Name = nameof(ValidationDisplayNameResources.LocalizedUserName), ResourceType = typeof(ValidationDisplayNameResources))]
+ public string? UserName
+ {
+ get => this.userName;
+ set => SetProperty(ref this.userName, value, true);
+ }
+ }
+
+ public static class ValidationDisplayNameResources
+ {
+ public static string LocalizedUserName => "Localized User Name";
+ }
+
public class ObservableValidatorBase : ObservableValidator
{
public int? MyDummyInt { get; set; } = 0;
@@ -874,14 +922,14 @@ public partial class PersonWithPartialDeclaration
public int Number { get; set; }
}
- public abstract class AbstractModelWithValidatableProperty : ObservableValidator
+ public abstract partial class AbstractModelWithValidatableProperty : ObservableValidator
{
[Required]
[MinLength(2)]
public string? Name { get; set; }
}
- public class DerivedModelWithValidatableProperties : AbstractModelWithValidatableProperty
+ public partial class DerivedModelWithValidatableProperties : AbstractModelWithValidatableProperty
{
[Range(10, 1000)]
public int Number { get; set; }
@@ -892,7 +940,7 @@ public class DerivedModelWithValidatableProperties : AbstractModelWithValidatabl
}
}
- public class GenericPerson : ObservableValidator
+ public partial class GenericPerson : ObservableValidator
{
[Required]
[MinLength(1)]