Darp.CommunityToolkit.MvvmValidator
0.1.1
dotnet add package Darp.CommunityToolkit.MvvmValidator --version 0.1.1
NuGet\Install-Package Darp.CommunityToolkit.MvvmValidator -Version 0.1.1
<PackageReference Include="Darp.CommunityToolkit.MvvmValidator" Version="0.1.1" />
<PackageVersion Include="Darp.CommunityToolkit.MvvmValidator" Version="0.1.1" />
<PackageReference Include="Darp.CommunityToolkit.MvvmValidator" />
paket add Darp.CommunityToolkit.MvvmValidator --version 0.1.1
#r "nuget: Darp.CommunityToolkit.MvvmValidator, 0.1.1"
#:package Darp.CommunityToolkit.MvvmValidator@0.1.1
#addin nuget:?package=Darp.CommunityToolkit.MvvmValidator&version=0.1.1
#tool nuget:?package=Darp.CommunityToolkit.MvvmValidator&version=0.1.1
Darp.CommunityToolkit.MvvmValidator
DataAnnotations validation for CommunityToolkit.Mvvm view models that works with trimming and Native AOT. A source generator writes the validation code, so nothing is discovered with reflection at runtime.
dotnet add package Darp.CommunityToolkit.MvvmValidator
Requirements:
- .NET 10
- View models and their containing types are
partial. A code fix adds missing modifiers. - Attributes that only target properties, like
[Compare], need[property: ...]on[ObservableProperty]fields.
Usage
Derive from ValidatingObservableObject instead of ObservableValidator and make the class partial:
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using Darp.MvvmValidator;
public partial class SignUpViewModel : ValidatingObservableObject
{
public SignUpViewModel() => ValidateAllProperties();
[ObservableProperty]
[Required]
[StringLength(20, MinimumLength = 3)]
[Display(Name = "User name")]
public partial string? UserName { get; set; }
[ObservableProperty]
[Required]
public partial string? Password { get; set; }
[ObservableProperty]
[Compare(nameof(Password))]
public partial string? ConfirmPassword { get; set; }
[CustomValidation(typeof(SignUpViewModel), nameof(ValidateInvite))]
public string? InviteCode
{
get;
set => SetProperty(ref field, value);
}
public static ValidationResult? ValidateInvite(string? code, ValidationContext context) =>
code is null or "WELCOME" ? ValidationResult.Success : new ValidationResult("Unknown invite code");
}
<details> <summary>Generated code</summary>
// <auto-generated/>
#nullable enable
partial class SignUpViewModel
{
private static readonly global::System.ComponentModel.DataAnnotations.RequiredAttribute __dmv_rule0_0 = new global::System.ComponentModel.DataAnnotations.RequiredAttribute();
private static readonly global::System.ComponentModel.DataAnnotations.StringLengthAttribute __dmv_rule0_1 = new global::System.ComponentModel.DataAnnotations.StringLengthAttribute(((int)(20))) { MinimumLength = ((int)(3)) };
private static readonly global::System.ComponentModel.DataAnnotations.DisplayAttribute __dmv_display0 = new global::System.ComponentModel.DataAnnotations.DisplayAttribute() { Name = "User name" };
private static readonly global::System.ComponentModel.DataAnnotations.RequiredAttribute __dmv_rule1_0 = new global::System.ComponentModel.DataAnnotations.RequiredAttribute();
/// <inheritdoc />
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override bool TryValidatePropertyCore(string propertyName, global::System.Collections.Generic.ICollection<global::System.ComponentModel.DataAnnotations.ValidationResult> errors)
{
switch (propertyName)
{
case "UserName":
__dmv_validate0(this.UserName, errors);
return true;
case "Password":
__dmv_validate1(this.Password, errors);
return true;
case "ConfirmPassword":
__dmv_validate2(this.ConfirmPassword, errors);
return true;
case "InviteCode":
__dmv_validate3(this.InviteCode, errors);
return true;
default:
return base.TryValidatePropertyCore(propertyName, errors);
}
}
/// <inheritdoc />
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void ValidateAllPropertiesCore()
{
base.ValidateAllPropertiesCore();
ValidateProperty("UserName");
ValidateProperty("Password");
ValidateProperty("ConfirmPassword");
ValidateProperty("InviteCode");
}
/// <inheritdoc />
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void ValidateDependentPropertiesCore(string propertyName)
{
switch (propertyName)
{
case "Password":
RevalidateIfValidated("ConfirmPassword");
break;
}
base.ValidateDependentPropertiesCore(propertyName);
}
private void __dmv_validate0(string? value, global::System.Collections.Generic.ICollection<global::System.ComponentModel.DataAnnotations.ValidationResult> errors)
{
global::System.ComponentModel.DataAnnotations.ValidationContext context = CreateValidationContext("UserName", __dmv_display0.GetName());
global::System.ComponentModel.DataAnnotations.ValidationResult? result;
result = __dmv_rule0_0.GetValidationResult(value, context);
if (result is not null)
{
errors.Add(result);
return;
}
result = __dmv_rule0_1.GetValidationResult(value, context);
if (result is not null)
{
errors.Add(result);
}
}
private void __dmv_validate1(string? value, global::System.Collections.Generic.ICollection<global::System.ComponentModel.DataAnnotations.ValidationResult> errors)
{
global::System.ComponentModel.DataAnnotations.ValidationContext context = CreateValidationContext("Password", null);
global::System.ComponentModel.DataAnnotations.ValidationResult? result;
result = __dmv_rule1_0.GetValidationResult(value, context);
if (result is not null)
{
errors.Add(result);
return;
}
}
private void __dmv_validate2(string? value, global::System.Collections.Generic.ICollection<global::System.ComponentModel.DataAnnotations.ValidationResult> errors)
{
global::System.ComponentModel.DataAnnotations.ValidationContext context = CreateValidationContext("ConfirmPassword", null);
global::System.ComponentModel.DataAnnotations.ValidationResult? result;
result = ValidateCompare(value, this.Password, "Password", null, context);
if (result is not null)
{
errors.Add(result);
}
}
private void __dmv_validate3(string? value, global::System.Collections.Generic.ICollection<global::System.ComponentModel.DataAnnotations.ValidationResult> errors)
{
global::System.ComponentModel.DataAnnotations.ValidationContext context = CreateValidationContext("InviteCode", null);
global::System.ComponentModel.DataAnnotations.ValidationResult? result;
result = CompleteCustomValidationResult(global::SignUpViewModel.ValidateInvite(value, context), null, context);
if (result is not null)
{
errors.Add(result);
}
}
}
</details>
Behavior
- Properties with validation attributes are validated whenever they change, before
PropertyChangedreaches subscribers.[NotifyDataErrorInfo]andSetProperty(..., validate: true)are not needed. - Results match
Validator.TryValidateProperty, including[CustomValidation],[Compare]and resource-based messages. - When a
[Compare]target changes, the comparing property is validated again, unless it is still untouched. ValidateAllPropertiesandClearErrorswork as inObservableValidator.SetErrorsadds results of asynchronous or server-side validation.
Diagnostics
The package turns off the toolkit's MVVMTK0006, which only accepts ObservableValidator, and reports these instead:
| Id | Reported when |
|---|---|
| DMV0001 | A validated type or a containing type is not partial, or is file-local. A code fix adds partial |
| DMV0002 | A [CustomValidation] method is missing, not public static, or has the wrong signature |
| DMV0003 | An [ObservableProperty] member has validation attributes, but its type is no validating view model |
| DMV0004 | A [Compare] target is not a readable, accessible property |
| DMV0005 | An attribute or property cannot be expressed in generated code, e.g. a ref struct or dynamic property |
[Compare], [MinLength], [MaxLength] and [Length] make the trim analyzer and the trimmer warn where they are
applied. The package suppresses these warnings when the generated code avoids the reflection they warn about. For
properties with hand-written accessors or a field initializer, the warnings stay, because suppressing them would also
hide warnings in that code.
Development
- Format with CSharpier:
dotnet tool restore, thendotnet csharpier format . - Runtime tests also run as a Native AOT executable in CI, which proves the generated code is trim safe.
- Generator tests snapshot the generated code with Verify.
- Releases come from Conventional Commits via
release-please. Every pull request publishes a preview package to
https://nuget.pkg.github.com/rosslight/index.json.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CommunityToolkit.Mvvm (>= 8.4.2)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Darp.CommunityToolkit.MvvmValidator:
| Package | Downloads |
|---|---|
|
Darp.Utils.Dialog
A very simple dialog service targeting avalonia |
|
|
Darp.Utils.Dialog.FluentAvalonia
Implementation of Darp.Utils.Dialog compatible with Avalonia |
GitHub repositories
This package is not used by any popular GitHub repositories.