Validation, converters and input handling

Validation rules, IDataErrorInfo and INotifyDataErrorInfo, when a converter beats a string format, and the canExecute and focus details that make a form feel finished.

Three validation mechanisms

MechanismLives inBest forLimitation
ValidationRuleThe bindingFormat checks local to one fieldCannot see other fields or the model
IDataErrorInfoThe view modelSingle-property business rulesReports one error per property at a time
INotifyDataErrorInfoThe view modelAsync and multi-error validationMore code; the modern choice
public sealed class OrderForm : INotifyDataErrorInfo
{
    private readonly Dictionary<string, List<string>> _errors = new();

    public bool HasErrors => _errors.Count > 0;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

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

    private void Validate(string propertyName, object value)
    {
        var errors = propertyName switch
        {
            nameof(Customer) when string.IsNullOrWhiteSpace(value as string) => new List<string> { "Customer is required." },
            nameof(Total) when (decimal)value <= 0m => new List<string> { "Total must be greater than zero." },
            _ => new List<string>()
        };

        if (errors.Count > 0) _errors[propertyName] = errors;
        else _errors.Remove(propertyName);

        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        OnPropertyChanged(nameof(HasErrors));
    }
}
<TextBox Text="{Binding Customer, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />
<Button Content="Save"
        IsEnabled="{Binding HasErrors, Converter={StaticResource InverseBool}}" />
  • UpdateSourceTrigger=PropertyChanged validates as the user types; the default LostFocus waits until they leave the field.
  • Set ValidatesOnExceptions only for genuinely unexpected exceptions - a parse failure should be handled by the binding, not surfaced as an error dialog.
  • Validation errors show as a red border by default; add a Validation.ErrorTemplate when you need a message beside the field.

Converters and formatting

public sealed class BoolToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
        value is true ? Visibility.Visible : Visibility.Collapsed;

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
        value is Visibility.Visible;
}

public sealed class MoneyConverter : IValueConverter
{
    public object Convert(object value, Type t, object p, CultureInfo c) =>
        ((decimal)value).ToString("C", c);

    public object ConvertBack(object value, Type t, object p, CultureInfo c) =>
        decimal.TryParse((string)value, NumberStyles.Currency, c, out var d) ? d : Binding.DoNothing;
}

Use StringFormat for display-only formatting - {Binding Total, StringFormat={}{0:C}} is cheaper and shorter than a converter. Write a converter when the transformation is structural (boolean to visibility, enum to colour) or when the value must also convert back.

<!-- StringFormat, not a converter, for simple display formatting -->
<TextBlock Text="{Binding Total, StringFormat={}{0:N2}}" />

<!-- the built-in converter for booleans -->
<Button IsEnabled="{Binding IsEditable}" Visibility="{Binding IsEditable, Converter={StaticResource BoolToVisibility}}" />

Input, commands and focus

// a relay command that re-evaluates on its own
public sealed class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool> _canExecute;

    public RelayCommand(Action execute, Func<bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
    public void Execute(object parameter) => _execute();
    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}
  • Attach a KeyBinding or an AccessKey (_Save) so the form is usable without a mouse.
  • Never put focus logic inside a property setter - schedule it with Dispatcher.BeginInvoke so the visual tree has finished updating.
  • A disabled button with no explanation is a support call. Use the same CanExecute predicate to drive a tooltip that says why.
⚠️
A converter that returns null for an unexpected input type will throw during binding and WPF will silently fall back to the default value. Return Binding.DoNothing to skip an update and DependencyProperty.UnsetValue to fall back to the default, and log the unexpected type instead of hiding it.

FAQ

Which validation approach should a new project use?
INotifyDataErrorInfo in the view model. It supports several errors per property, works with async rules, and keeps the rules unit-testable away from the view.
How do I stop a user saving an invalid form?
Bind the Save command's CanExecute to the same validity state the errors come from. Two independent checks will eventually disagree.

MVVM basics and commands Navigation, windows and dialogs

Last refreshed 2026-09-18.