{"id":23062219,"url":"https://github.com/dotnet9/fluentvalidationforwpf","last_synced_at":"2026-04-02T18:39:09.054Z","repository":{"id":108208558,"uuid":"222522415","full_name":"dotnet9/FluentValidationForWPF","owner":"dotnet9","description":"FluentValidation在C# WPF中的应用，同步博文见：https://codewf.com/2019/11/Uses-fluent-validation-in-WPF","archived":false,"fork":false,"pushed_at":"2024-01-26T08:03:41.000Z","size":56,"stargazers_count":16,"open_issues_count":1,"forks_count":6,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-08-28T22:50:10.960Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://codewf.com/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dotnet9.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-11-18T19:01:08.000Z","updated_at":"2025-04-09T10:15:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"800d0a5e-7459-4a34-89d7-db9ef5aacc6b","html_url":"https://github.com/dotnet9/FluentValidationForWPF","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dotnet9/FluentValidationForWPF","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet9%2FFluentValidationForWPF","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet9%2FFluentValidationForWPF/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet9%2FFluentValidationForWPF/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet9%2FFluentValidationForWPF/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dotnet9","download_url":"https://codeload.github.com/dotnet9/FluentValidationForWPF/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet9%2FFluentValidationForWPF/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31313095,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-16T03:25:06.427Z","updated_at":"2026-04-02T18:39:09.038Z","avatar_url":"https://github.com/dotnet9.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\ntitle: FluentValidation在C# WPF中的应用\nslug: Uses-fluent-validation-in-WPF\ndescription: 介绍FluentValidation的文章不少，其实它也可以用于WPF属性验证，本文主要也是讲解该组件在WPF中的使用\ndate: 2019-11-19 03:43:13\nlastmod: 2024-01-20 23:43:47\nauthor: 沙漠尽头的狼\ndraft: false\ncover: https://img1.dotnet9.com/2019/11/cover_01.png\ncategories: WPF\ncopyright: Original\ntags: WPF,FluentValiatoin\n---\n\n## 1. 引言\n\n在.NET开发领域，`FluentValidation`以其优雅、易扩展的特性成为开发者进行属性验证的首选工具。它不仅适用于Web开发，如MVC、Web API和ASP.NET CORE，同样也能完美集成在WPF应用程序中，提供强大的数据验证功能。本文将深入探讨如何在C# WPF项目中运用FluentValidation进行属性验证，并展示如何通过MVVM模式实现这一功能。\n\n## 2. 功能概览\n\n我们的目标是构建一个WPF应用程序，它能够通过FluentValidation实现以下验证功能：\n\n1. 验证ViewModel层的基本数据类型属性，如int、string等。\n2. 对ViewModel中的复杂属性进行验证，这包括对象属性的子属性以及集合属性。\n3. 提供两种直观的错误提示样式，以增强用户体验。\n\n先看实现效果图：\n\n![](https://img1.dotnet9.com/2019/11/0101.png)\n\n## 3. 解决问题与探索\n\n在调研过程中，我发现FluentValidation官方文档主要关注于Web应用的验证。对于WPF和复杂属性的验证，官方文档提供的示例有限。然而，通过深入研究和实践，我找到了将FluentValidation与WPF结合使用的有效方法，特别是针对复杂属性的验证。\n\n## 4. 开发步骤\n\n### 4.1. 创建工程、库引入\n\n首先，创建一个新的WPF项目，并引入`FluentValidation`库用于属性验证，以及`Prism.Wpf`库以简化MVVM模式的实现。\n\n```xml\n\u003cItemGroup\u003e\n  \u003cPackageReference Include=\"FluentValidation\" Version=\"11.9.0\" /\u003e\n  \u003cPackageReference Include=\"Prism.Wpf\" Version=\"9.0.271-pre\" /\u003e\n\u003c/ItemGroup\u003e\n```\n\n### 4.2. 创建实体类\n\n我创建了两个实体类：Student和Field，分别代表对象属性和集合项属性。这两个类都实现了IDataErrorInfo接口，以触发FluentValidation的验证机制。\n\n#### 4.2.1. 普通类 - Student\n\n学生类包含3个属性：名字、年龄、邮政编码。\n\n```C#\n/// \u003csummary\u003e\n///     学生实体\n///     继承BindableBase,即继承属性变化接口INotifyPropertyChanged\n///     实现IDataErrorInfo接口，用于FluentValidation验证，必须实现此接口\n/// \u003c/summary\u003e\npublic class Student : BindableBase, IDataErrorInfo\n{\n    private int _age;\n    private string? _name;\n    private string? _zip;\n    private readonly StudentValidator _validator = new();\n\n    public string? Name\n    {\n        get =\u003e _name;\n        set =\u003e SetProperty(ref _name, value);\n    }\n\n    public int Age\n    {\n        get =\u003e _age;\n        set =\u003e SetProperty(ref _age, value);\n    }\n\n    public string? Zip\n    {\n        get =\u003e _zip;\n        set =\u003e SetProperty(ref _zip, value);\n    }\n\n\n    public string this[string columnName]\n    {\n        get\n        {\n            var validateResult = _validator.Validate(this);\n            if (validateResult.IsValid)\n            {\n                return string.Empty;\n            }\n\n            var firstOrDefault =\n                validateResult.Errors.FirstOrDefault(error =\u003e error.PropertyName == columnName);\n            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;\n        }\n    }\n\n    public string Error\n    {\n        get\n        {\n            var validateResult = _validator.Validate(this);\n            if (validateResult.IsValid)\n            {\n                return string.Empty;\n            }\n\n            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x =\u003e x.ErrorMessage).ToArray());\n            return errors;\n        }\n    }\n}\n```\n\n上面关键代码在`public string this[string columnName]`：这里进行输入表单项的数据校验，`FluentValidation`调用就在这里，校验逻辑封装在`StudentValidator`，表单输入时会实时调用该处代码，`columnName`表示表单项的列名，就是ViewModel绑定的属性名。\n\n#### 4.2.2. 集合类 - Field\n\n此类用作ViewModel中的集合项使用，模拟动态表单数据校验，简单包含4个属性：字段名称、字段显示名称、数据类型、数据值，表单主要根据数据类型验证输入的数据值是否合法。同样此实体需要继承IDataErrorInfo接口，用于触发FluentValidation验证使用。\n\n```csharp\n/// \u003csummary\u003e\n///     扩展字段，用于生成动态表单\n///     继承BindableBase,即继承属性变化接口INotifyPropertyChanged\n///     实现IDataErrorInfo接口，用于FluentValidation验证，必须实现此接口\n/// \u003c/summary\u003e\npublic class Field : BindableBase, IDataErrorInfo\n{\n    private string _value;\n    private readonly FieldValidator _validator = new();\n\n\n    public Field(DataType type, string typeLabel, string name, string value)\n    {\n        Type = type;\n        TypeLabel = typeLabel;\n        Name = name;\n        Value = value;\n    }\n\n    /// \u003csummary\u003e\n    ///     数据类型\n    /// \u003c/summary\u003e\n    public DataType Type { get; set; }\n\n    /// \u003csummary\u003e\n    ///     数据类型名称\n    /// \u003c/summary\u003e\n    public string TypeLabel { get; set; }\n\n    /// \u003csummary\u003e\n    ///     名称\n    /// \u003c/summary\u003e\n    public string Name { get; set; }\n\n    /// \u003csummary\u003e\n    ///     值\n    /// \u003c/summary\u003e\n    public string Value\n    {\n        get =\u003e _value;\n        set =\u003e SetProperty(ref _value, value);\n    }\n\n    public string this[string columnName]\n    {\n        get\n        {\n            var validateResult = _validator.Validate(this);\n            if (validateResult.IsValid)\n            {\n                return string.Empty;\n            }\n\n            var firstOrDefault =\n                validateResult.Errors.FirstOrDefault(error =\u003e error.PropertyName == columnName);\n            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;\n        }\n    }\n\n    public string Error\n    {\n        get\n        {\n            var validateResult = _validator.Validate(this);\n            if (validateResult.IsValid)\n            {\n                return string.Empty;\n            }\n\n            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x =\u003e x.ErrorMessage).ToArray());\n            return errors;\n        }\n    }\n}\n\npublic enum DataType\n{\n    Text,\n    Number,\n    Date\n}\n```\n\n看上面代码，`public string this[string columnName]`代码处写法和`Student`类一样，只是`_validator`变量类型不同，前者为`StudentValidator`，这里是`FieldValidator`，下面我们看看这两个类怎么写。\n\n### 4.3. 创建验证器\n\n对于每个实体类，我都创建了一个对应的验证器类：StudentValidator和FieldValidator。这些验证器类继承自AbstractValidator，并在其中定义了验证规则。\n\n\u003e 注：验证属性的写法有两种：\n\u003e\n\u003e 1. 可以在实体属性上方添加特性（本文不作特别说明，百度文章介绍很多）；\n\u003e 2. 通过代码的形式添加，如下方，创建一个验证器类，继承自AbstractValidator，在此验证器构造函数中写规则验证属性，方便管理。\n\n本文使用第二种，通过创建`StudentValidator`和`FieldValidator`两个验证器类介绍。\n\n#### 4.3.1. StudentValidator\n\n这是学生验证器`StudentValidator`，需要继承`AbstractValidator`，泛型指定前面需要验证的实体类`Student`：\n\n```C#\npublic class StudentValidator : AbstractValidator\u003cStudent\u003e\n{\n    public StudentValidator()\n    {\n        RuleFor(vm =\u003e vm.Name)\n            .NotEmpty()\n            .WithMessage(\"请输入学生姓名！\")\n            .Length(5, 30)\n            .WithMessage(\"学生姓名长度限制在5到30个字符之间！\");\n\n        RuleFor(vm =\u003e vm.Age)\n            .GreaterThanOrEqualTo(0)\n            .WithMessage(\"学生年龄为整数！\")\n            .ExclusiveBetween(10, 150)\n            .WithMessage(\"请正确输入学生年龄(10-150)\");\n\n        RuleFor(vm =\u003e vm.Zip)\n            .NotEmpty()\n            .WithMessage(\"邮政编码不能为空！\")\n            .Must(BeAValidZip)\n            .WithMessage(\"邮政编码由六位数字组成。\");\n    }\n\n    private static bool BeAValidZip(string zip)\n    {\n        if (!string.IsNullOrEmpty(zip))\n        {\n            var regex = new Regex(@\"\\d{6}\");\n            return regex.IsMatch(zip);\n        }\n\n        return false;\n    }\n}\n```\n\n代码简单，使用到数字的大小和范围验证（见Age）、字符串不能为空和长度限制（见Name）、字符串正则表达式验证（见Zip）。\n\n#### 4.3.2. FieldValidator\n\n动态表单数据值校验器，同理需要继承`AbstractValidator`，泛型指定前面需要验证的实体类`Field`：：\n\n```csharp\npublic class FieldValidator : AbstractValidator\u003cField\u003e\n{\n    public FieldValidator()\n    {\n        RuleFor(field =\u003e field.Value)\n            .Must((field, value) =\u003e (field.Type == DataType.Text \u0026\u0026 !string.IsNullOrWhiteSpace(field.Value))\n                                    || (field.Type == DataType.Number \u0026\u0026 double.TryParse(field.Value, out var _))\n                                    || (field.Type == DataType.Date \u0026\u0026 DateTime.TryParse(field.Value, out var _)))\n            .WithMessage(\"1.文本不能为空；2.数字类型请填写数字；3.日志类型请填写日期类型\");\n    }\n}\n```\n\n这里写的简单了点：\n\n1. 文本数据类型，值不能为空；\n2. 数字数据类型，必须是`double`类型；\n3. 日期类型，必须能使用`DateTime`转换；\n\n本文只做简单演示，可按实际情况修改。\n\n#### 4.3.3. StudentViewModelValidator\n\n此外，我们还创建了一个StudentViewModelValidator，用于验证ViewModel层的属性。这个验证器能够处理基本数据类型、对象属性以及集合属性的验证。\n\n```csharp\npublic class StudentViewModelValidator : AbstractValidator\u003cStudentViewModel\u003e\n{\n    public StudentViewModelValidator()\n    {\n        RuleFor(vm =\u003e vm.Title)\n            .NotEmpty()\n            .WithMessage(\"标题长度不能为空！\")\n            .Length(5, 30)\n            .WithMessage(\"标题长度限制在5到30个字符之间！\");\n\n        RuleFor(vm =\u003e vm.CurrentStudent).SetValidator(new StudentValidator());\n\n        RuleForEach(vm =\u003e vm.Fields).SetValidator(new FieldValidator());\n    }\n}\n```\n\n1. `Title`用于关联验证基本数据类型(string类型)；\n2. `CurrentStudent`用于验证对象属性（Student类的实例），设置验证该属性时使用`StudentValidator`验证器；\n3. `Fields`用于验证集合属性(`ObservableCollection\u003cField\u003e`)，设置验证该属性子项时使用`FieldValidator`验证器，注意前面使用的`RuleForEach`表示关联集合中的项验证器。\n\n### 4.4. ViewModel层实现\n\n`StudentViewModel`与`Student`实体类结构类似，都需要实现`IDataErrorInfo`接口，该类由一个简单的`string`属性(`Title`)和一个复杂的`Student`对象属性(`CurrentStudent`)、集合属性`ObservableCollection\u003cField\u003e Fields`组成，代码如下：\n\n```C#\n/// \u003csummary\u003e\n///     视图ViewModel\n///     继承BindableBase,即继承属性变化接口INotifyPropertyChanged\n///     实现IDataErrorInfo接口，用于FluentValidation验证，必须实现此接口\n/// \u003c/summary\u003e\npublic class StudentViewModel : BindableBase, IDataErrorInfo\n{\n    private Student _currentStudent;\n    private string _title;\n\n    private readonly StudentViewModelValidator _validator;\n\n    public string Title\n    {\n        get =\u003e _title;\n        set =\u003e SetProperty(ref _title, value);\n    }\n\n    public Student CurrentStudent\n    {\n        get =\u003e _currentStudent;\n        set =\u003e SetProperty(ref _currentStudent, value);\n    }\n\n    public ObservableCollection\u003cField\u003e Fields { get; } = new();\n\n    private DelegateCommand _saveCommand;\n\n    public DelegateCommand SaveCommand =\u003e _saveCommand ??= new DelegateCommand(HandleSaveCommand,\n        HandleCanExecuteSaveCommand);\n\n    private DelegateCommand _cancelCommand;\n\n    public DelegateCommand CancelCommand =\u003e\n        _cancelCommand ??= new DelegateCommand(HandleCancelCommand, () =\u003e true);\n\n    public StudentViewModel()\n    {\n        _validator = new StudentViewModelValidator();\n        CurrentStudent = new Student\n        {\n            Name = \"李刚的儿\",\n            Age = 23\n        };\n        Fields.Add(new Field(DataType.Text, \"文本，比如：四川省成都市场\", \"地址\", \"\"));\n        Fields.Add(new Field(DataType.Number, \"数字，比如：12\", \"工龄\", \"\"));\n        Fields.Add(new Field(DataType.Date, \"时间，比如：2023-09-26 05:13:23\", \"培训时间\", \"\"));\n\n        PropertyChanged += Validate;\n        CurrentStudent.PropertyChanged += Validate;\n        foreach (var field in Fields)\n        {\n            field.PropertyChanged += Validate;\n        }\n    }\n\n    ~StudentViewModel()\n    {\n        PropertyChanged -= Validate;\n        CurrentStudent.PropertyChanged -= Validate;\n        foreach (var field in Fields)\n        {\n            field.PropertyChanged -= Validate;\n        }\n    }\n\n    private void Validate(object sender, PropertyChangedEventArgs e)\n    {\n        _isCanExecuteSaveCommand = _validator.Validate(this).IsValid;\n        SaveCommand.RaiseCanExecuteChanged();\n    }\n\n    private void HandleSaveCommand()\n    {\n        var validateResult = _validator.Validate(this);\n        if (validateResult.IsValid)\n        {\n            MessageBox.Show(\"看到我说明验证成功！\");\n        }\n        else\n        {\n            var errorMsg = string.Join(Environment.NewLine,\n                validateResult.Errors.Select(x =\u003e x.ErrorMessage).ToArray());\n            MessageBox.Show($\"慌啥子嘛，你再检查下输入噻：\\r\\n{errorMsg}\");\n        }\n    }\n\n    private bool _isCanExecuteSaveCommand;\n\n    private bool HandleCanExecuteSaveCommand()\n    {\n        return _isCanExecuteSaveCommand;\n    }\n\n    private void HandleCancelCommand()\n    {\n        MessageBox.Show(\"我啥都不做，退休了\");\n    }\n\n    public string this[string columnName]\n    {\n        get\n        {\n            var validateResult = _validator.Validate(this);\n            if (validateResult.IsValid)\n            {\n                return string.Empty;\n            }\n\n            var firstOrDefault =\n                validateResult.Errors.FirstOrDefault(error =\u003e error.PropertyName == columnName);\n            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;\n        }\n    }\n\n    public string Error\n    {\n        get\n        {\n            var validateResult = _validator.Validate(this);\n            if (validateResult.IsValid)\n            {\n                return string.Empty;\n            }\n\n            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x =\u003e x.ErrorMessage).ToArray());\n            return errors;\n        }\n    }\n}\n```\n\n`ViewModel`属性验证和`Student`及`Field`类似，这里我加上了保存(`SaveCommand`)和取消(`CancelCommand`)两个命令，其中保存命令需要所有属性验证通过才可用，通过注册属性的变化事件`PropertyChanged`，在变化事件处理程序中验证：\n\n```csharp\nPropertyChanged += Validate;\nCurrentStudent.PropertyChanged += Validate;\nforeach (var field in Fields)\n{\n    field.PropertyChanged += Validate;\n}\n```\n\n```csharp\nprivate void Validate(object sender, PropertyChangedEventArgs e)\n{\n    _isCanExecuteSaveCommand = _validator.Validate(this).IsValid;\n    SaveCommand.RaiseCanExecuteChanged();\n}\n```\n\n### 4.5. 视图层实现\n\n在视图层，我创建了一个用户控件StudentView，用于显示输入表单和验证结果。通过绑定ViewModel层的属性和命令，视图层能够与ViewModel层进行交互，并实时显示验证错误。这里比较简单，提供简单属性标题(Title)、复杂属性(包括学生姓名(CurrentStudent.Name)、学生年龄( CurrentStudent .Age)、学生邮政编码( CurrentStudent .Zip)）验证、集合属性验证，xaml代码如下：\n\n```xml\n\u003cUserControl\n    x:Class=\"WpfFluentValidation.Views.StudentView\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n    xmlns:models=\"clr-namespace:WpfFluentValidation.Models\"\n    xmlns:vm=\"clr-namespace:WpfFluentValidation.ViewModels\"\n    mc:Ignorable=\"d\" Padding=\"10\"\u003e\n    \u003cUserControl.DataContext\u003e\n        \u003cvm:StudentViewModel /\u003e\n    \u003c/UserControl.DataContext\u003e\n    \u003cGrid\u003e\n        \u003cGrid.RowDefinitions\u003e\n            \u003cRowDefinition Height=\"*\" /\u003e\n            \u003cRowDefinition Height=\"50\" /\u003e\n        \u003c/Grid.RowDefinitions\u003e\n        \u003cScrollViewer HorizontalScrollBarVisibility=\"Hidden\" VerticalScrollBarVisibility=\"Auto\"\u003e\n            \u003cGrid\u003e\n                \u003cGrid.RowDefinitions\u003e\n                    \u003cRowDefinition Height=\"Auto\" /\u003e\n                    \u003cRowDefinition Height=\"Auto\" /\u003e\n                    \u003cRowDefinition Height=\"*\" /\u003e\n                \u003c/Grid.RowDefinitions\u003e\n\n                \u003cGroupBox Header=\"ViewModel直接属性验证\"\u003e\n                    \u003cStackPanel\u003e\n                        \u003cLabel Content=\"标题：\" /\u003e\n                        \u003cTextBox Style=\"{StaticResource Styles.TextBox.ErrorStyle1}\"\n                                 Text=\"{Binding Title, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}\" /\u003e\n                    \u003c/StackPanel\u003e\n                \u003c/GroupBox\u003e\n\n                \u003cGroupBox Grid.Row=\"1\" Header=\"ViewModel对象属性CurrentStudent的属性验证\"\u003e\n                    \u003cStackPanel\u003e\n                        \u003cStackPanel\u003e\n                            \u003cLabel Content=\"姓名：\" /\u003e\n                            \u003cTextBox Style=\"{StaticResource Styles.TextBox.ErrorStyle2}\"\n                                     Text=\"{Binding CurrentStudent.Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}\" /\u003e\n                        \u003c/StackPanel\u003e\n                        \u003cStackPanel\u003e\n                            \u003cLabel Content=\"年龄：\" /\u003e\n                            \u003cTextBox Style=\"{StaticResource Styles.TextBox.ErrorStyle2}\"\n                                     Text=\"{Binding CurrentStudent.Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}\" /\u003e\n                        \u003c/StackPanel\u003e\n                        \u003cStackPanel\u003e\n                            \u003cLabel Content=\"邮编：\" /\u003e\n                            \u003cTextBox Style=\"{StaticResource Styles.TextBox.ErrorStyle2}\"\n                                     Text=\"{Binding CurrentStudent.Zip, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}\" /\u003e\n                        \u003c/StackPanel\u003e\n                    \u003c/StackPanel\u003e\n                \u003c/GroupBox\u003e\n\n                \u003cGroupBox Grid.Row=\"2\" Header=\"ViewModel集合属性Fields的属性验证\"\u003e\n                    \u003cItemsControl ItemsSource=\"{Binding Fields}\"\u003e\n                        \u003cItemsControl.ItemTemplate\u003e\n                            \u003cDataTemplate DataType=\"{x:Type models:Field}\"\u003e\n                                \u003cBorder Padding=\"10\"\u003e\n                                    \u003cGrid\u003e\n                                        \u003cGrid.RowDefinitions\u003e\n                                            \u003cRowDefinition Height=\"Auto\" /\u003e\n                                            \u003cRowDefinition Height=\"Auto\" /\u003e\n                                        \u003c/Grid.RowDefinitions\u003e\n                                        \u003cTextBlock Margin=\"0,0,0,5\"\u003e\n                                            \u003cRun Text=\"{Binding Name}\" /\u003e\n                                            \u003cRun Text=\"(\" /\u003e\n                                            \u003cRun Text=\"{Binding TypeLabel}\" /\u003e\n                                            \u003cRun Text=\")\" /\u003e\n                                        \u003c/TextBlock\u003e\n                                        \u003cTextBox Grid.Row=\"1\" Style=\"{StaticResource Styles.TextBox.ErrorStyle2}\"\n                                                 Text=\"{Binding Value, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}\" /\u003e\n                                    \u003c/Grid\u003e\n                                \u003c/Border\u003e\n                            \u003c/DataTemplate\u003e\n                        \u003c/ItemsControl.ItemTemplate\u003e\n                    \u003c/ItemsControl\u003e\n                \u003c/GroupBox\u003e\n            \u003c/Grid\u003e\n        \u003c/ScrollViewer\u003e\n\n        \u003cStackPanel Grid.Row=\"1\" HorizontalAlignment=\"Right\" Orientation=\"Horizontal\"\u003e\n            \u003cButton Content=\"取消\" Command=\"{Binding CancelCommand}\" Style=\"{StaticResource Styles.Button.Common}\"\n                    Margin=\"0 3 40 3\" /\u003e\n            \u003cButton Content=\"提交\" Command=\"{Binding SaveCommand}\" Style=\"{StaticResource Styles.Button.Blue}\"\n                    Margin=\"0 3 10 3\" /\u003e\n        \u003c/StackPanel\u003e\n    \u003c/Grid\u003e\n\u003c/UserControl\u003e\n```\n\n### 4.6. 错误提示样式\n\n为了提升用户体验，我定义了两种错误提示样式：一种是通过红色图标提示输入框旁边的错误，另一种是在输入框右侧显示错误文字。这些样式定义在App.xaml中，并可以在整个应用程序中复用。\n\n```xml\n\u003cApplication\n    x:Class=\"WpfFluentValidation.App\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    StartupUri=\"MainWindow.xaml\"\u003e\n    \u003cApplication.Resources\u003e\n        \u003cStyle TargetType=\"StackPanel\"\u003e\n            \u003cSetter Property=\"Margin\" Value=\"0,5\" /\u003e\n        \u003c/Style\u003e\n        \u003c!--  第一种错误样式，红色边框  --\u003e\n        \u003cStyle x:Key=\"Styles.TextBox.ErrorStyle1\" TargetType=\"{x:Type TextBox}\"\u003e\n            \u003cSetter Property=\"Width\" Value=\"250\" /\u003e\n            \u003cSetter Property=\"Height\" Value=\"25\" /\u003e\n            \u003cSetter Property=\"HorizontalAlignment\" Value=\"Left\" /\u003e\n            \u003cSetter Property=\"Validation.ErrorTemplate\"\u003e\n                \u003cSetter.Value\u003e\n                    \u003cControlTemplate\u003e\n                        \u003cDockPanel\u003e\n                            \u003cGrid\n                                Width=\"16\"\n                                Height=\"16\"\n                                Margin=\"3,0,0,0\"\n                                VerticalAlignment=\"Center\"\n                                DockPanel.Dock=\"Right\"\u003e\n                                \u003cEllipse\n                                    Width=\"16\"\n                                    Height=\"16\"\n                                    Fill=\"Red\" /\u003e\n                                \u003cEllipse\n                                    Width=\"3\"\n                                    Height=\"8\"\n                                    Margin=\"0,2,0,0\"\n                                    HorizontalAlignment=\"Center\"\n                                    VerticalAlignment=\"Top\"\n                                    Fill=\"White\" /\u003e\n                                \u003cEllipse\n                                    Width=\"2\"\n                                    Height=\"2\"\n                                    Margin=\"0,0,0,2\"\n                                    HorizontalAlignment=\"Center\"\n                                    VerticalAlignment=\"Bottom\"\n                                    Fill=\"White\" /\u003e\n                            \u003c/Grid\u003e\n                            \u003cBorder\n                                BorderBrush=\"Red\"\n                                BorderThickness=\"2\"\n                                CornerRadius=\"2\"\u003e\n                                \u003cAdornedElementPlaceholder /\u003e\n                            \u003c/Border\u003e\n                        \u003c/DockPanel\u003e\n                    \u003c/ControlTemplate\u003e\n                \u003c/Setter.Value\u003e\n            \u003c/Setter\u003e\n            \u003cStyle.Triggers\u003e\n                \u003cTrigger Property=\"Validation.HasError\" Value=\"true\"\u003e\n                    \u003cSetter Property=\"ToolTip\"\n                            Value=\"{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}\" /\u003e\n                \u003c/Trigger\u003e\n            \u003c/Style.Triggers\u003e\n        \u003c/Style\u003e\n\n        \u003c!--  第二种错误样式，右键文字提示  --\u003e\n        \u003cStyle x:Key=\"Styles.TextBox.ErrorStyle2\" TargetType=\"{x:Type TextBox}\"\u003e\n            \u003cSetter Property=\"Width\" Value=\"250\" /\u003e\n            \u003cSetter Property=\"Height\" Value=\"25\" /\u003e\n            \u003cSetter Property=\"VerticalContentAlignment\" Value=\"Center\" /\u003e\n            \u003cSetter Property=\"Padding\" Value=\"5,0\" /\u003e\n            \u003cSetter Property=\"HorizontalAlignment\" Value=\"Left\" /\u003e\n            \u003cSetter Property=\"Validation.ErrorTemplate\"\u003e\n                \u003cSetter.Value\u003e\n                    \u003cControlTemplate\u003e\n                        \u003cStackPanel Orientation=\"Horizontal\"\u003e\n                            \u003cAdornedElementPlaceholder x:Name=\"textBox\" /\u003e\n                            \u003cGrid\u003e\n                                \u003cTextBlock Margin=\"10 0 0 0\" Width=\"130\"\n                                           Foreground=\"Red\" TextWrapping=\"Wrap\"\n                                           Text=\"{Binding [0].ErrorContent}\" /\u003e\n                            \u003c/Grid\u003e\n                        \u003c/StackPanel\u003e\n                    \u003c/ControlTemplate\u003e\n                \u003c/Setter.Value\u003e\n            \u003c/Setter\u003e\n            \u003cStyle.Triggers\u003e\n                \u003cTrigger Property=\"Validation.HasError\" Value=\"true\"\u003e\n                    \u003cSetter Property=\"ToolTip\"\n                            Value=\"{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}\" /\u003e\n                    \u003cSetter Property=\"Background\" Value=\"LightPink\" /\u003e\n                    \u003cSetter Property=\"BorderBrush\" Value=\"Red\" /\u003e\n                    \u003cSetter Property=\"Foreground\" Value=\"White\" /\u003e\n                \u003c/Trigger\u003e\n            \u003c/Style.Triggers\u003e\n        \u003c/Style\u003e\n\n        \u003cStyle TargetType=\"GroupBox\"\u003e\n            \u003cSetter Property=\"Margin\" Value=\"5\" /\u003e\n            \u003cSetter Property=\"Padding\" Value=\"2\" /\u003e\n            \u003cSetter Property=\"BorderBrush\" Value=\"#FF0078D7\" /\u003e\n            \u003cSetter Property=\"BorderThickness\" Value=\"2\" /\u003e\n            \u003cSetter Property=\"Background\" Value=\"#FFF0F0F0\" /\u003e\n            \u003cSetter Property=\"Foreground\" Value=\"#FF0078D7\" /\u003e\n            \u003cSetter Property=\"FontWeight\" Value=\"Bold\" /\u003e\n        \u003c/Style\u003e\n\n        \u003cStyle x:Key=\"Styles.Button.Common\" TargetType=\"{x:Type Button}\"\u003e\n            \u003cSetter Property=\"MinWidth\" Value=\"75\" /\u003e\n            \u003cSetter Property=\"MinHeight\" Value=\"25\" /\u003e\n            \u003cSetter Property=\"Background\" Value=\"White\" /\u003e\n            \u003cSetter Property=\"Foreground\" Value=\"Black\" /\u003e\n        \u003c/Style\u003e\n\n        \u003cStyle\n            x:Key=\"Styles.Button.Blue\"\n            BasedOn=\"{StaticResource ResourceKey=Styles.Button.Common}\"\n            TargetType=\"{x:Type Button}\"\u003e\n            \u003cSetter Property=\"Background\" Value=\"Green\" /\u003e\n            \u003cSetter Property=\"Foreground\" Value=\"White\" /\u003e\n        \u003c/Style\u003e\n    \u003c/Application.Resources\u003e\n\u003c/Application\u003e\n```\n\n## 5. 效果展示\n\n通过上述步骤的实现，我们得到了一个功能完善的WPF应用程序。它能够根据用户输入实时进行验证，并提供直观的错误提示。当所有属性都验证通过时，提交按钮将变为可用状态。\n\n![](https://img1.dotnet9.com/2019/11/0103.gif)\n\n## 6. 源码分享\n\n为了方便读者学习和交流，本文将所有代码同步到了Gitee和Github平台上。欢迎感兴趣的开发者访问以下链接获取源码：\n\n- gitee: https://gitee.com/dotnet9/FluentValidationForWpf\n- github： https://github.com/dotnet9/FluentValidationForWPF\n\n## 7. 总结\n\n通过本文的介绍和实践，我们成功将FluentValidation应用于C# WPF项目中，实现了对ViewModel层属性的全面验证。这不仅提升了数据的安全性和准确性，也为用户提供了更好的交互体验。希望本文能对广大开发者在WPF项目中使用FluentValidation提供有益的参考和启示。\n\n参考：\n\n- FluentValidation官网： https://fluentvalidation.net/ ","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdotnet9%2Ffluentvalidationforwpf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdotnet9%2Ffluentvalidationforwpf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdotnet9%2Ffluentvalidationforwpf/lists"}