59 votes

Style d'erreur de validation dans WPF, similaire à Silverlight

Par défaut, le Validation.ErrorTemplate sur WPF est juste une petite bordure rouge sans aucune ToolTip .

Sur Silverlight 4 l'erreur de validation est joliment stylisée et prête à l'emploi.

Voici une comparaison d'une erreur de validation survenant dans Silverlight 4 et WPF

Silverlight 4
enter image description here
WPF
enter image description here

Remarquez l'aspect vraiment plat et ennuyeux de la version WPF par rapport à l'aspect, à mon avis superbe, de Silverlight.

Existe-t-il des styles/templates de validation similaires dans le cadre de WPF ou quelqu'un a-t-il créé des templates de validation joliment stylisés comme le Silverlight version ci-dessus ? Ou dois-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, qui fonctionne pour les deux. 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;
    }
}

105voto

Fredrik Hedblad Points 42772

J'ai étudié le Silverlight de la version du modèle d'erreur de validation et créé une WPF qui ressemble à ceci

enter image description here
J'ai ajouté un GIF animé en bas du message mais après l'avoir terminé, j'ai remarqué qu'il pouvait être gênant à cause de la souris qui bouge. Faites-moi savoir si je dois le supprimer :)

J'ai utilisé un MultiBinding avec un BooleanOrConverter pour afficher l'"info-bulle d'erreur" lorsque l'élément TextBox a le focus sur le clavier ou la souris est au-dessus du coin supérieur droit. Pour l'animation du fondu, j'ai utilisé un DoubleAnimation pour le Opacity et un ThicknessAnimation avec un BackEase / EaseOut EasingFunction pour le Margin

Utilisable comme ceci

<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();
    }
}

enter image description here

36voto

Matt Davis Points 22019

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.

  1. 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.
  2. 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.

Basic Dialog

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.

enter image description here

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.

  1. 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.
  2. 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 : App.xaml
  • Namespace : MonApp.Application
  • Assemblée : 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>


  • 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>

3voto

Rohit Vats Points 36234

J'ai créé mon ornement d'erreur personnalisé dans l'un des projets pour afficher l'ornement d'erreur juste en dessous de ma zone de texte contenant le 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é quelques pinceaux ici, remplacez-les par votre propre ensemble de pinceaux que vous voulez pour votre messgae d'ornement. Peut-être que cela peut vous aider :

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

0voto

IslandCoder Points 1

J'ai rencontré 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 de lancer 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 d'application) :

xmlns:validators="clr-namespace:ParcelRatesViewModel.Validators;assembly=ParcelRatesViewModel"

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X