Par défaut, le Validation.ErrorTemplate
dans WPF est simplement une petite bordure rouge sans aucune ToolTip
.
Dans Silverlight 4, l'erreur de validation est bien prête à l'emploi.
Voici une comparaison d'une erreur de validation survenue dans Silverlight 4 et WPF
Silverlight 4
WPF
Remarquez le look vraiment plat et ennuyeux de la version WPF par rapport au superbe look de Silverlight.
Existe-t-il des styles/modèles de validation similaires dans le cadre WPF ou quelqu'un a-t-il créé des modèles de validation bien stylés, comme la version Silverlight ci-dessus? Ou devrais-je les créer à partir de zéro?
Si quelqu'un veut l'essayer, l'erreur de validation ci-dessus peut être reproduite avec le code suivant, fonctionne à la fois pour Silverlight et WPF
MainWindow/MainPage.xaml
<StackPanel Orientation="Horizontal" Margin="10" VerticalAlignment="Top">
<TextBox Text="{Binding Path=TextProperty, Mode=TwoWay, ValidatesOnExceptions=True}"/>
<Button Content="Tab To Me..." Margin="20,0,0,0"/>
</StackPanel>
MainWindow/MainPage.xaml.cs
public MainWindow/MainPage()
{
InitializeComponent();
this.DataContext = this;
}
private string _textProperty;
public string TextProperty
{
get { return _textProperty; }
set
{
if (value.Length > 5)
{
throw new Exception("Too many characters");
}
_textProperty = value;
}
}
J'ai étudié la version Silverlight du modèle d'erreur de validation et en ai créé uneWPFqui ressemble à ceci
Ajout d’un GIF animé au bas de la publication mais après l’avoir terminé, j’ai remarqué que c’était peut-être gênant à cause du déplacement de la souris. Faites-moi savoir si je devrais l'enlever .. :)
J'ai utilisé une MultiBinding
avec une BooleanOrConverter
pour afficher "l'erreur d'info-bulle" lorsque la TextBox
a le focus clavier ou que la souris se trouve dans le coin supérieur droit. Pour l'animation d'ouverture en fondu, j'ai utilisé une DoubleAnimation
pour la Opacity
et une ThicknessAnimation
avec une BackEase
EaseOut
EasingFunction
pour la Margin
Utilisable comme ça
<TextBox Validation.ErrorTemplate="{StaticResource errorTemplateSilverlightStyle}" />
_/errorTemplateSilverlightStyle
<ControlTemplate x:Key="errorTemplateSilverlightStyle">
<StackPanel Orientation="Horizontal">
<Border BorderThickness="1" BorderBrush="#FFdc000c" CornerRadius="0.7"
VerticalAlignment="Top">
<Grid>
<Polygon x:Name="toolTipCorner"
Grid.ZIndex="2"
Margin="-1"
Points="6,6 6,0 0,0"
Fill="#FFdc000c"
HorizontalAlignment="Right"
VerticalAlignment="Top"
IsHitTestVisible="True"/>
<Polyline Grid.ZIndex="3"
Points="7,7 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>
<Border x:Name="errorBorder" Background="#FFdc000c" Margin="1,0,0,0"
Opacity="0" CornerRadius="1.5"
IsHitTestVisible="False"
MinHeight="24" MaxWidth="267">
<Border.Effect>
<DropShadowEffect ShadowDepth="2.25"
Color="Black"
Opacity="0.4"
Direction="315"
BlurRadius="4"/>
</Border.Effect>
<TextBlock Text="{Binding ElementName=adorner,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
Foreground="White" Margin="8,3,8,3" TextWrapping="Wrap"/>
</Border>
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource BooleanOrConverter}">
<Binding ElementName="adorner" Path="AdornedElement.IsKeyboardFocused" />
<Binding ElementName="toolTipCorner" Path="IsMouseOver"/>
</MultiBinding>
</DataTrigger.Binding>
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="fadeInStoryboard">
<Storyboard>
<DoubleAnimation Duration="00:00:00.15"
Storyboard.TargetName="errorBorder"
Storyboard.TargetProperty="Opacity"
To="1"/>
<ThicknessAnimation Duration="00:00:00.15"
Storyboard.TargetName="errorBorder"
Storyboard.TargetProperty="Margin"
FillBehavior="HoldEnd"
From="1,0,0,0"
To="5,0,0,0">
<ThicknessAnimation.EasingFunction>
<BackEase EasingMode="EaseOut" Amplitude="2"/>
</ThicknessAnimation.EasingFunction>
</ThicknessAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="fadeInStoryboard"/>
<BeginStoryboard x:Name="fadeOutStoryBoard">
<Storyboard>
<DoubleAnimation Duration="00:00:00"
Storyboard.TargetName="errorBorder"
Storyboard.TargetProperty="Opacity"
To="0"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
BooleanOrConverter
public class BooleanOrConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
foreach (object value in values)
{
if ((bool)value == true)
{
return true;
}
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Cette réponse ne fait que développer l'excellente réponse de Fredrik Hedblad . Nouveau dans WPF et XAML, la réponse de Fredrik a servi de tremplin pour définir la manière dont je souhaitais afficher les erreurs de validation dans mon application. Bien que le XAML ci-dessous fonctionne pour moi, c’est un travail en cours. Je ne l'ai pas complètement testé et j'avouerai volontiers que je ne peux pas expliquer complètement chaque étiquette. Avec ces mises en garde, j'espère que cela s'avérera utile pour les autres.
Bien que le TextBlock animé soit une bonne approche, il présente deux inconvénients que je voulais aborder.
Voici le dialogue autour duquel j'ai réalisé mon développement.
Comme vous pouvez le constater, deux contrôles TextBox doivent être validés. Les deux sont relativement proches du bord droit de la fenêtre. Par conséquent, les messages d'erreur longs risquent d'être tronqués. Et remarquez que le second TextBox a un bouton Parcourir que je ne veux pas masquer en cas d'erreur.
Alors voici à quoi ressemble une erreur de validation en utilisant mon implémentation.
Sur le plan fonctionnel, cela ressemble beaucoup à la mise en œuvre de Fredrik. Si le TextBox a le focus, l'erreur sera visible. Une fois le focus perdu, l'erreur disparaît. Si l'utilisateur passe la souris sur l'outil toolTipCorner , l'erreur apparaîtra que la TextBox ait le focus ou non. Il y a également quelques modifications esthétiques, telles que toolTipCorner étant 50% plus grand (9 pixels par rapport à 6 pixels).
La différence évidente, bien sûr, est que mon implémentation utilise un 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 contraint par les bordures de la boîte de dialogue. Cependant, l’utilisation de Popup présentait quelques difficultés à surmonter.
Heureusement, ces deux problèmes ont été résolus.
Voici le code. Commentaires et améliorations sont les bienvenus!
<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>
( REMARQUE: CECI NECESSITE L'ASSEMBLAGE DE L'EXPRESSION ASSEMBLE 4 System.Windows.Interactivity Assembly)
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
}
}
<ResourceDictionary xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<!-- Styles -->
...
<!-- Templates -->
<ResourceDictionary Source="Templates/ErrorTemplateSilverlightStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Converters -->
...
</ResourceDictionary>
<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>
<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>
J'ai créé mon adorner d'erreur personnalisé dans l'un des projets pour afficher l'adorateur d'erreur juste en dessous de ma zone de texte contenant un message d'erreur. Il vous suffit de définir la propriété "Validation.ErrorTemplate" dans le style par défaut de votre zone de texte, que vous pouvez conserver dans les ressources de votre application afin qu'elle soit appliquée à toutes les zones de texte de votre application.
Note: J'ai utilisé des pinceaux ici, remplacez-les par votre propre jeu de pinceaux que vous voulez pour votre message adorner. Peut-être cela peut-il être utile:
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<!--TextBox Error template-->
<Canvas Panel.ZIndex="1099">
<DockPanel>
<Border BorderBrush="{DynamicResource HighlightRedBackgroundBrush}" BorderThickness="2" Padding="1" CornerRadius="3">
<AdornedElementPlaceholder x:Name="ErrorAdorner" />
</Border>
</DockPanel>
<Popup IsOpen="True" AllowsTransparency="True" Placement="Bottom" PlacementTarget="{Binding ElementName=ErrorAdorner}" StaysOpen="False">
<Border Canvas.Bottom="4"
Canvas.Left="{Binding Path=AdornedElement.ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Adorner}}}"
BorderBrush="{DynamicResource HighlightRedBackgroundBrush}"
BorderThickness="1"
Padding="4"
CornerRadius="5"
Background="{DynamicResource ErrorBackgroundBrush}">
<StackPanel Orientation="Horizontal">
<ContentPresenter Width="24" Height="24" Content="{DynamicResource ExclamationIcon}" />
<TextBlock TextWrapping="Wrap"
Margin="4"
MaxWidth="250"
Text="{Binding Path=AdornedElement.(Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Adorner}}}" />
</StackPanel>
</Border>
</Popup>
</Canvas>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
Je me suis heurté à un problème en essayant de l'appliquer à un projet wpf sur lequel je travaille. Si vous rencontrez le problème suivant lorsque vous essayez d'exécuter le projet:
"Une exception de type 'System.Windows.Markup.XamlParseException' s'est produite dans PresentationFramework.dll mais n'a pas été gérée dans le code utilisateur"
Vous devez créer une instance de la classe booleanOrConverter dans vos ressources (dans app.xaml):
<validators:BooleanOrConverter x:Key="myConverter" />
N'oubliez pas non plus d'ajouter l'espace de nom en haut du fichier (dans la balise application):
xmlns: validators = "espace de noms clr: ParcelRatesViewModel.Validators; Assembly = ParcelRatesViewModel"