it-swarm.com.de

Validierungsfehlerstil in WPF, ähnlich wie bei Silverlight

Standardmäßig ist der Validation.ErrorTemplate in WPF nur eine kleine rote Umrandung ohne ToolTip.

In Silverlight 4 ist der Überprüfungsfehler sofort einsatzbereit. 

Hier ist ein Vergleich eines Validierungsfehlers in Silverlight 4 und WPF

Silverlight 4
enter image description here
WPF
enter image description here

Beachten Sie den wirklich flachen, langweiligen Look der WPF-Version im Vergleich zu dem aus meiner Sicht großartigen Look in Silverlight.

Gibt es ähnliche Überprüfungsstile/-vorlagen im WPF-Framework oder hat jemand schön gestaltete Überprüfungsvorlagen wie die Silverlight -Version oben erstellt? Oder muss ich sie von Grund auf neu erstellen?

Wenn jemand es ausprobieren möchte, kann der obige Validierungsfehler mit dem folgenden Code reproduziert werden. Er funktioniert sowohl für Silverlight als auch für 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

Ich habe die Silverlight - Version der Validierungsfehlervorlage studiert und eineWPF- Version davon erstellt, die so aussieht

enter image description here
Ein animiertes GIF wurde am Ende des Beitrags hinzugefügt, aber als ich fertig war, bemerkte ich, dass es aufgrund der sich bewegenden Maus möglicherweise nervt. Lass es mich wissen, wenn ich es entfernen soll .. :)

Ich habe eine MultiBinding mit einer BooleanOrConverter verwendet, um den "Tooltip-Fehler" anzuzeigen, wenn die TextBox den Tastaturfokus hat oder sich die Maus über der rechten oberen Ecke befindet. Für die Einblendanimation habe ich eine DoubleAnimation für die Opacity und eine ThicknessAnimation mit einer BackEaseEaseOutEasingFunction für die Margin

So verwendbar

<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

Diese Antwort erweitert lediglich die ausgezeichnete Antwort von Fredrik Hedblad . Fredriks Antwort war für WPF und XAML neu und diente als Sprungbrett, um zu definieren, wie Validierungsfehler in meiner Anwendung angezeigt werden sollen. Während die unten stehende XAML für mich funktioniert, ist sie noch in Arbeit. Ich habe es nicht vollständig getestet und ich gebe gerne zu, dass ich nicht jeden Tag vollständig erklären kann. Mit diesen Einschränkungen hoffe ich, dass sich dies für andere als nützlich erweist.

Während der animierte TextBlock ein guter Ansatz ist, hat er zwei Mängel, die ich ansprechen wollte.

  1. Zunächst wird der Text, wie im Kommentar von Brent vermerkt, durch die Rahmen des Besitzers des Fensters eingeschränkt, sodass der Text ausgeschnitten wird, wenn sich das ungültige Steuerelement am Rand des Fensters befindet aus. Fredriks Lösungsvorschlag bestand darin, es "außerhalb des Fensters" anzeigen zu lassen. Das ergibt für mich einen Sinn.
  2. Zweitens ist das Anzeigen des TextBlock rechts vom ungültigen Steuerelement nicht immer optimal. Angenommen, der TextBlock wird verwendet, um eine bestimmte zu öffnende Datei anzugeben. Rechts daneben befindet sich eine Schaltfläche zum Durchsuchen. Wenn der Benutzer eine nicht vorhandene Datei eingibt, wird der Fehler TextBlock die Schaltfläche Durchsuchen verdecken und den Benutzer möglicherweise daran hindern, darauf zu klicken, um den Fehler zu korrigieren. Für mich ist es sinnvoll, die Fehlermeldung diagonal nach oben und rechts vom ungültigen Steuerelement anzeigen zu lassen. Damit werden zwei Dinge erreicht. Erstens wird vermieden, dass Companion-Steuerelemente rechts vom ungültigen Steuerelement ausgeblendet werden. Es hat auch den visuellen Effekt, dass toolTipCorner auf die Fehlermeldung zeigt .

Hier ist der Dialog, um den ich meine Entwicklung gemacht habe.

Basic Dialog

