DEV Community

Cover image for 🧼 Elegant WPF Validation with FluentValidation and CommunityToolkit.Mvvm
Spyros Ponaris
Spyros Ponaris

Posted on • Edited on

🧼 Elegant WPF Validation with FluentValidation and CommunityToolkit.Mvvm

In our previous article, we explained why MVVM is not just for WPF anymore — it’s a powerful, reusable pattern that applies equally well in WinForms, Blazor, and beyond.

In this follow-up, we’ll show how to use FluentValidation with CommunityToolkit.Mvvm and INotifyDataErrorInfo to handle clean, per-field validation in WPF and the same pattern can extend to WinForms or even Blazor (with minor tweaks).

Source code :

GitHub Repo: MVVM-WPF-BLAZOR-WINFORMS

Why this combo?

FluentValidation gives you a fluent, reusable way to define business rules.

  • CommunityToolkit.Mvvm makes ViewModels lightweight and clean.
  • INotifyDataErrorInfo works with WPF’s built-in validation system (and WinForms with a little glue).
  • Together, they give you real-time validation with red borders, tooltips, and clean logic.

🧱 The Setup

(💡 Skip to the GitHub repo or read below for the essentials)

  1. Install the packages:
Install-Package FluentValidation
Install-Package CommunityToolkit.Mvvm
Enter fullscreen mode Exit fullscreen mode
  1. Create your model and validator:
public class Customer
{
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required.");
        RuleFor(x => x.Email).EmailAddress().WithMessage("Invalid email.");
    }
}
Enter fullscreen mode Exit fullscreen mode

👨‍💻 The ViewModel

public partial class CustomerViewModel : ObservableObject, INotifyDataErrorInfo
{
    private readonly CustomerValidator _validator = new();
    private readonly Customer _customer = new();

    private readonly Dictionary<string, List<string>> _errors = new();

    [ObservableProperty]
    private string name = string.Empty;

    [ObservableProperty]
    private string email = string.Empty;

    partial void OnNameChanged(string value)
    {
        if (_customer.Name == value)
            return;

        _customer.Name = value;
        ValidateProperty(nameof(Name));
    }

    partial void OnEmailChanged(string value)
    {
        if (_customer.Email == value)
            return;

        _customer.Email = value;
        ValidateProperty(nameof(Email));
    }

    public bool HasErrors => _errors.Any();

    public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;

    public IEnumerable GetErrors(string? propertyName)
    {
        if (propertyName is null) return Enumerable.Empty<string>();
        return _errors.TryGetValue(propertyName, out var errors) ? errors : Enumerable.Empty<string>();
    }

    [RelayCommand]
    private void Submit()
    {
        ValidateAllProperties();

        if (!HasErrors)
        {
            MessageBox.Show("Customer data is valid!", "Success", MessageBoxButton.OK, MessageBoxImage.Information);
        }
    }

    private void ValidateAllProperties()
    {
        _errors.Clear();
        var results = _validator.Validate(_customer);

        foreach (var error in results.Errors)
        {
            if (!_errors.ContainsKey(error.PropertyName))
                _errors[error.PropertyName] = new List<string>();

            _errors[error.PropertyName].Add(error.ErrorMessage);
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(error.PropertyName));
        }
    }

    private void ValidateProperty(string propertyName)
    {
        var results = _validator.Validate(_customer, options => options.IncludeProperties(propertyName));

        if (results.IsValid)
        {
            _errors.Remove(propertyName);
        }
        else
        {
            _errors[propertyName] = results.Errors.Select(e => e.ErrorMessage).ToList();
        }

        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }

}
Enter fullscreen mode Exit fullscreen mode

🖼 XAML Example (WPF)

<StackPanel>
    <TextBox Text="{Binding Name, 
            UpdateSourceTrigger=PropertyChanged, 
            ValidatesOnNotifyDataErrors=True}" 
     Margin="0,0,0,10"
     ToolTip="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource Self}}" />

    <TextBox Text="{Binding Email, 
            UpdateSourceTrigger=PropertyChanged, 
            ValidatesOnNotifyDataErrors=True}" 
     ToolTip="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource Self}}" />

    <Button Content="Submit" Command="{Binding SubmitCommand}" />
</StackPanel>
Enter fullscreen mode Exit fullscreen mode

WPF will automatically show red borders and tooltips for errors when your ViewModel implements INotifyDataErrorInfo.

🌐 In Blazor? Use Blazored.FluentValidation

If you're working in Blazor, you don’t need to manually implement validation logic. The Blazored.FluentValidation package integrates FluentValidation directly into the EditForm component.

🔧 Setup:

Install-Package Blazored.FluentValidation
Enter fullscreen mode Exit fullscreen mode

👇 Usage:

<EditForm Model="@model" OnValidSubmit="HandleSubmit">
    <FluentValidationValidator />
    <InputText @bind-Value="model.Name" />
    <ValidationMessage For="@(() => model.Name)" />
    <button type="submit">Submit</button>
</EditForm>
Enter fullscreen mode Exit fullscreen mode

