Search code examples
wpfdata-bindingwpf-style

WPF - TextBox validation error displayed twice and not removed


Issue description

I'm developing an application in which I have a ListBox where, when an element is selected, it's details are shown in an editing control.

I'm binding the SelectedItem to the control and, as I want to apply different DataTemplates, I'm trying to use VM first approach and bind directly to the control contents.

My custom TextBox style displays validation errors with a blue border (for the sake of the example). However, when using this approach, a red validation border is also shown and it's not being removed once the data is correct. This is not the expected behavior, the red border should not show at all.

I don't know if the error is in the style or in the binding.

Example and testing

I've tried different things to try to debug the error. This is not happening with the standard style nor with a DataContext approach. However, I cannot use the DataContext approach as I will need to apply different templates to different types of elements in the list.

See the pictures below.

When the data is invalid (empty) the "VM First + Custom style" option shows both the blue and the red borders:

Data with error border

When I write some text, the red border is not removed:

Valid data where error border should be gone

ViewModels

There are two ViewModels, one for the main window and another for each element in the list:

public class ChildViewModel : ViewModelBase
{
    private string _name;
    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
    public override string this[string propertyName]
    {
        get
        {
            if (propertyName == nameof(Name))
            {
                if (string.IsNullOrEmpty(Name))
                {
                    return "The name is mandatory.";
                }
            }
            return base[propertyName];
        }
    }

}

public class ParentViewModel : ViewModelBase
{
    private ChildViewModel _selectedItem;
    public ObservableCollection<ChildViewModel> Collection { get; private set; }
    public ChildViewModel SelectedItem
    {
        get => _selectedItem;
        set => SetProperty(ref _selectedItem, value);
    }
    public ParentViewModel()
    {
        Collection = new ObservableCollection<ChildViewModel>();
        Collection.Add(new ChildViewModel());
        Collection.Add(new ChildViewModel());
    }
}

public abstract class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string Error => this[null];
    public virtual string this[string propertyName] => string.Empty;

    protected void NotifyPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, value))
        {
            field = value;
            NotifyPropertyChanged(propertyName);
        }
    }
}

Views

The MainWindow is just as follows, with no code behind apart from the standard.

<Window x:Class="TestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TestApp"
        Title="MainWindow" Height="300" Width="400">
    <Window.DataContext>
        <local:ParentViewModel />
    </Window.DataContext>
    <Window.Resources>
        <ResourceDictionary Source="Style.xaml" />
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <ListBox Grid.RowSpan="2" ItemsSource="{Binding Collection, Mode=OneWay}" SelectedItem="{Binding SelectedItem}"/>
        <UserControl Grid.Column="1" Grid.Row="0" DataContext="{Binding SelectedItem}">
            <StackPanel Orientation="Vertical">
                <Label Content="DataContext + No style" />
                <TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
                <Label Content="DataContext + Custom style" />
                <TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TextBoxStyle}" />
            </StackPanel>
        </UserControl>
        <ContentControl Grid.Column="1" Grid.Row="1" Content="{Binding SelectedItem}">
            <ContentControl.Resources>
                <DataTemplate DataType="{x:Type local:ChildViewModel}">
                    <StackPanel Orientation="Vertical">
                        <Label Content="VM First + No style" />
                        <TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
                        <Label Content="VM First + Custom style" />
                        <TextBox Margin="6" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource TextBoxStyle}" />
                    </StackPanel>
                </DataTemplate>
            </ContentControl.Resources>
        </ContentControl>
    </Grid>
</Window>

Style

This is located in a ResourceDictionary named "Style.xaml". The Border and the ValidationErrorElement are separated elements in order to apply different visual states for mouse over, focused...

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type TextBox}">
                    <Grid x:Name="RootElement">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="ValidationStates">
                                <VisualState x:Name="Valid" />
                                <VisualState x:Name="InvalidUnfocused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidFocused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationErrorElement">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Border x:Name="Border" BorderThickness="1" BorderBrush="Black" Opacity="1">
                            <Grid>
                                <ScrollViewer x:Name="PART_ContentHost" BorderThickness="0" IsTabStop="False" Padding="{TemplateBinding Padding}" />
                            </Grid>
                        </Border>
                        <Border x:Name="ValidationErrorElement" BorderBrush="Blue" BorderThickness="1" Visibility="Collapsed">
                        </Border>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Solution

  • You are not displaying the error condition correctly.
    WPF uses a ControlTemplate with an AdornedElementPlaceholder for this, which is set in the attached Validation.ErrorTemplate property.

    A big example with implementation can be found here: https://stackoverflow.com/a/68748914/13349759

    Here is a small snippet of XAML from another small example:

        <Window.Resources>
            <ControlTemplate x:Key="validationFailed">
                <StackPanel Orientation="Horizontal">
                    <Border BorderBrush="Violet" BorderThickness="2">
                        <AdornedElementPlaceholder />
                    </Border>
                    <TextBlock Foreground="Red" FontSize="26" FontWeight="Bold">!</TextBlock>
                </StackPanel>
            </ControlTemplate>
        </Window.Resources>
        <Grid>
             
            <TextBox Margin="10"
                Validation.ErrorTemplate="{StaticResource validationFailed}" >
                <TextBox.Text>
                    <Binding Path="Age">
                        <Binding.ValidationRules>
                            <DataErrorValidationRule />
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>