You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,导致找不到对应错误。

修复步骤:

  1. 调整错误字典的键为完整属性路径
    在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));
    }
    
  2. 同步GetErrors方法的键匹配
    确保GetErrors方法使用完整路径查找错误(当前代码逻辑已支持,无需修改,但需保证传入的propertyName是完整路径)。

  3. 可选:优化验证触发时机
    当前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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 06:07:05