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

基于INotifyDataErrorInfo的WPF自定义TextBox验证问题

自定义TextBox控件内部绑定Validation.Errors显示错误信息失败问题

我基于TextBox实现了自定义控件AppTextBox,原本通过重写OnTextChanged实现按键触发验证。现在ViewModel改用点击保存按钮时,通过INotifyDataErrorInfo进行验证。目前已经通过Validation.HasError实现了红色边框和标签变色,但无法在控件内部显示错误信息——只能在外部Window的ListBox中显示,内部绑定Validation.Errors的尝试全部失败。

窗口中添加控件的XAML代码

<ctrl:AppTextBox ButtonCommand="{Binding AddNewWordCommand}"
                 ButtonLabel="+"
                 Label="New Word"
                 Text="{Binding NewWord, 
                                UpdateSourceTrigger=PropertyChanged, 
                                ValidatesOnDataErrors=True}"
                 x:Name="txtNewWord" />

自定义控件AppTextBox的XAML代码

<Style TargetType="{x:Type ctrl:AppTextBox}"
       BasedOn="{StaticResource {x:Type TextBox}}">

  <Setter Property="Background" Value="Transparent" />
  <Setter Property="BorderThickness" Value="0" />
  <Setter Property="FontFamily" Value="{DynamicResource Inter400}" />
  <Setter Property="FontSize" Value="{DynamicResource Font_Medium}" />
  <Setter Property="Foreground" Value="{DynamicResource Theme_Foreground}" />
  <Setter Property="Margin" Value="20,8" />
  <Setter Property="TextWrapping" Value="Wrap" />
  <Setter Property="VerticalAlignment" Value="Center" />
  <Setter Property="Validation.ErrorTemplate" Value="{x:Null}" />

  <Setter Property="Template">

    <Setter.Value>

      <ControlTemplate TargetType="{x:Type ctrl:AppTextBox}">

        <StackPanel Orientation="Vertical">

          <Border Background="{TemplateBinding Background}"
                  BorderBrush="{DynamicResource TextBox_Border_Focus}" 
                  BorderThickness="1"
                  CornerRadius="{DynamicResource Control_CornerRadius}"
                  Cursor="IBeam"
                  Focusable="False"
                  MinHeight="{DynamicResource TextBox_Height}"
                  Padding="10,2,5,2"
                  x:Name="bdrOutline" >

            <Grid>

              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
              </Grid.ColumnDefinitions>

              <StackPanel Background="Transparent"
                          Grid.Column="0"
                          Orientation="Vertical"
                          VerticalAlignment="Center"
                          x:Name="stpLayout" >

                <Label Background="Transparent"
                       Content="{TemplateBinding Label}"
                       Focusable="False"
                       FontFamily="{DynamicResource Inter600}"
                       FontSize="12"
                       Foreground="{DynamicResource TextBox_Label_Focus}"
                       IsTabStop="False"
                       VerticalAlignment="Center"
                       Visibility="{TemplateBinding LabelVisibility}"
                       x:Name="lblHeading" />

                <ScrollViewer Background="Transparent"
                              BorderBrush="Transparent"
                              Cursor="IBeam"
                              Foreground="{DynamicResource Theme_Foreground}" 
                              HorizontalAlignment="Stretch"
                              Margin="3,0,0,0"
                              VerticalAlignment="Center"
                              VerticalScrollBarVisibility="Auto"
                              Visibility="Visible"
                              x:Name="PART_ContentHost" >

                  <i:Interaction.Triggers>
                    <i:KeyTrigger Key="Return">
                      <i:InvokeCommandAction Command="{Binding ButtonCommand}" />
                    </i:KeyTrigger>
                  </i:Interaction.Triggers>

                </ScrollViewer>

              </StackPanel>

              <ContentControl Focusable="False"
                              Grid.Column="1"
                              HorizontalAlignment="Center"
                              Margin="5,0"
                              Style="{DynamicResource ErrorIcon_Canvas}"
                              VerticalAlignment="Center"
                              Visibility="Collapsed"
                              x:Name="cvsError" />

              <Button Command="{TemplateBinding ButtonCommand}"
                      Content="+"
                      Cursor="Hand"
                      Grid.Column="2"            
                      Margin="5,0"
                      Padding="10,7"
                      Style="{DynamicResource FilledSquareButton_Base}"
                      VerticalAlignment="Center"
                      Visibility="Visible"
                      x:Name="btnAction" />

            </Grid>

          </Border>

          <!-- List Errors Here -->
          <ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
                   ItemsSource="{Binding RelativeSource={x:Static RelativeSource.Self},
                                         Path=(Validation.Errors)/ErrorContent}" 
                   Style="{DynamicResource ErrorMessagesListBox}" />

        </StackPanel>

        <ControlTemplate.Triggers>

          <Trigger Property="ButtonIsActive" Value="False">
            <Setter TargetName="btnAction" Property="Visibility" Value="Collapsed" />
          </Trigger>

          <Trigger Property="HasBorder" Value="False">
            <Setter TargetName="bdrOutline" Property="BorderBrush" Value="Transparent" />
          </Trigger>

          <Trigger Property="Validation.HasError" Value="True">
            <Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource Theme_ErrorBrush}" />
          </Trigger>

          <Trigger Property="Layout" Value="Inline">
            <Setter TargetName="lblHeading" Property="VerticalAlignment" Value="Top" />
            <Setter TargetName="PART_ContentHost" Property="Margin" Value="40,5,0,0" />
            <Setter TargetName="stpLayout" Property="Orientation" Value="Horizontal" />
          </Trigger>

          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="HasBorder" Value="True" />
              <Condition Property="Validation.HasError" Value="True" />
            </MultiTrigger.Conditions>
            <Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource Theme_ErrorBrush}" />
          </MultiTrigger>

          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="TextFillState" Value="Empty" />
              <Condition Property="IsKeyboardFocusWithin" Value="False" />
              <Condition Property="Validation.HasError" Value="False" />
            </MultiTrigger.Conditions>
            <Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Empty}" />
            <Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Empty}" />
            <Setter TargetName="PART_ContentHost" Property="Visibility" Value="Collapsed" />
          </MultiTrigger>

          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="TextFillState" Value="Empty" />
              <Condition Property="IsMouseOver" Value="True" />
              <Condition Property="IsKeyboardFocusWithin" Value="False" />
              <Condition Property="Validation.HasError" Value="False" />
            </MultiTrigger.Conditions>
            <Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Empty_Hover}" />
            <Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Empty_Hover}" />
          </MultiTrigger>

          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="TextFillState" Value="Filled" />
              <Condition Property="IsMouseOver" Value="False" />
              <Condition Property="IsKeyboardFocusWithin" Value="False" />
              <Condition Property="Validation.HasError" Value="False" />
            </MultiTrigger.Conditions>
            <Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Filled}" />
            <Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Filled}" />
          </MultiTrigger>

          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="TextFillState" Value="Filled" />
              <Condition Property="IsMouseOver" Value="True" />
              <Condition Property="Validation.HasError" Value="False" />
            </MultiTrigger.Conditions>
            <Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Filled_Hover}" />
            <Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Filled_Hover}" />
          </MultiTrigger>


          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="IsKeyboardFocusWithin" Value="True" />
              <Condition Property="Validation.HasError" Value="False" />
            </MultiTrigger.Conditions>
            <Setter TargetName="bdrOutline" Property="BorderBrush" Value="{DynamicResource TextBox_Border_Focus}" />
            <Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource TextBox_Label_Focus}" />
          </MultiTrigger>

        </ControlTemplate.Triggers>

      </ControlTemplate>

    </Setter.Value>

  </Setter>