No extra boilerplate, no manual wiring. The validator is automatically discovered via DI.

🌐 Portable MVVM: WPF, WinForms, and Blazor

While this example is in WPF, the MVVM validation logic is platform-agnostic. In WinForms, you can bind to the same ViewModel using BindingSource.

🎁 Bonus Tips: FluentValidation + CommunityToolkit.Mvvm

Level up your validation architecture with these practical enhancements:

✅ 1. Combine Validation with [ObservableProperty] Seamlessly
If you use [ObservableProperty] from CommunityToolkit.Mvvm, FluentValidation still works—you just need to revalidate on property change:

partial class CustomerViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    private string name;

    partial void OnNameChanged(string oldValue, string newValue)
    {
        ValidateProperty(newValue, nameof(Name));
    }
}

Enter fullscreen mode Exit fullscreen mode

Tip: If you're validating multiple fields, consider triggering ValidateAllProperties() in batch updates.

🧠 2. Use a Shared FluentValidator Across ViewModels
If multiple view models share validation rules (e.g., for DTOs or domain models), create a standalone IValidator and inject it:

public class CustomerValidator : AbstractValidator<CustomerViewModel>
{
    public CustomerValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Then plug it in:

public CustomerViewModel(CustomerValidator validator)
{
    _validator = validator;
    ValidateAllProperties();
}
Enter fullscreen mode Exit fullscreen mode

🛡️ 3. Revalidate on Submit (for safety)
Users can sometimes bypass UI validation. Always validate again before processing:

if (_validator.Validate(this).IsValid)
{
    // Proceed safely
}
else
{
    // Notify user
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 4. Avoid Redundant Notifications

If you're calling SetProperty or OnPropertyChanged, avoid triggering ValidateAllProperties() unless you really need to. Prefer ValidateProperty() per field to reduce noise and improve performance.

🧰 5. Custom Error Presentation

Want cleaner UI? Instead of binding directly to Validation.Errors, use a custom ValidationSummaryViewModel to collect and display messages elegantly.

🧼 6. Clear Errors on Reset
When resetting a form, call:

ClearErrors();
ValidateAllProperties();
Enter fullscreen mode Exit fullscreen mode

This avoids stale errors appearing when repopulating fields.

**ObservableValidator **is a base class provided by CommunityToolkit.Mvvm that combines:

✅ ObservableObject (INotifyPropertyChanged support), and

✅ INotifyDataErrorInfo (WPF validation support).

It’s designed to simplify validation in MVVM, especially when using libraries like FluentValidation.

🔍 Why Use ObservableValidator?

WPF supports validation via INotifyDataErrorInfo. If you want automatic UI error feedback (e.g. red borders, tooltips) when a property is invalid, your ViewModel needs to implement this interface. But doing that manually is verbose.

  • ObservableValidator handles this for you:
  • It tracks property errors
  • Raises validation changed events
  • Integrates well with data-binding

Want to dive deeper into MVVM?

Check out my latest article:

🔗 MVVM Is Not Just for WPF – Why It Still Matters in 2025 (Even in Blazor and WinForms)

Whether you're building with Blazor, WinForms, or something else entirely, MVVM can still be your secret weapon for clean, maintainable UI logic.

🧠 Final Thoughts

This approach keeps your ViewModel clean, testable, and logic-driven — exactly what MVVM is all about. FluentValidation brings expressive rules. CommunityToolkit.Mvvm eliminates boilerplate. And INotifyDataErrorInfo ties it all together with real-time UI feedback.

Top comments (5)

Collapse
 
glebasos profile image
Gleb

By the way, I've got a questions on it.
1) You bind to Email, but use OnEmailChanged to trigger validation. Can we do it without additional OnXXXChanged on every property?
2) Can we
[ObservableProperty]
private Customer _customer;

And bind to Customer.Email etc and somehow validate there with less boilerplate?

Collapse
 
stevsharp profile image
Spyros Ponaris • Edited

Great questions!
Not always. If you're using [ObservableProperty], it generates OnPropertyChanged, and you can hook into partial void OnPropertyNameChanged(...) only when needed. For validation, you can centralize it using INotifyDataErrorInfo or a shared Validate method, so you don’t need OnXXXChanged for every property.

learn.microsoft.com/en-us/dotnet/c...

You can bind to Customer.Email, but validation is harder unless Customer also supports validation (like INotifyDataErrorInfo). A common workaround is to expose a property in your ViewModel like:

public string Email
{
    get => Customer?.Email;
    set
    {
        if (Customer != null)
        {
            Customer.Email = value;
            OnPropertyChanged();
            ValidateEmail();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
glebasos profile image
Gleb

Thank you! I was just looking for something that is not a tons of boilerplate and millions of [TAGS] for my avalonia project. So far it really seemed kinda confusing especially with lack of examples from avalonia docs

Collapse
 
stevsharp profile image
Spyros Ponaris

Unfortunately, that's true , the lack of proper documentation is a big issue. It makes getting started with Avalonia more difficult than it should be, especially when you're looking for clear, minimal examples without all the boilerplate or excessive tags.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.