FluentValidation in C# WPF Applications

FluentValidation in C# WPF Applications

This article explores how to use FluentValidation for property validation in C# WPF projects and demonstrates how to implement this through the MVVM pattern.

Last updated 1/25/2024 5:17 AM
沙漠尽头的狼
19 min read
Category
WPF
Tags
.NET C# WPF MVVM FluentValidation

1. Introduction

In the .NET development landscape, FluentValidation has become the preferred tool for developers to perform property validation due to its elegance and extensibility. It is not only suitable for web development, such as MVC, Web API, and ASP.NET Core, but also integrates seamlessly into WPF applications, providing powerful data validation capabilities. This article will delve into how to use FluentValidation for property validation in C# WPF projects and demonstrate how to achieve this through the MVVM pattern.

2. Feature Overview

Our goal is to build a WPF application that uses FluentValidation to implement the following validation features:

  1. Validate basic data type properties in the ViewModel layer, e.g., int, string.
  2. Validate complex properties in the ViewModel, including sub-properties of object properties and collection properties.
  3. Provide two intuitive error prompt styles to enhance the user experience.

First, take a look at the effect:

3. Problem Solving and Exploration

During my research, I found that the official FluentValidation documentation mainly focuses on validation for web applications. For WPF and complex property validation, the official documentation provides limited examples. However, through in-depth research and practice, I have found effective methods to combine FluentValidation with WPF, especially for complex property validation.

4. Development Steps

4.1. Create Project and Import Libraries

First, create a new WPF project and import the FluentValidation library for property validation, as well as the Prism.Wpf library to simplify the implementation of the MVVM pattern.

<ItemGroup>
  <PackageReference Include="FluentValidation" Version="11.9.0" />
  <PackageReference Include="Prism.Wpf" Version="9.0.271-pre" />
</ItemGroup>

4.2. Create Entity Classes

I created two entity classes: Student and Field, representing object properties and collection item properties, respectively. Both classes implement the IDataErrorInfo interface:

  1. The IDataErrorInfo interface is commonly used to provide error information for entity data validation. This interface includes two members: an indexer (this[string columnName]) and an Error property. The indexer provides error information per property name, while the Error property provides an overall error summary for the entity.
  2. Both entity classes and the ViewModel mentioned later implement the IDataErrorInfo interface, using FluentValidation in the this[string columnName] indexer and the Error property to validate properties.

4.2.1. Plain Class - Student

The Student class contains five properties: Name, Age, Zip, MinValue, and MaxValue. The MinValue and MaxValue involve cross-validation, meaning that when the minimum value changes, it triggers validation of the maximum value, and vice versa.

/// <summary>
///     Student entity
///     Inherits BindableBase, i.e., inherits the property change interface INotifyPropertyChanged
///     Implements IDataErrorInfo interface, required for FluentValidation validation
/// </summary>
public class Student : BindableBase, IDataErrorInfo
{
    private int _age;
    private string? _name;
    private string? _zip;
    private readonly StudentValidator _validator = new();

    public string? Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }

    public int Age
    {
        get => _age;
        set => SetProperty(ref _age, value);
    }

    public string? Zip
    {
        get => _zip;
        set => SetProperty(ref _zip, value);
    }

    private int _minValue;

    public int MinValue
    {
        get => _minValue;
        set
        {
            SetProperty(ref _minValue, value);

            // Trigger validation of MaxValue
            RaisePropertyChanged(nameof(MaxValue));
        }
    }

    private int _maxValue;

    public int MaxValue
    {
        get => _maxValue;
        set
        {
            SetProperty(ref _maxValue, value);

            // Trigger validation of MinValue
            RaisePropertyChanged(nameof(MinValue));
        }
    }

    public string this[string columnName]
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var firstOrDefault =
                validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
        }
    }

    public string Error
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            return errors;
        }
    }
}

The key code is in public string this[string columnName]: data validation for input form fields occurs here. FluentValidation is invoked here, with the validation logic encapsulated in StudentValidator. When a form field is typed into, this code is called in real-time, and columnName represents the property name bound to the View.