</Style>

更新说明

已实现部分验证功能,可通过<Condition Property="Validation.HasError" Value="True" />设置红色边框和标签颜色,但无法在控件内部显示错误信息,只能在Window中添加ListBox显示:

<ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
         ItemsSource="{Binding ElementName=txtEntry, 
                               Path=(Validation.Errors)}" 
         Style="{DynamicResource ErrorMessagesListBox}" />

尝试在自定义控件内添加以下代码但均失败:

<ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
         ItemsSource="{Binding RelativeSource={x:Static RelativeSource.Self}, 
                               Path=(Validation.Errors)}" 
         Style="{DynamicResource ErrorMessagesListBox}" />
<ListBox ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
         ItemsSource="{Binding ElementName=PART_ContentHost,
                               Path=(Validation.Errors)}" 
         Style="{DynamicResource ErrorMessagesListBox}" />

解决方法

问题出在控件模板内部绑定的相对源指向错误,以及Validation.Errors的绑定方式。需要修改控件内ListBox的绑定,使其指向自定义控件本身的Validation.Errors集合:

  1. 修改控件内的ListBox绑定:
<ListBox x:Name="lstErrors"
         ItemContainerStyle="{DynamicResource ErrorMessagesListBoxItem}"
         ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, 
                               Path=(Validation.Errors)}"
         DisplayMemberPath="ErrorContent"
         Style="{DynamicResource ErrorMessagesListBox}" />
  • 使用RelativeSource TemplatedParent指向自定义控件实例(而非ListBox自身),确保获取到正确的验证错误集合。
  • 通过DisplayMemberPath="ErrorContent"直接指定显示错误内容,无需在绑定路径中嵌套/ErrorContent。
  1. 可选:添加触发器控制错误提示的可见性
    在ControlTemplate.Triggers中添加触发器,无错误时隐藏ListBox:
<Trigger Property="Validation.HasError" Value="False">
  <Setter TargetName="lstErrors" Property="Visibility" Value="Collapsed" />
</Trigger>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 13:09:52