Cette réponse ne fait que développer Fredrik Hedblad L'excellente réponse de l'auteur. Étant novice en matière de WPF et de XAML, la réponse de Fredrik m'a servi de tremplin pour définir comment je voulais que les erreurs de validation soient affichées dans mon application. Bien que le XAML ci-dessous fonctionne pour moi, il s'agit d'un travail en cours. Je ne l'ai pas entièrement testé, et j'admets volontiers que je ne peux pas expliquer complètement chaque balise. Avec ces réserves, j'espère que cela sera utile à d'autres.
Alors que l'animation Bloc de texte est une bonne approche, elle présente deux lacunes que je voulais aborder.
- Tout d'abord, comme Brent Comme l'indique le commentaire de l'auteur, le texte est limité par les bords de la fenêtre propriétaire, de sorte que si le contrôle non valide se trouve au bord de la fenêtre, le texte est coupé. La solution proposée par Fredrik est de l'afficher "en dehors de la fenêtre". Cela me semble logique.
- Deuxièmement, montrer le Bloc de texte à droite du contrôle invalide n'est pas toujours optimale. Par exemple, disons que le Bloc de texte est utilisé pour spécifier un fichier particulier à ouvrir et qu'il y a un bouton Parcourir à sa droite. Si l'utilisateur saisit un fichier inexistant, l'erreur Bloc de texte couvrira le bouton Parcourir et empêchera potentiellement l'utilisateur de cliquer dessus pour corriger l'erreur. Ce qui me paraît logique, c'est d'afficher le message d'erreur en diagonale vers le haut et à droite du contrôle non valide. Cela permet de réaliser deux choses. Tout d'abord, cela évite de masquer tout contrôle complémentaire à droite du contrôle non valide. Cela a également pour effet visuel que le coin de l'extrémité de l'outil est pointage vers le message d'erreur.
Voici le dialogue autour duquel j'ai fait mon développement.
Comme vous pouvez le voir, il y a deux TextBox les contrôles qui doivent être validés. Les deux sont relativement proches du bord droit de la fenêtre, de sorte que les longs messages d'erreur seraient probablement tronqués. Et remarquez que le deuxième TextBox comporte un bouton "Browse" que je ne veux pas voir masqué en cas d'erreur.
Voici donc à quoi ressemble une erreur de validation en utilisant mon implémentation.
Fonctionnellement, elle est très similaire à l'implémentation de Fredrik. Si le TextBox a le focus, l'erreur sera visible. Lorsqu'il perd le focus, l'erreur disparaît. Si l'utilisateur passe la souris sur l'icône coin de l'extrémité de l'outil l'erreur apparaîtra indépendamment du fait que le TextBox a le focus ou non. Il y a également quelques changements cosmétiques, comme l'ajout d'une nouvelle fonctionnalité à l'écran. coin de l'extrémité de l'outil étant 50% plus grande (9 pixels contre 6 pixels).
La différence évidente, bien sûr, est que ma mise en œuvre utilise une Popup pour afficher l'erreur. Cela résout le premier problème, car le Popup affiche son contenu dans sa propre fenêtre, de sorte qu'il n'est pas limité par les limites de la boîte de dialogue. Cependant, l'utilisation d'une Popup a présenté quelques défis à surmonter.
- Il ressort des tests et des discussions en ligne que le Popup est considérée comme une fenêtre supérieure. Ainsi, même lorsque mon application était masquée par une autre application, la Popup était toujours visible. C'était un comportement peu souhaitable.
- L'autre problème était que si l'utilisateur déplaçait ou redimensionnait la boîte de dialogue pendant que l'option Popup était visible, le Popup ne s'est pas repositionné pour maintenir sa position par rapport au contrôle non valide.
Heureusement, ces deux défis ont été relevés.
Voici le code. Les commentaires et les améliorations sont les bienvenus !
- Fichier : ErrorTemplateSilverlightStyle.xaml
- Namespace : MonApp.Application.UI.Templates
-
Assemblée : MonApp.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">
<!-- Defines TextBox outline border and the ToolTipCorner -->
<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>
<!-- Defines the Popup -->
<Popup x:Name="placard"
AllowsTransparency="True"
PopupAnimation="Fade"
Placement="Top"
PlacementTarget="{Binding ElementName=toolTipCorner}"
PlacementRectangle="10,-1,0,0">
<!-- Used to reposition Popup when dialog moves or resizes -->
<i:Interaction.Behaviors>
<behaviors:RepositionPopupBehavior/>
</i:Interaction.Behaviors>
<Popup.Style>
<Style TargetType="{x:Type Popup}">
<Style.Triggers>
<!-- Shows Popup when TextBox has focus -->
<DataTrigger Binding="{Binding ElementName=adorner, Path=AdornedElement.IsFocused}"
Value="True">
<Setter Property="IsOpen" Value="True"/>
</DataTrigger>
<!-- Shows Popup when mouse hovers over ToolTipCorner -->
<DataTrigger Binding="{Binding ElementName=toolTipCorner, Path=IsMouseOver}"
Value="True">
<Setter Property="IsOpen" Value="True"/>
</DataTrigger>
<!-- Hides Popup when window is no longer active -->
<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>
- Fichier : RepositionPopupBehavior.cs
- Namespace : MonApp.Application.UI.Behaviors
- Assemblée : MonApp.Application.UI.dll
( REMARQUE : L'EXPRESSION BLEND 4 System.Windows.Interactivity ASSEMBLY est nécessaire.)
using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;
namespace MyApp.Application.UI.Behaviors
{
/// <summary>
/// Defines the reposition behavior of a <see cref="Popup"/> control when the window to which it is attached is moved or resized.
/// </summary>
/// <remarks>
/// This solution was influenced by the answers provided by <see href="https://stackoverflow.com/users/262204/nathanaw">NathanAW</see> and
/// <see href="https://stackoverflow.com/users/718325/jason">Jason</see> to
/// <see href="https://stackoverflow.com/questions/1600218/how-can-i-move-a-wpf-popup-when-its-anchor-element-moves">this</see> question.
/// </remarks>
public class RepositionPopupBehavior : Behavior<Popup>
{
#region Protected Methods
/// <summary>
/// Called after the behavior is attached to an <see cref="Behavior.AssociatedObject"/>.
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
var window = Window.GetWindow(AssociatedObject.PlacementTarget);
if (window == null) { return; }
window.LocationChanged += OnLocationChanged;
window.SizeChanged += OnSizeChanged;
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
//AssociatedObject.HorizontalOffset = 7;
//AssociatedObject.VerticalOffset = -AssociatedObject.Height;
}
/// <summary>
/// Called when the behavior is being detached from its <see cref="Behavior.AssociatedObject"/>, but before it has actually occurred.
/// </summary>
protected override void OnDetaching()
{
base.OnDetaching();
var window = Window.GetWindow(AssociatedObject.PlacementTarget);
if (window == null) { return; }
window.LocationChanged -= OnLocationChanged;
window.SizeChanged -= OnSizeChanged;
AssociatedObject.Loaded -= AssociatedObject_Loaded;
}
#endregion Protected Methods
#region Private Methods
/// <summary>
/// Handles the <see cref="Window.LocationChanged"/> routed event which occurs when the window's location changes.
/// </summary>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An object that contains the event data.
/// </param>
private void OnLocationChanged(object sender, EventArgs e)
{
var offset = AssociatedObject.HorizontalOffset;
AssociatedObject.HorizontalOffset = offset + 1;
AssociatedObject.HorizontalOffset = offset;
}
/// <summary>
/// Handles the <see cref="Window.SizeChanged"/> routed event which occurs when either then <see cref="Window.ActualHeight"/> or the
/// <see cref="Window.ActualWidth"/> properties change value.
/// </summary>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An object that contains the event data.
/// </param>
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
var offset = AssociatedObject.HorizontalOffset;
AssociatedObject.HorizontalOffset = offset + 1;
AssociatedObject.HorizontalOffset = offset;
}
#endregion Private Methods
}
}
- Fichier : NewProjectView.xaml
- Namespace : MonApp.Application.Views
-
Assemblée : 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">
<!-- DATA CONTEXT -->
<Window.DataContext>
<viewModels:NewProjectViewModel/>
</Window.DataContext>
<!-- WINDOW GRID -->
...
<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>