Validation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Validation answers one question: is what the user typed acceptable? In WPF the answer is produced by the binding engine, which already sits between the control and the data. When the user changes a value, the binding converts it, runs the validators attached to it, writes it to the source if all is well, and otherwise records one or more ValidationError objects on the target element. The element then shows them through an error template, and the rest of the UI can react to Validation.HasError. This page covers the four validation mechanisms, the attached properties and error templates that present the result, and the bridge to the MVVM Toolkit.

The page was written against these versions, current in October 2026:

  • .NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (general availability is expected 10 to 13 November 2026). The WPF binding validation API is unchanged between .NET Framework 4.8 and .NET 8, 9 and 10, so everything here applies to all of them; the .NET 10 WPF release notes do not list a change in this area.

  • CommunityToolkit.Mvvm 8.4.2 for ObservableValidator, used in the examples.

  • Windows App SDK 2.5.1 (stable channel) for the WinUI 3 comparison.

  • Visual Studio 2026 18.11.1.

What Microsoft Learn Provides, and What It Does Not

The official WPF data-binding documentation describes validation under "Data validation" in the binding overview, and has a how-to for implementing binding validation (ValidationRule, ExceptionValidationRule, IDataErrorInfo). The .NET API reference documents INotifyDataErrorInfo and ObservableValidator.

Microsoft Learn does not have a WPF-specific INotifyDataErrorInfo how-to. The interface is documented only as an API reference page, and the WPF binding engine’s support for it (NotifyDataErrorValidationRule, on by default) is mentioned in the reference for Binding.ValidatesOnNotifyDataErrors rather than in a tutorial. The bridge between the interface and DataAnnotations, ObservableValidator, is documented by the MVVM Toolkit (see the MVVM Toolkit page of this section for the toolkit as a whole). Where this page shows an INotifyDataErrorInfo implementation with asynchronous checks, it is a pattern built on the documented interface, not an official sample.

The Validation Pipeline

A binding validates when it writes a value from the target to the source (TwoWay or OneWayToSource modes, on the UpdateSourceTrigger event). It follows this order:

flowchart TD A["User edits the control"] --> B["UpdateSourceTrigger fires"] B --> C["ValidationRules at RawProposedValue"] C --> D["Value converter runs"] D --> E["ValidationRules at ConvertedProposedValue"] E --> F["Source property is set"] F --> G["ValidationRules at UpdatedValue"] G --> H["Rules at CommittedValue when the edit is committed"] F --> I["Exception from the setter becomes an error with ValidatesOnExceptions"] F --> J["IDataErrorInfo and INotifyDataErrorInfo queried"] C --> K["Error added to Validation.Errors"] E --> K G --> K I --> K J --> K K --> L["Validation.HasError true and ErrorTemplate shown"]

If a rule that runs before the source is updated fails, the source is not written, and the control keeps showing the invalid text. That is the main difference from errors reported by IDataErrorInfo or INotifyDataErrorInfo, which are checked after the setter has run, so the invalid value does reach the view model.

The Four Mechanisms

Mechanism Where the rule lives Strengths Limits

ValidationRule subclass

In a class referenced from the binding in XAML

Runs before the value reaches the source, can see the raw text, reusable across views

The rule knows nothing about other properties; it sits in the view and can duplicate view-model rules

ValidatesOnExceptions (ExceptionValidationRule)

The property setter or the value converter throws

No extra class; works with existing guard code

Exceptions as control flow; one message per failure; the exception message is shown to users

IDataErrorInfo

The view model implements an indexer and Error

Rules live next to the data; sees the whole object

One message (a string) per property; synchronous; legacy interface

INotifyDataErrorInfo

The view model implements GetErrors, HasErrors and ErrorsChanged

Several errors per property, entity-level errors, asynchronous validation, errors can be raised at any time

More code to write by hand; the toolkit’s ObservableValidator removes most of it

For new code the recommendation is INotifyDataErrorInfo through ObservableValidator, with ValidationRule kept for rules about the text itself (format, characters) that must run before conversion. IDataErrorInfo is mostly met in older code.

ValidationRule

A ValidationRule is an abstract class with one method to override, Validate(object value, CultureInfo cultureInfo), which returns a ValidationResult. ValidationResult.ValidResult signals success; new ValidationResult(false, "message") signals failure with an error content that ends up in ValidationError.ErrorContent.

using System.Globalization;
using System.Windows.Controls;

