Search code examples
wpfvalidationtextboxinotifydataerrorinfo

WPF TextBox Validation using INotifyDataErrorInfo


I have created a Custom TextBox control, which overrides TextBox. I have it all working perfectly and I also have validation working with an Override of OnTextChanged.

The issue I have now is that the implementation within the ViewModel has been changed, so validation is now taking place on a button click 'save' at the end of the form (using INotifyDataErrorInfo) rather than on a keystroke basis

My question is, how to I react in my CustomControl to the validation Errors being updated because I want to recolour my textbox border based on the validation result.


Xaml code for where my control is added to the Window:

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

Xaml code for my CustomControl (AppTextBox):

  <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>

</ResourceDictionary>


UPDATE:

I have managed to get SOME validation working in my user control:

I am able to set the red border and labels by checking

<Condition Property="Validation.HasError" Value="True" />

BUT I am unable to display the errors within the control, instead I have had to add a listbox to the Window, and this is not something I want to do:

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

I have tried adding this code to my custom Control but it doesnt work for some reason:

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

And neither does this:

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

Solution

  • Binding RelativeSource.Self references the current element the Binding is defined on. But you are inside the ControlTemplate and want to read from the attached property of the parent type that the ControlTemplate is applied to. You must therefore use RelativeSource.TemplatedParent. And because you want to bind the ListBox to the error collection and not to the current error item you must not bind the ListBox.ItemsSource to the current item's ValidationError.ErrorContent property.

    Define the Binding on the ListBox inside the ControlTemplate as follows:

    <!-- List Errors Here -->
    <ListBox ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(Validation.Errors)}" 
             DisplayMemberName="ErrorContent"
             Foreground="Red" />
    

    I highly recommend the use of an error ControlTemplate that you assign to the Validation.ErrorTemplate attached property instead of adding error feedback visuals and logic directly to the visual tree of the default ControlTemplate of the control. You can follow and extend this example: How to add validation to view model properties or how to implement INotifyDataErrorInfo.