Wie Sie sehen, müssen zwei TextBox Steuerelemente überprüft werden. Beide befinden sich relativ nahe am rechten Fensterrand, sodass lange Fehlermeldungen wahrscheinlich abgeschnitten werden. Und beachten Sie, dass die zweite TextBox eine Schaltfläche zum Durchsuchen hat, die ich im Fehlerfall nicht verbergen möchte.

So sieht ein Validierungsfehler mit meiner Implementierung aus.

enter image description here

Funktionell ist es der Implementierung von Fredrik sehr ähnlich. Wenn das Feld TextBox den Fokus hat, wird der Fehler angezeigt. Sobald es den Fokus verliert, verschwindet der Fehler. Wenn der Benutzer mit der Maus über toolTipCorner fährt, wird der Fehler unabhängig davon angezeigt, ob das TextBox den Fokus hat oder nicht. Es gibt auch ein paar kosmetische Änderungen, wie zum Beispiel, dass toolTipCorner 50% größer ist (9 Pixel gegenüber 6 Pixel).

Der offensichtliche Unterschied besteht natürlich darin, dass meine Implementierung ein Popup verwendet, um den Fehler anzuzeigen. Dies behebt den ersten Mangel, da das Popup seinen Inhalt in einem eigenen Fenster anzeigt, so dass es nicht an die Rahmen des Dialogfelds gebunden ist. Die Verwendung von Popup war jedoch mit einigen Herausforderungen verbunden.

  1. Aus Tests und Online-Diskussionen geht hervor, dass das Popup als oberstes Fenster betrachtet wird. Selbst wenn meine Anwendung von einer anderen Anwendung ausgeblendet wurde, war das Popup immer noch sichtbar. Dies war ein unerwünschtes Verhalten.
  2. Das andere Problem war, dass, wenn der Benutzer das Dialogfeld verschoben oder in der Größe geändert hat, während Popup sichtbar war, Popup Ich habe mich nicht neu positioniert, um seine Position in Bezug auf die ungültige Kontrolle beizubehalten.

Glücklicherweise wurden diese beiden Herausforderungen angegangen.

Hier ist der Code. Kommentare und Verfeinerungen sind willkommen!


  • Datei: 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">
      <!-- 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>

  • Datei: RepositionPopupBehavior.cs
  • Namespace: MyApp.Application.UI.Behaviors
  • Assembly: MyApp.Application.UI.dll

( HINWEIS: Dies erfordert die EXPRESSION BLEND 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
    }
}

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

        <!-- Styles -->
        ...

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

    </ResourceDictionary.MergedDictionaries>

    <!-- Converters -->
    ...

</ResourceDictionary>

  • Datei: App.xaml
  • Namespace: MyApp.Application
  • Assembly: 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>

  • Datei: NewProjectView.xaml
  • Namespace: MyApp.Application.Views
  • Assembly: 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

Ich habe meinen benutzerdefinierten Fehleranzeiger in einem der Projekte erstellt, um den Fehleranzeiger direkt unter meinem Textfeld mit einer Fehlermeldung anzuzeigen. Sie müssen lediglich die Eigenschaft "Validation.ErrorTemplate" in Ihrem Textfeld-Standardstil festlegen, den Sie in Ihren App-Ressourcen beibehalten können, sodass sie auf alle Textfelder in Ihrer Anwendung angewendet wird.

Hinweis: Ich habe hier einige Bürsten verwendet. Ersetzen Sie sie durch Ihre eigenen Bürsten, die Sie für Ihre Adorner Messgae benötigen. Vielleicht kann dies etwas helfen:

<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

Ich hatte ein Problem damit, als ich versuchte, es auf ein Wpf-Projekt anzuwenden, an dem ich gerade arbeite. Wenn beim Ausführen des Projekts das folgende Problem auftritt:

"Eine Ausnahme des Typs 'System.Windows.Markup.XamlParseException' ist in PresentationFramework.dll aufgetreten, wurde aber nicht im Benutzercode behandelt."

Sie müssen eine Instanz der booleanOrConverter-Klasse in Ihren Ressourcen (in app.xaml) erstellen:

<validators:BooleanOrConverter x:Key="myConverter" />

Vergessen Sie auch nicht, den Namespace am Anfang der Datei (im Anwendungs-Tag) hinzuzufügen:

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

0
IslandCoder