namespace Bookshelf.Desktop.Validation;

public sealed class YearRule : ValidationRule
{
    public int Min { get; set; } = 1450;

    public int Max { get; set; } = DateTime.Now.Year + 1;

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var text = (value as string)?.Trim();
        if (!int.TryParse(text, NumberStyles.Integer, cultureInfo, out var year))
        {
            return new ValidationResult(false, "Enter the year as a whole number.");
        }

        return year >= Min && year <= Max
            ? ValidationResult.ValidResult
            : new ValidationResult(false, $"The year must be between {Min} and {Max}.");
    }
}

The rule is added to the binding’s ValidationRules collection with the property element syntax, because a markup extension cannot hold a collection of objects:

<TextBox Grid.Column="1" Grid.Row="2">
    <TextBox.Text>
        <Binding Path="Year" UpdateSourceTrigger="PropertyChanged" ValidatesOnExceptions="True">
            <Binding.ValidationRules>
                <local:YearRule Min="1450" ValidationStep="RawProposedValue" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

The properties of ValidationRule that matter:

Property Meaning

ValidationStep

When the rule runs: RawProposedValue (default, the unconverted control value), ConvertedProposedValue (after the converter), UpdatedValue (after the source is set) or CommittedValue (after the edit is committed)

ValidatesOnTargetUpdated

When true, the rule also runs when the source writes to the target, so an invalid initial value is flagged on load; the default is false

ValidationRule instances in Binding.ValidationRules

Evaluated in order; the first failing rule at a step stops the later ones at that step

A rule is a plain object that is not part of the element tree, so it has no DataContext and its properties cannot be bound to view-model values. Keep rules self-contained and give them configuration through ordinary properties, as Min and Max above.

Validating With Exceptions

ValidatesOnExceptions="True" adds an ExceptionValidationRule to the binding. If a value converter or the property setter throws while the binding is updating the source, the exception becomes a ValidationError whose ErrorContent is the exception message and whose Exception property holds the exception. This is what produces the red border when a user types abc into a Pages box bound to an int: the default conversion fails and the rule reports it.

namespace Bookshelf.Core;

public sealed class BookEntry
{
    private int _pages;

    public int Pages
    {
        get => _pages;
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
            _pages = value;
        }
    }
}

The binding declares {Binding Pages, ValidatesOnExceptions=True}. The message is the framework’s, and so is not localised or written for users, which is the main reason to prefer other mechanisms for anything beyond a prototype. Throwing from a setter also makes the view model refuse invalid values rather than store them, which is sometimes what is wanted.

IDataErrorInfo

IDataErrorInfo (namespace System.ComponentModel) has two members: string Error, an object-level message, and the indexer string this[string columnName], which returns the message for one property, or an empty string (or null) when the property is valid. The binding asks for it when ValidatesOnDataErrors="True" is set, which adds a DataErrorValidationRule.

using System.ComponentModel;

namespace Bookshelf.Desktop.ViewModels;

public sealed class LegacyBookViewModel : IDataErrorInfo
{
    public string Title { get; set; } = string.Empty;

    public string Error => string.Empty;

    public string this[string columnName] => columnName switch
    {
        nameof(Title) when string.IsNullOrWhiteSpace(Title) => "Title is required.",
        nameof(Title) when Title.Length > 200 => "Title is limited to 200 characters.",
        _ => string.Empty,
    };
}
<TextBox Text="{Binding Title, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />

The indexer is called when the binding updates the source and when the source raises PropertyChanged for the bound property. The interface cannot report two errors on one property, cannot report errors asynchronously, and cannot say "the object is invalid" except through Error, which a plain binding does not display.

INotifyDataErrorInfo

INotifyDataErrorInfo (namespace System.ComponentModel, since .NET Framework 4.5) fixes the limits of IDataErrorInfo:

Member Contract

bool HasErrors

true when the object has any error

IEnumerable GetErrors(string? propertyName)

The errors of one property; for null or an empty string, the errors of the whole object (entity-level errors). Items may be any object, usually strings

event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged

Must be raised whenever the errors of a property (or of the entity) change; the argument names the property

The binding engine listens to ErrorsChanged and calls GetErrors again for that property, so validation can complete at any time, including after the user has moved on. The support is on by default (Binding.ValidatesOnNotifyDataErrors is true), so no binding attribute is needed.

