web-dev-qa-db-fra.com

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

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
enter image description here
WPF
enter image description here

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;
    }
}
59
Fredrik Hedblad

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

enter image description here
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 BackEaseEaseOutEasingFunction 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();
    }
}

enter image description here

103
Fredrik Hedblad

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.

  1. Tout d'abord, comme le remarque Brent , le texte est contraint par les bordures de la fenêtre propriétaire, de sorte que si le contrôle non valide est situé au bord de la fenêtre, le texte est coupé. La solution suggérée par Fredrik était de l'afficher "hors de la fenêtre". Cela a du sens pour moi.
  2. Deuxièmement, afficher le TextBlock à la droite du contrôle non valide n’est pas toujours optimal. Par exemple, disons que TextBlock est utilisé pour spécifier un fichier particulier à ouvrir et qu'il existe un bouton Parcourir à sa droite. Si l'utilisateur tape un fichier inexistant, l'erreur TextBlock couvrira le bouton Parcourir et empêchera éventuellement l'utilisateur de cliquer dessus pour corriger l'erreur. Ce qui a du sens pour moi, c’est d’afficher le message d’erreur en diagonale et à droite du contrôle non valide. Cela accomplit deux choses. Premièrement, cela évite de cacher les contrôles compagnons à la droite du contrôle non valide. Cela a également pour effet visuel que toolTipCorner est pointant vers le message d'erreur.

Voici le dialogue autour duquel j'ai réalisé mon développement.

Basic Dialog

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.

enter image description here

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.

  1. Il ressort des tests et des discussions en ligne que le Popup est considéré comme la fenêtre la plus haute. Ainsi, même lorsque mon application était masquée par une autre application, le Popup était toujours visible. C'était un comportement moins que souhaitable.
  2. L'autre problème était que s'il arrivait à l'utilisateur de déplacer ou de redimensionner la boîte de dialogue alors que le Popup était visible, le Popup ne se repositionnait pas pour conserver sa position par rapport à l'invalide. contrôle.

Heureusement, ces deux problèmes ont été résolus.

Voici le code. Commentaires et améliorations sont les bienvenus!


  • Fichier: ErrorTemplateSilverlightStyle.xaml
  • Espace de noms: 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">
      <!-- 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
  • Espace de noms: MyApp.Application.UI.Behaviors
  • Assembly: MyApp.Application.UI.dll

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

  • Fichier: ResourceLibrary.xaml
  • Espace de noms: 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>

        <!-- Styles -->
        ...

        <!-- Templates -->
        <ResourceDictionary Source="Templates/ErrorTemplateSilverlightStyle.xaml"/>

    </ResourceDictionary.MergedDictionaries>

    <!-- Converters -->
    ...

</ResourceDictionary>

  • Fichier: App.xaml
  • Espace de noms: MyApp.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
  • Espace de noms: MyApp.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>
35
Matt Davis

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>
3
Rohit Vats

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" 

0
IslandCoder