WPF应用中表单验证错误无法在UI显示的问题求助
WPF .NET8 文本生成应用验证错误不显示问题
我是WPF新手,正在开发基于.NET8的文本文件生成小应用。UI有两个必填字段,其中一个仅接受大于0的数字。我用了Fluent Validation、MVVM模式,ViewModel实现了INotifyDataErrorInfo和INotifyPropertyChanged接口,希望在按钮点击、失去焦点等场景触发验证。调试时发现验证错误已存入字典,但UI无法显示,求指点问题原因。
模型代码(TargetFile.cs)
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ABCText.FileGeneratorUI.Models { public class TargetFile : INotifyPropertyChanged { private string _size; private string _fileName; public string Size { get => _size; set { _size = value; OnPropertyChanged(nameof(Size)); } } public string FileName { get => _fileName; set { _fileName = value; OnPropertyChanged(nameof(FileName)); } } public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
视图模型代码(MainViewModel.cs)
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using ABCText.FileGeneratorUI.Models; using ABCText.FileGeneratorUI.Validation; using FluentValidation.Results; namespace ABCText.FileGeneratorUI.ViewModels { public class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo { private TargetFile _targetFile; private readonly TargetFileValidator _validator; private Dictionary<string, List<string>> _validationErrors = new Dictionary<string, List<string>>(); public MainViewModel() { _targetFile = new TargetFile(); _validator = new TargetFileValidator(); _targetFile.PropertyChanged += OnTargetFilePropertyChanged; } public TargetFile TargetFile { get => _targetFile; set { if (_targetFile != null) _targetFile.PropertyChanged -= OnTargetFilePropertyChanged; _targetFile = value; if (_targetFile != null) _targetFile.PropertyChanged += OnTargetFilePropertyChanged; OnPropertyChanged(nameof(TargetFile)); Validate(); } } public bool HasErrors => _validationErrors.Any(); public void Validate() { var validationResult = _validator.Validate(_targetFile); _validationErrors.Clear(); if (!validationResult.IsValid) { foreach (var error in validationResult.Errors) { if (!_validationErrors.ContainsKey(error.PropertyName)) _validationErrors.Add(error.PropertyName, new List<string>()); _validationErrors[error.PropertyName].Add(error.ErrorMessage); ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(error.PropertyName)); } } } protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } protected void OnTargetFilePropertyChanged(object? sender, PropertyChangedEventArgs e) { Validate(); } public IEnumerable GetErrors(string? propertyName) { return _validationErrors.TryGetValue(propertyName ?? string.Empty, out var errors) ? errors : null; } public event PropertyChangedEventHandler? PropertyChanged; public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; } }
视图XAML代码(MainWindow.xaml)
<Window x:Class="ABCText.FileGeneratorUI.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:ABCText.FileGeneratorUI.Views" xmlns:contract="clr-namespace:ABCText.Contract;assembly=ABCText.Contract" xmlns:ext="clr-namespace:ABCText.FileGeneratorUI.Extensions" xmlns:vms="clr-namespace:ABCText.FileGeneratorUI.ViewModels" d:DataContext="{d:DesignInstance Type=vms:MainViewModel}" mc:Ignorable="d" Title="ABC Text File Generator" Height="300" Width="500"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="60"/> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="10,10,10,10"> <TextBlock Text="ABC Text File Generator" FontSize="26" FontFamily="Segoe UI Light" VerticalAlignment="Top" TextWrapping="Wrap"/> <StackPanel Orientation="Horizontal" Margin="0,0,0,10"> <TextBlock Text="Target approx. file size:" Margin="0,0,10,0"/> <StackPanel Orientation="Vertical" Margin="0,0,5,0" MinHeight="50"> <TextBox Text="{Binding TargetFile.Size, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" Width="200"> <Validation.ErrorTemplate> <ControlTemplate> <StackPanel> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder x:Name="FileSizeTextBox" /> </Border> <ItemsControl ItemsSource="{Binding (Validation.Errors), RelativeSource={RelativeSource Mode=TemplatedParent}}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </StackPanel> </ControlTemplate> </Validation.ErrorTemplate> </TextBox> </StackPanel> <ComboBox x:Name="fileSizeTypeComboBox" ItemsSource="{ext:EnumBindingSource contract:FileSizeTypeEnum}" SelectedIndex="0" MaxHeight="19" VerticalAlignment="Top"/> </StackPanel> <StackPanel Orientation="Horizontal" Margin="0,0,0,10"> <TextBlock Text="Target file name:" Margin="0,0,10,0"/> <StackPanel Orientation="Vertical" Margin="0,0,5,0" MinHeight="50"> <TextBox Text="{Binding TargetFile.FileName, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" Width="200"> <Validation.ErrorTemplate> <ControlTemplate> <StackPanel> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder x:Name="FileNameTextBox"/> </Border> <ItemsControl ItemsSource="{Binding (Validation.Errors), RelativeSource={RelativeSource Mode=TemplatedParent}}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </StackPanel> </ControlTemplate> </Validation.ErrorTemplate> </TextBox> </StackPanel> <TextBlock Text=".txt" Margin="0,0,10,0"/> </StackPanel> </StackPanel> <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="10,10,10,10"> <Button Content="Generate" FontSize="20" FontWeight="SemiBold" Padding="10,5,10,5" x:Name="GenerateButton" Click="GenerateButton_Click" Background="LightSeaGreen"/> </StackPanel> </Grid> </Window>
视图代码后置(MainWindow.xaml.cs)
using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using ABCText.FileGeneratorUI.ViewModels; using ABCText.FileGeneratorUI.Models; namespace ABCText.FileGeneratorUI.Views { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new MainViewModel(); } private void GenerateButton_Click(object sender, RoutedEventArgs e) { if (this.DataContext is MainViewModel viewModel) { viewModel.Validate(); } } } }
验证器代码(TargetFileValidator.cs)
using ABCText.FileGeneratorUI.Models; using FluentValidation; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ABCText.FileGeneratorUI.Validation { public class TargetFileValidator : AbstractValidator<TargetFile> { public TargetFileValidator() { RuleFor(x => x.Size) .NotEmpty().WithMessage("Field file size is required.") .Must(IsIntegerNumberGreaterThanZero).WithMessage("Field must be a number greater than 0."); RuleFor(x => x.FileName) .NotEmpty().WithMessage("Field file name is required"); } private bool IsIntegerNumberGreaterThanZero(string value) => int.TryParse(value, out var result) && result > 0; } }
问题原因及解决方案
核心问题:属性路径不匹配
你的INotifyDataErrorInfo实现是在MainViewModel上,但UI绑定的是TargetFile.Size和TargetFile.FileName。WPF的ValidatesOnNotifyDataErrors=True会向绑定源(即MainViewModel)请求错误,但你的错误字典里存储的是Size和FileName,而WPF实际会请求的属性路径是TargetFile.Size和TargetFile.FileName,导致找不到对应错误。
修复步骤:
调整错误字典的键为完整属性路径
在Validate方法中,将错误的PropertyName改为TargetFile.+原始属性名:foreach (var error in validationResult.Errors) { string fullPropertyName = $"TargetFile.{error.PropertyName}"; if (!_validationErrors.ContainsKey(fullPropertyName)) _validationErrors.Add(fullPropertyName, new List<string>()); _validationErrors[fullPropertyName].Add(error.ErrorMessage); ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(fullPropertyName)); }同步
GetErrors方法的键匹配
确保GetErrors方法使用完整路径查找错误(当前代码逻辑已支持,无需修改,但需保证传入的propertyName是完整路径)。可选:优化验证触发时机
当前OnTargetFilePropertyChanged会触发全量验证,若需要更精准的单属性验证,可修改为只验证变化的属性:protected void OnTargetFilePropertyChanged(object? sender, PropertyChangedEventArgs e) { string fullPropertyName = $"TargetFile.{e.PropertyName}"; var validationResult = _validator.Validate(_targetFile, options => options.IncludeProperties(e.PropertyName)); _validationErrors.Remove(fullPropertyName); if (!validationResult.IsValid) { _validationErrors[fullPropertyName] = validationResult.Errors.Select(err => err.ErrorMessage).ToList(); } ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(fullPropertyName)); }
额外注意点
- 调试时可在
GetErrors方法加断点,查看WPF实际请求的属性名,确认是否与字典中的键一致。 - 确保
ErrorsChanged事件触发时的属性名与绑定的属性路径完全匹配,WPF依赖此路径关联控件与错误信息。
内容的提问来源于stack exchange,提问作者Ceres
相关产品推荐
相关产品推荐