The next example implements the interface by hand, with a synchronous rule and an asynchronous uniqueness check for the ISBN against IBookRepository. It is a Bookshelf Desktop pattern; the documentation provides the interface contract, not this implementation.

using System.Collections;
using System.ComponentModel;
using Bookshelf.Core;

namespace Bookshelf.Desktop.ViewModels;

public sealed class IsbnEditor(IBookRepository repository) : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private readonly Dictionary<string, List<string>> _errors = [];
    private CancellationTokenSource? _checking;
    private string _isbn = string.Empty;

    public event PropertyChangedEventHandler? PropertyChanged;
    public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;

    public bool HasErrors => _errors.Count > 0;

    public string Isbn
    {
        get => _isbn;
        set
        {
            if (_isbn == value) return;
            _isbn = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Isbn)));
            _ = ValidateIsbnAsync(value);
        }
    }

    public IEnumerable GetErrors(string? propertyName) =>
        propertyName is not null && _errors.TryGetValue(propertyName, out var list) ? list : Array.Empty<string>();

    private async Task ValidateIsbnAsync(string value)
    {
        _checking?.Cancel();
        _checking = new CancellationTokenSource();
        var token = _checking.Token;

        var errors = new List<string>();
        if (value.Replace("-", "").Length is not (10 or 13))
        {
            errors.Add("An ISBN has 10 or 13 digits.");
        }

        SetErrors(nameof(Isbn), errors);
        if (errors.Count > 0) return;

        try
        {
            if (await repository.ExistsIsbnAsync(value, token))
            {
                errors.Add("This ISBN is already in the library.");
                SetErrors(nameof(Isbn), errors);
            }
        }
        catch (OperationCanceledException)
        {
            // A newer value superseded this check; its own result will be reported.
        }
    }

    private void SetErrors(string propertyName, List<string> errors)
    {
        if (errors.Count == 0 && !_errors.Remove(propertyName)) return;
        if (errors.Count > 0) _errors[propertyName] = errors;
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }
}

IBookRepository.ExistsIsbnAsync(string, CancellationToken) is a method added to the repository interface of the running scenario for this example. Because ValidateIsbnAsync resumes on the UI thread (the setter is called there and await captures the synchronisation context), ErrorsChanged is raised on the UI thread. If errors are produced on another thread, marshal the event to the dispatcher before raising it.

The Validation Attached Properties

The System.Windows.Controls.Validation class is the bridge between the binding engine and the visual tree. All its members are attached properties and events, readable on any UIElement:

Member Meaning

Validation.HasError

Read-only bool; true when the element has at least one validation error. The usual trigger property for styles

Validation.Errors

Read-only ReadOnlyObservableCollection<ValidationError> with the active errors of the element’s bindings

Validation.ErrorTemplate

The ControlTemplate applied in the adorner layer while HasError is true

Validation.ValidationAdornerSite

Another element where the template is placed instead of the control itself (for composite controls)

Validation.ValidationAdornerSiteFor

On a part of a template: names the control it acts as adorner site for

Validation.Error

Routed event raised when an error is added or removed, but only for bindings with NotifyOnValidationError="True"

ValidationError carries ErrorContent (the message, or any object), RuleInUse (the ValidationRule that failed), Exception (set for exception errors) and BindingInError (the binding that produced it). To react in code, listen to Validation.Error on a container with NotifyOnValidationError set on the bindings:

using System.Windows;
using System.Windows.Controls;

namespace Bookshelf.Desktop.Views;

public partial class EditBookDialog : Window
{
    private int _errorCount;

    public EditBookDialog()
    {
        InitializeComponent();
        AddHandler(Validation.ErrorEvent, new RoutedEventHandler(OnValidationError));
    }

    private void OnValidationError(object sender, RoutedEventArgs e)
    {
        var args = (ValidationErrorEventArgs)e;
        _errorCount += args.Action == ValidationErrorEventAction.Added ? 1 : -1;
        SaveButton.IsEnabled = _errorCount == 0;
    }
}

Counting events in the view works but duplicates the state the view model already has. Prefer binding the Save command to the view model’s HasErrors (shown below).

Error Templates

By default, a control with Validation.HasError gets a red border drawn by the built-in error template. To change the presentation, replace Validation.ErrorTemplate. The template content is placed in the adorner layer, above the control, and must contain an AdornedElementPlaceholder, which marks where the original control is drawn. The AdornedElement property of the placeholder gives access to the control and so to its Validation.Errors.

