c# wpf silverlight styles errortemplate

c# - Estilo de error de validación en WPF, similar a Silverlight



styles errortemplate (4)

Creé mi decoder de errores personalizado en uno de los proyectos para mostrar el adorno de error justo debajo de mi cuadro de texto con un mensaje de error. Solo necesita establecer la propiedad "Validation.ErrorTemplate" en su estilo predeterminado de cuadro de texto que puede guardar en los recursos de su aplicación para que se aplique a todos los cuadros de texto en su aplicación.

Nota: He usado algunos pinceles aquí, reemplázalo con tu propio conjunto de pinceles que quieras para tu decoder messgae. Puede ser esto puede ser de alguna ayuda:

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

Por defecto, Validation.ErrorTemplate en WPF es solo un pequeño borde rojo sin ToolTip .

En Silverlight 4 , el error de validación está muy bien diseñado desde el primer momento.

Aquí hay una comparación de un error de validación que ocurre en Silverlight 4 y WPF

Silverlight 4

WPF

Observe el aspecto realmente plano y aburrido de la versión de WPF en comparación con, en mi opinión, una gran apariencia en Silverlight.

¿Existen estilos / plantillas de validación similares en el Marco de WPF o alguien ha creado plantillas de validación de estilo agradable como la versión de Silverlight anterior? ¿O tendré que crearlos desde cero?

Si alguien quiere probarlo, el error de validación anterior puede reproducirse con el siguiente código, funciona tanto para Silverlight como para 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; } }


Esta respuesta simplemente se expande en la excelente respuesta de . Siendo nuevo en WPF y XAML, la respuesta de Fredrik sirvió como trampolín para definir cómo quería que los errores de validación se mostraran en mi aplicación. Mientras que el XAML a continuación funciona para mí, es un trabajo en progreso. No lo he probado completamente, y admitiré fácilmente que no puedo explicar completamente cada etiqueta. Con esas advertencias, espero que esto sea útil para otros.

Si bien el TextBlock animado es un buen enfoque, tiene dos deficiencias que quería abordar.

  1. En primer lugar, como observó el comentario de Brent , el texto está limitado por los bordes de la ventana propietaria, de modo que si el control no válido está en el borde de la ventana, el texto se corta. La solución sugerida por Fredrik fue mostrarla "fuera de la ventana". Eso tiene sentido para mí.
  2. En segundo lugar, mostrar el TextBlock a la derecha del control no válido no siempre es óptimo. Por ejemplo, supongamos que TextBlock se usa para especificar un archivo en particular para abrir y que hay un botón Examinar a su derecha. Si el usuario escribe un archivo inexistente, el error TextBlock cubrirá el botón Examinar y posiblemente evitará que el usuario haga clic en él para corregir el error. Lo que tiene sentido para mí es que el mensaje de error se muestre en diagonal hacia arriba y hacia la derecha del control no válido. Esto logra dos cosas. En primer lugar, evita ocultar los controles complementarios a la derecha del control no válido. También tiene el efecto visual de que toolTipCorner apunta hacia el mensaje de error.

Aquí está el diálogo alrededor del cual hice mi desarrollo.

Como puede ver, hay dos controles TextBox que deben validarse. Ambos están relativamente cerca del borde derecho de la ventana, por lo que los mensajes de error largos probablemente se recortarán. Y observe que el segundo TextBox tiene un botón Examinar que no quiero ocultar en caso de error.

Así que aquí está el error de validación usando mi implementación.

Funcionalmente, es muy similar a la implementación de Fredrik. Si TextBox tiene foco, el error estará visible. Una vez que pierde el foco, el error desaparece. Si el usuario coloca el mouse sobre la toolTipCorner , el error aparecerá independientemente de si TextBox tiene foco o no. También hay algunos cambios cosméticos, como toolTipCorner que es un 50% más grande (9 píxeles frente a 6 píxeles).

La diferencia obvia, por supuesto, es que mi implementación usa una Popup para mostrar el error. Esto resuelve el primer inconveniente porque Popup muestra sus contenidos en su propia ventana, por lo que no está limitado por los bordes del diálogo. Sin embargo, usar un Popup presentó algunos desafíos para superar.

  1. Según las pruebas y las discusiones en línea, parece que Popup se considera una ventana superior. Entonces, incluso cuando mi aplicación estaba oculta por otra aplicación, la Popup aún era visible. Este fue un comportamiento menos que deseable.
  2. El otro problema era que si el usuario movía o cambiaba el tamaño del cuadro de diálogo mientras el Popup estaba visible, el Popup no se reposicionaba para mantener su posición relativa al control no válido.

Afortunadamente, ambos desafíos se han abordado.

Aquí está el código. Comentarios y refinamientos son bienvenidos!

  • Archivo: ErrorTemplateSilverlightStyle.xaml
  • Espacio de nombres: MyApp.Application.UI.Templates
  • Asamblea: 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>

  • Archivo: RepositionPopupBehavior.cs
  • Espacio de nombres: MyApp.Application.UI.Behaviors
  • Asamblea: MyApp.Application.UI.dll

( NOTA: ESTO REQUIERE LA 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://.com/users/262204/nathanaw">NathanAW</see> and /// <see href="https://.com/users/718325/jason">Jason</see> to /// <see href="https://.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 } }

  • Archivo: ResourceLibrary.xaml
  • Espacio de nombres: MyApp.Application.UI
  • Asamblea: 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>

  • Archivo: App.xaml
  • Espacio de nombres: MyApp.Application
  • Asamblea: 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>

  • Archivo: NewProjectView.xaml
  • Espacio de nombres: MyApp.Application.Views
  • Asamblea: 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>


Estudié la versión de Silverlight de la Plantilla de error de validación y creé una versión de WPF que se parece a esta


Agregué un GIF animado en la parte inferior de la publicación, pero después de que lo terminé noté que podría ser molesto debido al movimiento del mouse en él. Avísame si debería eliminarlo ... :)

MultiBinding un MultiBinding con BooleanOrConverter para mostrar el "tooltip-error" cuando el TextBox tiene el foco del teclado o el mouse está sobre la esquina superior derecha. Para la animación de fundido de entrada utilicé una DoubleAnimation para la Opacity y una BackEase ThicknessAnimation con una función BackEase / EaseOut EasingFunction para el Margin

Utilizable de esta manera

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


Me encontré con un problema al tratar de aplicarlo a un proyecto de wpf en el que estoy trabajando. Si tiene el siguiente problema cuando intenta ejecutar el proyecto:

"Se produjo una excepción de tipo ''System.Windows.Markup.XamlParseException'' en PresentationFramework.dll pero no se manejó en el código de usuario"

Necesita crear una instancia de la clase booleanOrConverter en sus recursos (dentro de app.xaml):

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

Además, no olvide agregar el espacio de nombres al principio del archivo (en la etiqueta de la aplicación):

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