FluentValidation在C# WPF中的應用

FluentValidation在C# WPF中的應用

本文將深入探討如何在C# WPF專案中運用FluentValidation進行屬性驗證,並展示如何透過MVVM模式實現這一功能。

最後更新 2024/1/25 上午5:17
沙漠尽头的狼
預計閱讀 18 分鐘
分類
WPF
標籤
.NET C# WPF MVVM FluentValidation

1. 引言

在 .NET 開發領域中,FluentValidation 以其優雅、易擴展的特性成為開發者進行屬性驗證的首選工具。它不僅適用於 Web 開發,如 MVC、Web API 和 ASP.NET CORE,同樣也能完美整合在 WPF 應用程式中,提供強大的資料驗證功能。本文將深入探討如何在 C# WPF 專案中運用 FluentValidation 進行屬性驗證,並展示如何透過 MVVM 模式實現此功能。

2. 功能概覽

我們的目標是建構一個 WPF 應用程式,它能透過 FluentValidation 實現以下驗證功能:

  1. 驗證 ViewModel 層的基本資料型別屬性,如 int、string 等。
  2. 對 ViewModel 中的複雜屬性進行驗證,這包括物件屬性的子屬性以及集合屬性。
  3. 提供兩種直觀的錯誤提示樣式,以增強使用者體驗。

先看實作效果圖:

3. 解決問題與探索

在調研過程中,我發現 FluentValidation 官方文件主要關注於 Web 應用的驗證。對於 WPF 和複雜屬性的驗證,官方文件提供的範例有限。然而,透過深入研究和實踐,我找到了將 FluentValidation 與 WPF 結合使用的有效方法,特別是針對複雜屬性的驗證。

4. 開發步驟

4.1. 建立專案、引入套件

首先,建立一個新的 WPF 專案,並引入 FluentValidation 套件用於屬性驗證,以及 Prism.Wpf 套件以簡化 MVVM 模式的實作。

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

4.2. 建立實體類別

我建立了兩個實體類別:Student 和 Field,分別代表物件屬性和集合項屬性。這兩個類別都實作了 IDataErrorInfo 介面:

  1. IDataErrorInfo 介面常用於提供實體資料驗證的錯誤資訊。這個介面包含兩個成員:一個索引子(this[string columnName])和一個 Error 屬性。索引子用於依屬性名稱提供錯誤資訊,而 Error 屬性則用於提供整個實體的錯誤概述。
  2. 兩個實體類別和另外在後續提及的 ViewModel 中也實作 IDataErrorInfo 介面,並在 this[string columnName] 索引子和 Error 屬性中使用 FluentValidation 來驗證屬性。

4.2.1. 普通類別 - Student

學生類別包含 5 個屬性:名字、年齡、郵遞區號、最小值與最大值,其中最小值與最大值涉及關聯驗證,即最小值變更後通知最大值驗證,反之亦然。

/// <summary>
///     學生實體
///     繼承 BindableBase,即繼承屬性變更介面 INotifyPropertyChanged
///     實作 IDataErrorInfo 介面,用於 FluentValidation 驗證,必須實作此介面
/// </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);

            // 關聯更新最大值驗證
            RaisePropertyChanged(nameof(MaxValue));
        }
    }

    private int _maxValue;

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

            // 關聯更新最小值驗證
            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;
        }
    }
}

上面關鍵程式碼在 public string this[string columnName]:這裡進行輸入表單項目的資料校驗,FluentValidation 呼叫就在這裡,校驗邏輯封裝在 StudentValidator,表單輸入時會即時呼叫此處程式碼,columnName 表示表單項目的欄位名稱,就是 View 繫結的屬性名稱。

4.2.2. 集合類別 - Field

此類別用作 ViewModel 中的集合項目使用,模擬動態表單資料校驗,簡單包含 4 個屬性:欄位名稱、欄位顯示名稱、資料型別、資料值,表單主要根據資料型別驗證輸入的資料值是否合法。同樣此實體需要繼承 IDataErrorInfo 介面,用於觸發 FluentValidation 驗證使用。

/// <summary>
///     擴充欄位,用於產生動態表單
///     繼承 BindableBase,即繼承屬性變更介面 INotifyPropertyChanged
///     實作 IDataErrorInfo 介面,用於 FluentValidation 驗證,必須實作此介面
/// </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>
    ///     資料型別
    /// </summary>
    public DataType Type { get; set; }

    /// <summary>
    ///     資料型別名稱
    /// </summary>
    public string TypeLabel { get; set; }

    /// <summary>
    ///     名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    ///     值
    /// </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
}