<Window.Resources>
    <ControlTemplate x:Key="ErrorListTemplate">
        <StackPanel>
            <Border BorderBrush="Firebrick" BorderThickness="2" CornerRadius="2">
                <AdornedElementPlaceholder x:Name="placeholder" />
            </Border>
            <ItemsControl ItemsSource="{Binding ElementName=placeholder, Path=AdornedElement.(Validation.Errors)}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Foreground="Firebrick" FontSize="11" Text="{Binding ErrorContent}" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </StackPanel>
    </ControlTemplate>

    <Style TargetType="TextBox">
        <Setter Property="Validation.ErrorTemplate" Value="{StaticResource ErrorListTemplate}" />
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="True">
                <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

The parentheses in (Validation.Errors) mark an attached property in a binding path, and [0] selects the first error. The result is the dialog drawn below: each field with errors is outlined and lists every message, and Save is disabled while any error exists.

Mock-up of the Bookshelf Desktop edit book dialog with a red outline on invalid fields, a list of error messages under each, and a disabled Save button

Practical limits of the adorner approach:

  • The adorner is drawn on top of the layout and takes no layout space, so the message list overlaps the controls below it unless the row has a margin reserved for it (the dialog above uses a Margin of 0,0,0,22 on every field row).

  • The adorner is clipped by the nearest clipping container. In a ScrollViewer or a DataGrid cell the message may be cut; set Validation.ValidationAdornerSite to a named element inside the layout instead of relying on the overlay.

  • The default red border is part of the template, so a replacement template that omits a border shows no outline at all. Always draw something around AdornedElementPlaceholder.

  • The template is created every time HasError becomes true, so keep it light in long lists.

Bridging to ObservableValidator and DataAnnotations

ObservableValidator (package CommunityToolkit.Mvvm, namespace CommunityToolkit.Mvvm.ComponentModel) derives from ObservableObject and implements INotifyDataErrorInfo on top of System.ComponentModel.DataAnnotations. Validation attributes on a property are evaluated when the property is set through SetProperty(ref field, value, true), or when the source generator’s [NotifyDataErrorInfo] attribute is applied to an [ObservableProperty]. The base class provides ValidateProperty, ValidateAllProperties, ClearErrors, GetErrors, HasErrors and the ErrorsChanged event.

using System.ComponentModel.DataAnnotations;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Bookshelf.Desktop.ViewModels;

// Shown with a repository for brevity; the canonical BookEditorViewModel of the MVVM Toolkit page returns a Book from ToBook().
public sealed partial class BookEditorViewModel(IBookRepository repository) : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    [Required(ErrorMessage = "Title is required.")]
    [MaxLength(200, ErrorMessage = "Title is limited to 200 characters.")]
    public partial string Title { get; set; } = string.Empty;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    [RegularExpression(@"^(\d{9}[\dXx]|\d{13})$", ErrorMessage = "An ISBN has 10 or 13 digits without hyphens.")]
    [CustomValidation(typeof(BookEditorViewModel), nameof(ValidateIsbnChecksum))]
    public partial string Isbn { get; set; } = string.Empty;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    [Required(ErrorMessage = "Author is required.")]
    public partial string Author { get; set; } = string.Empty;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    [Range(1450, 2100, ErrorMessage = "Year must be between 1450 and 2100.")]
    public partial int Year { get; set; } = 2000;

    public static ValidationResult? ValidateIsbnChecksum(string isbn, ValidationContext context) =>
        IsbnChecksum.IsValid(isbn)
            ? ValidationResult.Success
            : new ValidationResult("The ISBN check digit is wrong.");

    private bool CanSave() => !HasErrors;

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync(CancellationToken token)
    {
        ValidateAllProperties();
        if (HasErrors) return;

        await repository.AddAsync(new Book(0, Title, Author, Year, 0m, Isbn), token);
    }
}

Notes on the example:

  • IsbnChecksum.IsValid is a small helper of the Bookshelf scenario, not a framework type, and AddAsync is the repository method that stores a book. Both are defined in the scenario rather than in the documentation.

  • The generated property forwards the DataAnnotations attributes, and [NotifyDataErrorInfo] makes the generated setter call ValidateProperty. Without it, the property is only validated when ValidateAllProperties runs.

  • The page for ObservableProperty still shows fields. Partial properties are a C# 13 feature (on .NET 9 the toolkit needed LangVersion set to preview); with CommunityToolkit.Mvvm 8.4.1 or later and the default C# 14 of .NET 10, no LangVersion setting is needed.

  • Validation attributes are evaluated synchronously. The toolkit documentation does not describe asynchronous checks such as the repository lookup shown earlier, so a view model that needs one either implements INotifyDataErrorInfo itself, as IsbnEditor does, or runs the check in a command before saving.