4.2.2. Collection Class - Field

This class is used as a collection item in the ViewModel to simulate dynamic form data validation. It contains four properties: Name, TypeLabel, DataType, and Value. The validation mainly checks whether the entered value is valid based on the data type. This entity also needs to implement the IDataErrorInfo interface to trigger FluentValidation validation.

/// <summary>
///     Extension field, used to generate dynamic forms
///     Inherits BindableBase, i.e., inherits the property change interface INotifyPropertyChanged
///     Implements IDataErrorInfo interface, required for FluentValidation validation
/// </summary>
public class Field : BindableBase, IDataErrorInfo
{
    private string? _value;
    private readonly FieldValidator _validator = new();

    public Field(DataType type, string typeLabel, string name, string value)
    {
        Type = type;
        TypeLabel = typeLabel;
        Name = name;
        Value = value;
    }

    /// <summary>
    ///     Data type
    /// </summary>
    public DataType Type { get; set; }

    /// <summary>
    ///     Data type display name
    /// </summary>
    public string TypeLabel { get; set; }

    /// <summary>
    ///     Field name
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    ///     Value
    /// </summary>
    public string? Value
    {
        get => _value;
        set => SetProperty(ref _value, value);
    }

    public string this[string columnName]
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var firstOrDefault =
                validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
        }
    }

    public string Error
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            return errors;
        }
    }
}

public enum DataType
{
    Text,
    Number,
    Date
}

As shown above, the public string this[string columnName] section is written similarly to the Student class, except the _validator variable type differs – in Student it's StudentValidator, here it's FieldValidator. Let's see how these two classes are written next.

4.3. Create Validators

For each entity class, I created a corresponding validator class: StudentValidator and FieldValidator. These validator classes inherit from AbstractValidator and define validation rules within them. There are two ways to write validation rules:

  1. Add attributes above entity properties (not specifically covered in this article, as many Baidu articles cover this).
  2. Add validation rules programmatically, as shown below: create a validator class inheriting from AbstractValidator, and write rules in its constructor. This is convenient for management.

This article uses the second method and introduces it by creating the StudentValidator and FieldValidator validator classes.

4.3.1. StudentValidator

This is the StudentValidator for the Student entity. It needs to inherit from AbstractValidator<T> with the generic type specified as the entity class to be validated: Student.

public class StudentValidator : AbstractValidator<Student>
{
    public StudentValidator()
    {
        RuleFor(vm => vm.Name)
            .NotEmpty()
            .WithMessage("Please enter the student's name!")
            .Length(5, 30)
            .WithMessage("The student name must be between 5 and 30 characters.");

        RuleFor(vm => vm.Age)
            .GreaterThanOrEqualTo(0)
            .WithMessage("Student age must be an integer!")
            .ExclusiveBetween(10, 150)
            .WithMessage("Please enter a correct student age (10-150)");

        _ = RuleFor(vm => vm.Zip)
            .NotEmpty()
            .WithMessage("Zip code cannot be empty!")
            .Must(BeAValidZip)
            .WithMessage("Zip code must consist of six digits.");

        RuleFor(model => model.MinValue).Must((model, minValue) => minValue < model.MaxValue).WithMessage("Min value must be less than max value");

        RuleFor(model => model.MaxValue).Must((model, maxValue) => maxValue > model.MinValue).WithMessage("Max value must be greater than min value");
    }

    private static bool BeAValidZip(string? zip)
    {
        if (string.IsNullOrEmpty(zip))
        {
            return false;
        }

        var regex = new Regex(@"\d{6}");
        return regex.IsMatch(zip);
    }
}

The code is straightforward, using numeric range validation (Age), string non-empty and length validation (Name), regex validation (Zip), and cross-property validation (Min and Max, combined with RaisePropertyChanged(nameof(MaxValue)); in the property setter to trigger validation notifications for other properties).

4.3.2. FieldValidator

The dynamic form data validator, similarly inherits from AbstractValidator<T> with the generic type specified as Field:

