Files
awesome-copilot/skills/mvvm-toolkit/references/validation.md
T
Alvin Ashcraft e7755069e9 WinUI plugin enhancements and add MVVM Toolkit skill (#1643)
* WinUI plugin enhancements and mvvm toolkit skill

* Split mvvm-toolkit skill for slimming
2026-05-11 11:29:33 +10:00

5.7 KiB

Validation with ObservableValidator

ObservableValidator extends ObservableObject with INotifyDataErrorInfo support, integrating with System.ComponentModel.DataAnnotations validation attributes.


Quick start

using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;

public sealed partial class RegistrationViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [MinLength(2), MaxLength(100)]
    private string? name;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required, EmailAddress]
    private string? email;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Range(13, 120)]
    private int age;

    [RelayCommand]
    private void Submit()
    {
        ValidateAllProperties();
        if (HasErrors) return;
        // submit...
    }
}

[NotifyDataErrorInfo] makes the generated setter call ValidateProperty(value) after each successful set, so validation runs as the user types.


Manual SetProperty validation

If you write the property by hand instead of using [ObservableProperty], opt into validation with the bool validate parameter:

[Required, MinLength(2), MaxLength(100)]
public string? Name
{
    get => name;
    set => SetProperty(ref name, value, validate: true);
}

TrySetProperty

Sometimes you want to set a property only if validation succeeds:

[Required, EmailAddress]
public string? Email
{
    get => email;
    set
    {
        if (TrySetProperty(ref email, value, out IReadOnlyCollection<ValidationResult> errors))
        {
            // value passed validation; success
        }
        else
        {
            // inspect errors
        }
    }
}

ValidateAllProperties()

Forces validation across every public property in the type that has at least one ValidationAttribute. Call before submission:

[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit()
{
    ValidateAllProperties();
    if (HasErrors) return;
    submitter.Submit(this);
}

private bool CanSubmit() => !HasErrors;

Pair with [NotifyCanExecuteChangedFor] on the input fields, plus a listener on ErrorsChanged (or override OnErrorsChanged) to keep the button state in sync as the user types.


ValidateProperty(value, propertyName)

Trigger validation manually for one property — useful when validation of property A depends on property B:

[Range(20, 80)]
[ObservableProperty]
private int b;

[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;

partial void OnBChanged(int value)
{
    // Re-run A's validation since it depends on B.
    ValidateProperty(A, nameof(A));
}

ClearAllErrors()

Reset the error state — common after a successful submit or when resetting a form:

[RelayCommand]
private void Reset()
{
    Name = null;
    Email = null;
    Age = 0;
    ClearAllErrors();
}

Custom validation method ([CustomValidation])

[Required, MinLength(3)]
[CustomValidation(typeof(RegistrationViewModel), nameof(ValidateUsername))]
[ObservableProperty]
private string? username;

public static ValidationResult ValidateUsername(string? value, ValidationContext context)
{
    var vm = (RegistrationViewModel)context.ObjectInstance;
    if (vm.userService.IsTaken(value!))
        return new ValidationResult("Username is already taken.");
    return ValidationResult.Success!;
}

The method must be static and accept (value, ValidationContext). Use context.ObjectInstance to reach back into the ViewModel.


Custom ValidationAttribute

For reusable rules, subclass ValidationAttribute:

public sealed class GreaterThanAttribute(string otherPropertyName)
    : ValidationAttribute
{
    public string OtherPropertyName { get; } = otherPropertyName;

    protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
    {
        var instance = ctx.ObjectInstance;
        var other = instance.GetType().GetProperty(OtherPropertyName)?.GetValue(instance);
        if (((IComparable)value!).CompareTo(other) > 0)
            return ValidationResult.Success;
        return new ValidationResult($"Must be greater than {OtherPropertyName}.");
    }
}

Apply to the property:

[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;

Reading errors in the View

ObservableValidator implements INotifyDataErrorInfo. XAML stacks render ErrorsChanged automatically when ValidatesOnNotifyDataErrors=True (WPF) or via control templates (WinUI 3, MAUI). To inspect errors in code:

foreach (ValidationResult result in vm.GetErrors(nameof(vm.Name)))
{
    Console.WriteLine(result.ErrorMessage);
}

// Across all properties
foreach (ValidationResult result in vm.GetErrors())
{
    Console.WriteLine(result.ErrorMessage);
}

bool any = vm.HasErrors;

Subscribe to changes:

vm.ErrorsChanged += (s, e) =>
{
    Debug.WriteLine($"Errors changed for {e.PropertyName}");
};

Tips

  • Combine ValidateAllProperties() with [NotifyCanExecuteChangedFor] so the Submit button reflects validity in real time.
  • Keep validation rules in the ViewModel (or via custom attributes), not in the model — the model should be a plain DTO.
  • For network or async validation (e.g., "is username taken?"), use [CustomValidation] calling a synchronous wrapper around an async lookup (or perform the async check separately and surface the result via AddError(propertyName, ...)-style helpers if you write your own).
  • ObservableValidator cannot also inherit from ObservableRecipient — if you need messaging, inject IMessenger and call Send directly.