The view then needs nothing beyond the binding. ValidatesOnNotifyDataErrors is on by default and the error template from the previous section renders the messages:

<TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Isbn, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Year, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" />

The Year box mixes two mechanisms: a conversion failure from text to int is raised by the binding itself (and needs ValidatesOnExceptions or the built-in conversion error), while [Range] runs only after a valid int reaches the view model.

When Validation Runs

Setting Effect

UpdateSourceTrigger=LostFocus (default for TextBox.Text)

Validation runs when the control loses focus; the user sees errors late but only once

UpdateSourceTrigger=PropertyChanged

Validation runs on every keystroke; errors appear and disappear while typing, which can be noisy for rules like "ISBN has 13 digits"

UpdateSourceTrigger=Explicit

Validation runs on BindingExpression.UpdateSource(); useful for "validate on Save"

Delay (milliseconds)

With PropertyChanged, waits for a pause in typing before updating and validating

ValidationRule.ValidatesOnTargetUpdated

Validates when the source writes to the target, so invalid stored data is flagged on load

NotifyOnValidationError

Raises Validation.Error for each added or removed error

INotifyDataErrorInfo errors that exist at load time are displayed as soon as the binding connects, since the binding queries GetErrors then. That is why a new, empty BookEditorViewModel should not call ValidateAllProperties in its constructor: the user would see a form full of errors before typing. Validate on change and on Save instead.

WPF, WinUI 3 and WinForms

WPF WinUI 3 WinForms

Binding-engine validation

ValidationRule, exceptions, IDataErrorInfo, INotifyDataErrorInfo

No ValidationRule or Validation class; validation is written in the view model

Binding raises BindingComplete; Control.Validating and Validated events

Error presentation

Validation.ErrorTemplate adorner

Own UI, for example a TextBlock bound to the view-model error, or InfoBar; third-party or toolkit helpers

ErrorProvider component with an error icon; DataSource can use IDataErrorInfo

INotifyDataErrorInfo

Understood by the binding engine

The interface can be implemented, but the binding does not display its errors automatically (verify against the Windows App SDK release you use; this is a claim made from knowledge, not from a Learn page)

Not a binding feature (use ErrorProvider with your own errors)

MVVM Toolkit ObservableValidator

Works with the default bindings

Works as a source of errors; the view reads GetErrors

Same, through the ErrorProvider

Pitfalls

  • The source is not updated when a ValidationRule fails at RawProposedValue. The view model keeps the last valid value, so a Save command that reads the view model saves old data while the box shows the invalid text. Disable Save while Validation.HasError is true somewhere in the form, or move the rule to the view model.

  • Validation.HasError is per element. There is no built-in "the whole window is valid". Either use the view model’s HasErrors, or walk the logical tree to find invalid elements.

  • IDataErrorInfo messages are strings. Exposing a localised string requires the view model to translate the message itself; do not return Resources lookups that fail in the designer.

  • Exceptions are expensive. ValidatesOnExceptions throws and catches an exception on every invalid keystroke with UpdateSourceTrigger=PropertyChanged. Use it for conversion failures, not for business rules.

  • Forgetting ErrorsChanged. If a hand-written INotifyDataErrorInfo updates its dictionary but does not raise the event, the UI keeps showing the old state. Raise it for every property whose error set changed, and also for entity-level errors (an empty or null property name).

  • Raising ErrorsChanged off the UI thread. The binding engine reacts to the event by updating UI state, so mutate the error collection and raise the event on the dispatcher thread.

  • The error template replaces the default, it does not extend it. Keep an outline around AdornedElementPlaceholder or the control looks valid.

  • Hidden controls. A control in a collapsed tab still has its errors, so HasErrors can be true for a field the user cannot see. Report the first invalid field and navigate to it.

  • Trimming and AOT. DataAnnotations discovers attributes by reflection. WPF and WinForms do not support Native AOT or trimming (see the Windows App Platforms page of this section), so this is not a restriction today, but it matters if the same view models move to WinUI 3 with trimming.