WPF TextBox with validation loses ErrorTemplate

1.7k Views Asked by At

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?

2

There are 2 best solutions below

0
On BEST ANSWER

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

I have seen some strange things with WPF and would offer this suggestion. Per the other posts you referenced and tried to apply here with wrapping your view with the adorner decorator, move the wrapping of the adorner directly into your "MvView1" XAML page.

From your existing...

<AdornerDecorator>
   <vw:MyView1 DataContext="{Binding}"/>
</AdornerDecorator>

change to...

<vw:MyView1 DataContext="{Binding}"/>

and in

<XAML for MyView1>
   <AdornerDecorator>
      [wrapping is within the view... then the rest of the view...]
   </AdornerDecorator>
</close XAML for MyView1>

I have noticed similar such as applying visibility of controls that unless some things are at one level they work, at another they don't. Never found the exact pattern though.