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

INotifyDataErrorInfo问题:错误清除后控件红色边框未消失

WPF验证错误边框未消失问题排查与解决

问题现象

  • 当通过依赖属性OutsideString设置值并手动清除INotifyDataErrorInfo错误后,TextBox的红色错误边框仍未消失
  • 若通过GUI按钮触发清除错误操作,边框可正常消失
  • 项目层级:MainWindow → 通过ContentControl加载TestContainer → TestContainer包含TestUserControl

复现项目代码

MainWindow.xaml

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:ConventionBasedDataTemplateSelector x:Key="ConventionBasedDataTemplateSelector"/>
    </Window.Resources>
    <Grid>
        <ContentControl Content="{Binding TestContainerViewModel}" ContentTemplateSelector="{StaticResource ConventionBasedDataTemplateSelector}" />
    </Grid>
</Window>

MainWindowViewModel.cs

namespace WpfApp1
{
    public class MainWindowViewModel
    {
        public TestContainerViewModel TestContainerViewModel { get; set; } = new();
    }
}

ConventionBasedDataTemplateSelector.cs

namespace WpfApp1
{
    public class ConventionBasedDataTemplateSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            Type viewType = typeof(TestContainer);
            FrameworkElementFactory frameworkElementFactory = new(viewType);
            return new DataTemplate { VisualTree = frameworkElementFactory };
        }
    }
}

TestContainer.xaml

<UserControl x:Class="WpfApp1.TestContainer"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             d:DataContext="{d:DesignInstance Type=local:TestContainerViewModel}">
    <Grid>
        <local:TestUserControl OutsideString="{Binding TotoString}"/>
    </Grid>
</UserControl>

TestContainerViewModel.cs

namespace WpfApp1
{
    public class TestContainerViewModel
    {
        public string TotoString => "Toto";
    }
}

TestUserControl.xaml

<UserControl x:Class="WpfApp1.TestUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid Name="RootContainer">
        <Grid.DataContext>
            <local:TestUserControlViewModel/>
        </Grid.DataContext>
        <StackPanel>
            <TextBox Text="{Binding OutsideString}" Margin="10" />
        </StackPanel>
    </Grid>
</UserControl>

TestUserControl.xaml.cs

using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public partial class TestUserControl : UserControl
    {
        public TestUserControl()
        {
            InitializeComponent();
        }

        public static readonly DependencyProperty OutsideStringProperty = DependencyProperty.Register(
            nameof(OutsideString), typeof(string), typeof(TestUserControl),
            new PropertyMetadata(default(string), OnOutsideStringPropertyChanged));

        public string OutsideString
        {
            get => (string)GetValue(OutsideStringProperty);
            set => SetValue(OutsideStringProperty, value);
        }

        private static void OnOutsideStringPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TestUserControlViewModel viewModel = (TestUserControlViewModel)((TestUserControl)d).RootContainer.DataContext;
            viewModel.OutsideString = (string)e.NewValue;
        }
    }
}

TestUserControlViewModel.cs

using System.Collections;
using System.ComponentModel;

namespace WpfApp1
{
    public class TestUserControlViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        private string _outsideString;
        private bool _hasError = true;

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
        public event PropertyChangedEventHandler PropertyChanged;

        public bool HasErrors => _hasError;
        
        public string OutsideString
        {
            get => _outsideString;
            set
            {
                _outsideString = value;
                _hasError = false;

                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OutsideString)));
                ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(OutsideString)));
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasErrors)));
            }
        }

        public IEnumerable GetErrors(string propertyName)
        {
            return HasErrors ? new []{"Error message"} : null;
        }
    }
}

问题根源与解决思路

核心问题分析

  1. GetErrors返回值不符合规范:WPF的INotifyDataErrorInfo要求无错误时返回空可枚举对象,而非null。返回null会导致验证系统无法正确识别错误已清除。
  2. 绑定时序问题:TestUserControl的ViewModel在XAML中直接实例化,依赖属性回调触发时,TextBox的绑定可能未完成初始化,导致ErrorsChanged事件未被验证系统正确捕获。
  3. 过时API的潜在问题:使用已废弃的FrameworkElementFactory创建DataTemplate,可能导致视觉树绑定上下文或事件注册异常。

具体解决方案

方案1:修正GetErrors返回值

修改TestUserControlViewModel的GetErrors方法,返回空枚举替代null:

using System.Linq;

public IEnumerable GetErrors(string propertyName)
{
    return HasErrors ? new []{"Error message"} : Enumerable.Empty<string>();
}

方案2:调整事件触发时机

在依赖属性回调中延迟执行ViewModel更新,确保TextBox绑定初始化完成:

private static void OnOutsideStringPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    TestUserControl control = (TestUserControl)d;
    TestUserControlViewModel viewModel = (TestUserControlViewModel)control.RootContainer.DataContext;
    // 延迟执行,保证绑定系统已初始化
    control.Dispatcher.BeginInvoke(() => 
    {
        viewModel.OutsideString = (string)e.NewValue;
    });
}

方案3:替换FrameworkElementFactory为XAML定义DataTemplate

在MainWindow.xaml的Resources中直接定义DataTemplate,移除自定义Selector:

<Window.Resources>
    <DataTemplate DataType="{x:Type local:TestContainerViewModel}">
        <local:TestContainer/>
    </DataTemplate>
</Window.Resources>

修改ContentControl,依赖WPF默认数据模板匹配:

<ContentControl Content="{Binding TestContainerViewModel}" />

同时删除ConventionBasedDataTemplateSelector类,此方式更稳定可靠。

方案4:统一ViewModel绑定方式

不在TestUserControl的XAML中直接实例化ViewModel,改为通过上级绑定传递:

  1. 修改TestUserControl.xaml,移除Grid的DataContext:
<Grid Name="RootContainer">
    <StackPanel>
        <TextBox Text="{Binding OutsideString}" Margin="10" />
    </StackPanel>
</Grid>
  1. 在TestContainer.xaml中绑定TestUserControl的DataContext:
<local:TestUserControl OutsideString="{Binding TotoString}" DataContext="{Binding TestUserControlViewModel}"/>
  1. 在TestContainerViewModel中添加ViewModel实例:
public class TestContainerViewModel
{
    public string TotoString => "Toto";
    public TestUserControlViewModel TestUserControlViewModel { get; set; } = new();
}

内容的提问来源于stack exchange,提问作者Nicolas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 11:45:05