DEV Community

Spyros Ponaris
Spyros Ponaris

Posted 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.

🧠 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.

πŸ“š Related

πŸ”— MVVM is not just for WPF β€” Why it still matters in 2025 (even in Blazor and WinForms)

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.