public class FieldValidator : AbstractValidator<Field>
{
    public FieldValidator()
    {
        RuleFor(field => field.Value)
            .Must((field, value) => (field.Type == DataType.Text && !string.IsNullOrWhiteSpace(value))
                                    || (field.Type == DataType.Number && double.TryParse(value, out _))
                                    || (field.Type == DataType.Date && DateTime.TryParse(value, out _)))
            .WithMessage("1. Text cannot be empty; 2. For numeric type, please enter a number; 3. For date type, please enter a date");
    }
}

This is simplified:

  1. For text data type, the value cannot be empty.
  2. For numeric data type, the value must be convertible to double.
  3. For date type, the value must be convertible using DateTime.Parse.

This article only provides a simple demonstration, combining multiple data types into the Must method for unified validation, with a unified error message. Readers can modify as needed for their scenarios.

4.3.3. StudentViewModelValidator

Additionally, I created a StudentViewModelValidator to validate properties at the ViewModel level. This validator can handle basic data types, object properties, and collection properties.

public class StudentViewModelValidator : AbstractValidator<StudentViewModel>
{
    public StudentViewModelValidator()
    {
        RuleFor(vm => vm.Title)
            .NotEmpty()
            .WithMessage("Title cannot be empty!")
            .Length(5, 30)
            .WithMessage("Title must be between 5 and 30 characters.");

        RuleFor(vm => vm.CurrentStudent).SetValidator(new StudentValidator());

        RuleForEach(vm => vm.Fields).SetValidator(new FieldValidator());
    }
}
  1. Title validates a basic data type (string).
  2. CurrentStudent validates an object property (an instance of Student), using StudentValidator for its validation.
  3. Fields validates a collection property (ObservableCollection<Field>), using FieldValidator for each item. Note the use of RuleForEach to associate the item validator with the collection.

4.4. ViewModel Implementation

StudentViewModel has a structure similar to the Student entity class, both needing to implement IDataErrorInfo. This ViewModel consists of a simple string property (Title), a complex Student object property (CurrentStudent), and a collection property ObservableCollection<Field> Fields. The code is as follows:

/// <summary>
///     ViewModel for the view
///     Inherits BindableBase, i.e., inherits the property change interface INotifyPropertyChanged
///     Implements IDataErrorInfo interface, required for FluentValidation validation
/// </summary>
public class StudentViewModel : BindableBase, IDataErrorInfo
{
    private Student _currentStudent;
    private string _title;

    private readonly StudentViewModelValidator _validator;

    public string Title
    {
        get => _title;
        set => SetProperty(ref _title, value);
    }

    public Student CurrentStudent
    {
        get => _currentStudent;
        set => SetProperty(ref _currentStudent, value);
    }

    public ObservableCollection<Field> Fields { get; } = new();

    private DelegateCommand _saveCommand;

    public DelegateCommand SaveCommand => _saveCommand ??= new DelegateCommand(HandleSaveCommand,
        HandleCanExecuteSaveCommand);

    private DelegateCommand _cancelCommand;

    public DelegateCommand CancelCommand =>
        _cancelCommand ??= new DelegateCommand(HandleCancelCommand, () => true);

    public StudentViewModel()
    {
        _validator = new StudentViewModelValidator();
        CurrentStudent = new Student
        {
            Name = "Li Gang's son",
            Age = 23
        };
        Fields.Add(new Field(DataType.Text, "Text, e.g., Chengdu, Sichuan", "Address", ""));
        Fields.Add(new Field(DataType.Number, "Number, e.g., 12", "Years of Service", ""));
        Fields.Add(new Field(DataType.Date, "Date, e.g., 2023-09-26 05:13:23", "Training Date", ""));

        PropertyChanged += Validate;
        CurrentStudent.PropertyChanged += Validate;
        foreach (var field in Fields)
        {
            field.PropertyChanged += Validate;
        }
    }

    ~StudentViewModel()
    {
        PropertyChanged -= Validate;
        CurrentStudent.PropertyChanged -= Validate;
        foreach (var field in Fields)
        {
            field.PropertyChanged -= Validate;
        }
    }