看上面程式碼,public string this[string columnName] 程式碼處寫法和 Student 類別一樣,只是 _validator 變數型別不同,前者為 StudentValidator,這裡是 FieldValidator,下面我們看看這兩個類別怎麼寫。

4.3. 建立驗證器

對於每個實體類別,我都建立了一個對應的驗證器類別:StudentValidatorFieldValidator。這些驗證器類別繼承自 AbstractValidator,並在其中定義了驗證規則。驗證屬性的寫法有兩種:

  1. 可以在實體屬性上方加入特性(本文不作特別說明,百度文章介紹很多);

  2. 透過程式碼的形式加入,如下方,建立一個驗證器類別,繼承自 AbstractValidator,在此驗證器建構函式中寫規則驗證屬性,方便管理。

本文使用第二種,下面透過建立 StudentValidatorFieldValidator 兩個驗證器類別介紹。

4.3.1. StudentValidator

這是學生驗證器 StudentValidator,需要繼承 AbstractValidator,泛型指定前面需要驗證的實體類別 Student

public class StudentValidator : AbstractValidator<Student>
{
    public StudentValidator()
    {
        RuleFor(vm => vm.Name)
            .NotEmpty()
            .WithMessage("請輸入學生姓名!")
            .Length(5, 30)
            .WithMessage("學生姓名長度限制在5到30個字元之間!");

        RuleFor(vm => vm.Age)
            .GreaterThanOrEqualTo(0)
            .WithMessage("學生年齡為整數!")
            .ExclusiveBetween(10, 150)
            .WithMessage("請正確輸入學生年齡(10-150)");

        _ = RuleFor(vm => vm.Zip)
            .NotEmpty()
            .WithMessage("郵遞區號不能為空!")
            .Must(BeAValidZip)
            .WithMessage("郵遞區號由六位數字組成。");

        RuleFor(model => model.MinValue).Must((model, minValue) => minValue < model.MaxValue).WithMessage("最小值應該小於最大值");

        RuleFor(model => model.MaxValue).Must((model, maxValue) => maxValue > model.MinValue).WithMessage("最大值應該大於最小值");
    }

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

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

程式碼簡單,使用到數字的大小和範圍驗證(見 Age)、字串不能為空和長度限制(見 Name)、字串正規表達式驗證(見 Zip)、多屬性關聯驗證(最小值與最大值,這裡配合屬性 set 時通知其他屬性驗證通知 RaisePropertyChanged(nameof(MaxValue));)。

4.3.2. FieldValidator

動態表單資料值驗證器,同理需要繼承 AbstractValidator,泛型指定前面需要驗證的實體類別 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.文字不能為空;2.數字類型請填寫數字;3.日期類型請填寫日期類型");
    }
}

這裡寫得簡單了點:

  1. 文字資料型別,值不能為空;
  2. 數字資料型別,必須是 double 型別;
  3. 日期型別,必須能使用 DateTime 轉換;

本文只做簡單示範,多種資料型別放 Must 方法中做統一驗證,驗證出錯給出統一的提示資訊,讀者可依實際情況修改。

4.3.3. StudentViewModelValidator

此外,我還建立了一個 StudentViewModelValidator,用於驗證 ViewModel 層的屬性。這個驗證器能夠處理基本資料型別、物件屬性以及集合屬性的驗證。

public class StudentViewModelValidator : AbstractValidator<StudentViewModel>
{
    public StudentViewModelValidator()
    {
        RuleFor(vm => vm.Title)
            .NotEmpty()
            .WithMessage("標題長度不能為空!")
            .Length(5, 30)
            .WithMessage("標題長度限制在5到30個字元之間!");

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

        RuleForEach(vm => vm.Fields).SetValidator(new FieldValidator());
    }
}
  1. Title 用於關聯驗證基本資料型別(string 型別);
  2. CurrentStudent 用於驗證物件屬性(Student 類別的實例),設定驗證該屬性時使用 StudentValidator 驗證器;
  3. Fields 用於驗證集合屬性(ObservableCollection<Field>),設定驗證該屬性子項時使用 FieldValidator 驗證器,注意前面使用的 RuleForEach 表示關聯集合中的項驗證器。

4.4. ViewModel 層實作

StudentViewModelStudent 實體類別結構類似,都需要實作 IDataErrorInfo 介面,該類別由一個簡單的 string 屬性(Title)和一個複雜的 Student 物件屬性(CurrentStudent)、集合屬性 ObservableCollection<Field> Fields 組成,程式碼如下:

/// <summary>
///     檢視 ViewModel
///     繼承 BindableBase,即繼承屬性變更介面 INotifyPropertyChanged
///     實作 IDataErrorInfo 介面,用於 FluentValidation 驗證,必須實作此介面
/// </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 = "李剛的兒",
            Age = 23
        };
        Fields.Add(new Field(DataType.Text, "文字,比如:四川省成都市", "地址", ""));
        Fields.Add(new Field(DataType.Number, "數字,比如:12", "工齡", ""));
        Fields.Add(new Field(DataType.Date, "時間,比如:2023-09-26 05:13:23", "培訓時間", ""));

        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("看到我說明驗證成功!");
        }
        else
        {
            var errorMsg = string.Join(Environment.NewLine,
                validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            MessageBox.Show($"慌啥子嘛,你再檢查下輸入噻:\r\n{errorMsg}");
        }
    }

    private bool _isCanExecuteSaveCommand;

    private bool HandleCanExecuteSaveCommand()
    {
        return _isCanExecuteSaveCommand;
    }

    private void HandleCancelCommand()
    {
        MessageBox.Show("我啥都不做,退休了");
    }

    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;
        }
    }
}

ViewModel 屬性驗證和 StudentField 類似,這裡我加上了儲存(SaveCommand)和取消(CancelCommand)兩個命令,其中儲存命令需要所有屬性驗證通過才可用,透過註冊屬性的變更事件 PropertyChanged,在變更事件處理程式中驗證:

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. 檢視層實作

在檢視層,我建立了一個使用者控制項 StudentView,用於顯示輸入表單和驗證結果。透過繫結 ViewModel 層的屬性和命令,檢視層能夠與 ViewModel 層進行互動,並即時顯示驗證錯誤。這裡比較簡單,提供簡單屬性標題(Title)、複雜屬性(包括學生姓名(CurrentStudent.Name)、學生年齡(CurrentStudent.Age)、學生郵遞區號(CurrentStudent.Zip)、最小值(CurrentStudent.MinValue)、最大值(CurrentStudent.MaxValue))驗證、集合屬性驗證(Fields),xaml 程式碼如下:

<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 直接屬性驗證">
                    <StackPanel>
                        <Label Content="標題:" />
                        <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle1}"
                                 Text="{Binding Title, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                    </StackPanel>
                </GroupBox>

                <GroupBox Grid.Row="1" Header="ViewModel 物件屬性 CurrentStudent 的屬性驗證">
                    <StackPanel>
                        <StackPanel>
                            <Label Content="姓名:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="年齡:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="郵遞區號:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Zip, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="最小值:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.MinValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="最大值:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.MaxValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                    </StackPanel>
                </GroupBox>

                <GroupBox Grid.Row="2" Header="ViewModel 集合屬性 Fields 的屬性驗證">
                    <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="取消" Command="{Binding CancelCommand}" Style="{StaticResource Styles.Button.Common}"
                    Margin="0 3 40 3" />
            <Button Content="提交" Command="{Binding SaveCommand}" Style="{StaticResource Styles.Button.Blue}"
                    Margin="0 3 10 3" />
        </StackPanel>
    </Grid>
</UserControl>

4.6. 錯誤提示樣式

為了提升使用者體驗,我定義了兩種錯誤提示樣式:一種是透過紅色圖示提示輸入框旁邊的錯誤,另一種是在輸入框右側顯示錯誤文字。這些樣式定義在 App.xaml 中,並可以在整個應用程式中重複使用。

<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>
        <!--  第一種錯誤樣式,紅色邊框  -->
        <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>

        <!--  第二種錯誤樣式,右鍵文字提示  -->
        <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. 效果展示

透過上述步驟的實作,我們得到了一個功能完善的 WPF 應用程式。它能根據使用者輸入即時進行驗證,並提供直觀的錯誤提示。當所有屬性都驗證通過時,提交按鈕將變為可用狀態。

6. 原始碼分享

為了方便讀者學習和交流,本文將所有程式碼同步到了 Gitee 和 GitHub 平台上。歡迎有興趣的開發者造訪以下連結取得原始碼:

7. 結論

透過本文的介紹和實踐,我們成功將 FluentValidation 應用於 C# WPF 專案中,實現了對 ViewModel 層屬性的全面驗證。這不僅提升了資料的安全性和準確性,也為使用者提供了更好的互動體驗。希望本文能對廣大開發者在 WPF 專案中使用 FluentValidation 提供有益的參考和啟發。

參考:

繼續探索

延伸閱讀

更多文章