This answer simply extends Fredrik Hedblad's excellent answer. Being new to WPF and XAML, Fredrik's answer served as a foothold to determine how I wanted validation errors to appear in my application. Although the XAML below works for me, it's work in progress. I have not fully tested it, and I readily admit that I cannot fully explain each tag. With these caveats, I hope this will prove useful to others.
While animated TextBlock is a great approach, it has two drawbacks that I wanted to address.
- Firstly, since the comment is Brent , the text is limited to the borders of the use window, so if the invalid control is on the edge of the window, the text is disabled. Fredrick proposed a solution so that it appears "outside the window." That makes sense to me.
- Secondly, displaying a TextBlock to the right of an invalid control is not always optimal. For example, let's say a TextBlock is used to indicate a specific file to open and that the Browse button to its right. If the user enters into a nonexistent file, the TextBlock error will cover the Browse button and could potentially prevent the user from clicking on it, correct the error. For me, it makes sense to have an error message displayed diagonally up and to the right of an invalid control. This does two things. First, it avoids hiding any auxiliary controls to the right of an invalid control. It also has a visual effect that toolTipCorner indicates an error message.
Here is the dialogue around which I made my development.

As you can see, there are two TextBox elements that need to be checked. Both are relatively close to the right edge of the window, so long error messages are likely to be cropped. And note that the second TextBox has a browse button, which I don’t want to hide in case of an error.
So what does the validation error look like using my implementation.

Functionally, it is very similar to the Fredrik implementation. If the focus is TextBox , the error will be visible. As soon as he loses focus, the error disappears. If the user hovers over the toolTipCorner, the error will be displayed regardless of whether the TextBox has focus or not. There are also a few cosmetic changes, such as toolTipCorner, 50% more (9 pixels vs 6 pixels).
The obvious difference, of course, is that my implementation uses Popup to display the error. This eliminates the first drawback, because Popup displays its contents in its own window, so it is not limited by the boundaries of the dialog. However, using Popup did present a couple of challenges to overcome.
- From testing and online discussions, it is clear that Popup is considered the topmost window. That way, even when my application was hidden by another application, Popup was still visible. This behavior was less desirable.
- Another way was that if the user moved or resized the dialog box when Popup was shown, Popup did not move to maintain its position relative to the invalid control.
Fortunately, both of these problems have been resolved.
Here is the code. Comments and clarifications are welcome!
- File: ErrorTemplateSilverlightStyle.xaml
- Namespace: MyApp.Application.UI.Templates
- Assembly: MyApp.Application.UI.dll
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:behaviors="clr-namespace:MyApp.Application.UI.Behaviors"> <ControlTemplate x:Key="ErrorTemplateSilverlightStyle"> <StackPanel Orientation="Horizontal"> <Border x:Name="border" BorderThickness="1.25" BorderBrush="#FFDC000C"> <Grid> <Polygon x:Name="toolTipCorner" Grid.ZIndex="2" Margin="-1" Points="9,9 9,0 0,0" Fill="#FFDC000C" HorizontalAlignment="Right" VerticalAlignment="Top" IsHitTestVisible="True"/> <Polyline Grid.ZIndex="3" Points="10,10 0,0" Margin="-1" HorizontalAlignment="Right" StrokeThickness="1.5" StrokeEndLineCap="Round" StrokeStartLineCap="Round" Stroke="White" VerticalAlignment="Top" IsHitTestVisible="True"/> <AdornedElementPlaceholder x:Name="adorner"/> </Grid> </Border> <Popup x:Name="placard" AllowsTransparency="True" PopupAnimation="Fade" Placement="Top" PlacementTarget="{Binding ElementName=toolTipCorner}" PlacementRectangle="10,-1,0,0"> <i:Interaction.Behaviors> <behaviors:RepositionPopupBehavior/> </i:Interaction.Behaviors> <Popup.Style> <Style TargetType="{x:Type Popup}"> <Style.Triggers> <DataTrigger Binding="{Binding ElementName=adorner, Path=AdornedElement.IsFocused}" Value="True"> <Setter Property="IsOpen" Value="True"/> </DataTrigger> <DataTrigger Binding="{Binding ElementName=toolTipCorner, Path=IsMouseOver}" Value="True"> <Setter Property="IsOpen" Value="True"/> </DataTrigger> <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=IsActive}" Value="False"> <Setter Property="IsOpen" Value="False"/> </DataTrigger> </Style.Triggers> </Style> </Popup.Style> <Border x:Name="errorBorder" Background="#FFDC000C" Margin="0,0,8,8" Opacity="1" CornerRadius="4" IsHitTestVisible="False" MinHeight="24" MaxWidth="267"> <Border.Effect> <DropShadowEffect ShadowDepth="4" Color="Black" Opacity="0.6" Direction="315" BlurRadius="4"/> </Border.Effect> <TextBlock Text="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors).CurrentItem.ErrorContent}" Foreground="White" Margin="8,3,8,3" TextWrapping="Wrap"/> </Border> </Popup> </StackPanel> </ControlTemplate> </ResourceDictionary>
- File: RepositionPopupBehavior.cs
- Namespace: MyApp.Application.UI.Behaviors
- Assembly: MyApp.Application.UI.dll
( NOTE: THIS REQUIRES EXPRESSION EXPRESSION 4 System.Windows.Interactivity ASSEMBLY)
using System; using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Interactivity; namespace MyApp.Application.UI.Behaviors {
- File: ResourceLibrary.xaml
- Namespace: MyApp.Application.UI
- Assembly: MyApp.Application.UI.dll
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ResourceDictionary.MergedDictionaries> ... <ResourceDictionary Source="Templates/ErrorTemplateSilverlightStyle.xaml"/> </ResourceDictionary.MergedDictionaries> ... </ResourceDictionary>
- File: App.xaml
- Namespace: MyApp.Application
- Build: MyApp.exe
<Application x:Class="MyApp.Application.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Views\MainWindowView.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/MyApp.Application.UI;component/ResourceLibrary.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
- File: NewProjectView.xaml
- Namespace: MyApp.Application.Views
- Build: MyApp.exe
<Window x:Class="MyApp.Application.Views.NewProjectView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:MyApp.Application.Views" xmlns:viewModels="clr-namespace:MyApp.Application.ViewModels" Title="New Project" Width="740" Height="480" WindowStartupLocation="CenterOwner"> <Window.DataContext> <viewModels:NewProjectViewModel/> </Window.DataContext> ... <Label x:Name="ProjectNameLabel" Grid.Column="0" Content="_Name:" Target="{Binding ElementName=ProjectNameTextBox}"/> <TextBox x:Name="ProjectNameTextBox" Grid.Column="2" Text="{Binding ProjectName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Validation.ErrorTemplate="{StaticResource ErrorTemplateSilverlightStyle}"/> ... </Window>