    private void Validate(object sender, PropertyChangedEventArgs e)
    {
        _isCanExecuteSaveCommand = _validator.Validate(this).IsValid;
        SaveCommand.RaiseCanExecuteChanged();
    }

    private void HandleSaveCommand()
    {
        var validateResult = _validator.Validate(this);
        if (validateResult.IsValid)
        {
            MessageBox.Show("Validation succeeded!");
        }
        else
        {
            var errorMsg = string.Join(Environment.NewLine,
                validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            MessageBox.Show($"Please check your input again:\r\n{errorMsg}");
        }
    }

    private bool _isCanExecuteSaveCommand;

    private bool HandleCanExecuteSaveCommand()
    {
        return _isCanExecuteSaveCommand;
    }

    private void HandleCancelCommand()
    {
        MessageBox.Show("I do nothing, retired");
    }

    public string this[string columnName]
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var firstOrDefault =
                validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
        }
    }

    public string Error
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            return errors;
        }
    }
}

The ViewModel property validation is similar to that of Student and Field. Here I added two commands: SaveCommand and CancelCommand. The save command is only enabled when all properties are valid. This is achieved by subscribing to the PropertyChanged event and validating in the event handler:

PropertyChanged += Validate;
CurrentStudent.PropertyChanged += Validate;
foreach (var field in Fields)
{
    field.PropertyChanged += Validate;
}
private void Validate(object sender, PropertyChangedEventArgs e)
{
    _isCanExecuteSaveCommand = _validator.Validate(this).IsValid;
    SaveCommand.RaiseCanExecuteChanged();
}

4.5. View Implementation

In the view layer, I created a user control StudentView to display the input form and validation results. By binding to the ViewModel's properties and commands, the view interacts with the ViewModel and displays validation errors in real time. The XAML code is relatively straightforward, providing validation for a simple property (Title), complex properties (including student name CurrentStudent.Name, age CurrentStudent.Age, zip CurrentStudent.Zip, min CurrentStudent.MinValue, max CurrentStudent.MaxValue), and collection property (Fields):

<UserControl
    x:Class="WpfFluentValidation.Views.StudentView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:models="clr-namespace:WpfFluentValidation.Models"
    xmlns:vm="clr-namespace:WpfFluentValidation.ViewModels"
    mc:Ignorable="d" Padding="10">
    <UserControl.DataContext>
        <vm:StudentViewModel />
    </UserControl.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>
        <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>

                <GroupBox Header="ViewModel Direct Property Validation">
                    <StackPanel>
                        <Label Content="Title:" />
                        <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle1}"
                                 Text="{Binding Title, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                    </StackPanel>
                </GroupBox>

                <GroupBox Grid.Row="1" Header="ViewModel Object Property CurrentStudent Validation">
                    <StackPanel>
                        <StackPanel>
                            <Label Content="Name:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="Age:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="Zip:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Zip, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="Min:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.MinValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="Max:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.MaxValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                    </StackPanel>
                </GroupBox>

                <GroupBox Grid.Row="2" Header="ViewModel Collection Property Fields Validation">
                    <ItemsControl ItemsSource="{Binding Fields}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate DataType="{x:Type models:Field}">
                                <Border Padding="10">
                                    <Grid>
                                        <Grid.RowDefinitions>
                                            <RowDefinition Height="Auto" />
                                            <RowDefinition Height="Auto" />
                                        </Grid.RowDefinitions>
                                        <TextBlock Margin="0,0,0,5">
                                            <Run Text="{Binding Name}" />
                                            <Run Text=" (" />
                                            <Run Text="{Binding TypeLabel}" />
                                            <Run Text=")" />
                                        </TextBlock>
                                        <TextBox Grid.Row="1" Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                                 Text="{Binding Value, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                                    </Grid>
                                </Border>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </GroupBox>
            </Grid>
        </ScrollViewer>

        <StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal">
            <Button Content="Cancel" Command="{Binding CancelCommand}" Style="{StaticResource Styles.Button.Common}"
                    Margin="0 3 40 3" />
            <Button Content="Submit" Command="{Binding SaveCommand}" Style="{StaticResource Styles.Button.Blue}"
                    Margin="0 3 10 3" />
        </StackPanel>
    </Grid>
