wpfvalidationtextboxtabcontroladornerdecorator

WPF TextBox with validation loses ErrorTemplate


I have an issue very similar to these:

Issue with WPF validation(IDataErrorInfo) and tab focusing

TextBox with validation loses ErrorTemplate on tab change

AdornerDecorator do the trick within the same instance of the Window, but when the Window is reloaded and I switch to the TabItem containing the TextBox in error, the ErrorTemplate won't show up anymore.

<Window x:Class="Views.MyWindowView">
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TabControl HorizontalAlignment="Stretch" 
                    Height="Auto"
                    VerticalAlignment="Top"
                    Width="Auto"
                    SelectionChanged="TabItemChanged"
                    Name="MyTabControl">

            <!-- Below, AdornerDecorator are added for the following reason:
                 the Validation.Error cues are painted in the Adorner Layer. 
                 When tabs are switched, that layer is discarded. -->

            <!-- The view 1 tab.-->
            <TabItem Header="{Resx tab1_Header}"
                     Name="Tbi1">
                <AdornerDecorator>
                    <vw:MyView1 DataContext="{Binding}"/>
                </AdornerDecorator>
            </TabItem>

            <!-- The view 2 tab.-->
            <TabItem Header="{Resx tab2_Header}"
                     Name="Tbi2">
                <AdornerDecorator>
                    <vw:MyView2 DataContext="{Binding}"/>
                </AdornerDecorator>
            </TabItem>
        </TabControl>

...

I tried to retrigger the validation in the code-behind on TabControl SelectionChanged, didn't work.

Any idea?


Solution

  • Putting together the pieces of the puzzle

    An AdornerLayer represents a surface for rendering adorners. As an AdornerLayer usually serves an entire view, not just one control, some containers implement them by default.

    An adorner is a custom FrameworkElement that is bound to a UIElement. Adorners are rendered in an AdornerLayer, which is a rendering surface that is always on top of the adorned element or a collection of adorned elements.

    So in this case the adorner (red rectangle) is bound to a TextBox, but is rendered in a layer on top of the TextBox.

    Adorning (e.g. in case of a validation error) is done by calling the static method GetAdornerLayer to get an AdornerLayer object for the UIElement to be adorned.

    Enough theory

    Changing TabItems discards the AdornerLayer, resulting in the adorner not being drawn. 2 fixes:

    \The manual way, as proposed by DRapp:

    <XAML for MyView1>
       <AdornerDecorator>
          ...
       </AdornerDecorator>
    </close XAML for MyView1>
    

    Of course, if there's another container implementing an AdornerLayer between the AdornerDecorator and the TextBox (in the visual tree), this won't do any good. So the explicit AdornerDecorator needs to be the last one wrapping the TextBox.

    <XAML for MyView1>
        <Grid>
    
            ...
    
            <GroupBox>
                <AdornerDecorator>
                    <Grid>
    
                        ...
    
                        <TextBox ... />
                    </Grid>
                </AdornerDecorator>
            </GroupBox>
        </Grid>
    </close XAML for MyView1>
    

    \Second solution (which I prefer) resetting the ErrorTemplate each time the TextBox becomes visible. In doing so the lack of AdornerLayer gets spotted and fixed.

    <UserControl.Resources>
        <Style TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="IsVisible" Value="true">
                    <Setter Property="Validation.ErrorTemplate">
                        <Setter.Value>
                            <ControlTemplate>
                                <Border BorderBrush="Red" BorderThickness="1">
                                    <AdornedElementPlaceholder/>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
    </UserControl.Resources>