</UserControl>

4.6. Error Prompt Styles

To enhance the user experience, I defined two error prompt styles: one uses a red icon next to the input box, and the other displays error text to the right of the input box. These styles are defined in App.xaml and can be reused throughout the application.

<Application
    x:Class="WpfFluentValidation.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <Style TargetType="StackPanel">
            <Setter Property="Margin" Value="0,5" />
        </Style>
        <!--  First error style: red border -->
        <Style x:Key="Styles.TextBox.ErrorStyle1" TargetType="{x:Type TextBox}">
            <Setter Property="Width" Value="250" />
            <Setter Property="Height" Value="25" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel>
                            <Grid
                                Width="16"
                                Height="16"
                                Margin="3,0,0,0"
                                VerticalAlignment="Center"
                                DockPanel.Dock="Right">
                                <Ellipse
                                    Width="16"
                                    Height="16"
                                    Fill="Red" />
                                <Ellipse
                                    Width="3"
                                    Height="8"
                                    Margin="0,2,0,0"
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Top"
                                    Fill="White" />
                                <Ellipse
                                    Width="2"
                                    Height="2"
                                    Margin="0,0,0,2"
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Bottom"
                                    Fill="White" />
                            </Grid>
                            <Border
                                BorderBrush="Red"
                                BorderThickness="2"
                                CornerRadius="2">
                                <AdornedElementPlaceholder />
                            </Border>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>

        <!--  Second error style: text on the right -->
        <Style x:Key="Styles.TextBox.ErrorStyle2" TargetType="{x:Type TextBox}">
            <Setter Property="Width" Value="250" />
            <Setter Property="Height" Value="25" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="Padding" Value="5,0" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel Orientation="Horizontal">
                            <AdornedElementPlaceholder x:Name="textBox" />
                            <Grid>
                                <TextBlock Margin="10 0 0 0" Width="130"
                                           Foreground="Red" TextWrapping="Wrap"
                                           Text="{Binding [0].ErrorContent}" />
                            </Grid>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                    <Setter Property="Background" Value="LightPink" />
                    <Setter Property="BorderBrush" Value="Red" />
                    <Setter Property="Foreground" Value="White" />
                </Trigger>
            </Style.Triggers>
        </Style>

        <Style TargetType="GroupBox">
            <Setter Property="Margin" Value="5" />
            <Setter Property="Padding" Value="2" />
            <Setter Property="BorderBrush" Value="#FF0078D7" />
            <Setter Property="BorderThickness" Value="2" />
            <Setter Property="Background" Value="#FFF0F0F0" />
            <Setter Property="Foreground" Value="#FF0078D7" />
            <Setter Property="FontWeight" Value="Bold" />
        </Style>

        <Style x:Key="Styles.Button.Common" TargetType="{x:Type Button}">
            <Setter Property="MinWidth" Value="75" />
            <Setter Property="MinHeight" Value="25" />
            <Setter Property="Background" Value="White" />
            <Setter Property="Foreground" Value="Black" />
        </Style>

        <Style
            x:Key="Styles.Button.Blue"
            BasedOn="{StaticResource ResourceKey=Styles.Button.Common}"
            TargetType="{x:Type Button}">
            <Setter Property="Background" Value="Green" />
            <Setter Property="Foreground" Value="White" />
        </Style>
    </Application.Resources>
</Application>

5. Effect Display

Through the above steps, we have a fully functional WPF application. It validates user input in real time and provides intuitive error prompts. When all properties pass validation, the submit button becomes enabled.

6. Source Code Sharing

For readers' convenience in learning and communication, all code in this article has been synchronized to Gitee and GitHub. Interested developers can access the source code via the following links:

7. Summary

Through the introduction and practice in this article, we have successfully applied FluentValidation in a C# WPF project, achieving comprehensive validation of ViewModel layer properties. This not only improves data security and accuracy but also provides a better interactive experience for users. We hope this article serves as a useful reference and inspiration for developers using FluentValidation in WPF projects.

References:

Keep Exploring

Related Reading

More Articles