validation错误WPF风格,类似于Silverlight

默认情况下, WPF中Validation.ErrorTemplate只是一个没有任何ToolTip的小红色边框。

Silverlight 4中 ,validation错误是非常好的样式。

以下是Silverlight 4和WPF中的validation错误的比较

Silverlight 4
在这里输入图像描述
WPF
在这里输入图像描述

注意到WPF版本的平坦,无聊的外观,相比之下,在我看来,在Silverlight的好看。

在WPF框架中是否存在任何类似的validation样式/模板,或者是否有人创build了像上面的Silverlight版本那样的很好的样式validation模板? 或者我将不得不从头开始创build它们?

如果有人想尝试一下,上面的validation错误可以重现与下面的代码,适用于SilverlightWPF

主窗口/ 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> 

主窗口/ 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; } } 

我研究了validation错误模板的Silverlight版本,并创build了它的WPF版本,如下所示

在这里输入图像描述
在post的底部添加了一个animationGIF,但是在完成之后,我注意到它可能因为鼠标移动而变得烦人。 让我知道如果我应该删除它.. 🙂

TextBox有键盘焦点或鼠标在右上angular时,我用一个带有BooleanOrConverterMultiBinding来显示“tooltip-error”。 对于淡入animation,我使用了DoubleAnimation作为OpacityThicknessAnimationanimation使用了BackEase / EaseOut EasingFunction作为Margin

像这样可用

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

在这里输入图像描述

这个答案仅仅扩展了Fredrik Hedblad的出色答案。 作为WPF和XAML的新手,Fredrik的回答充当了定义如何在我的应用程序中显示validation错误的跳板。 下面的XAML适用于我,这是一个正在进行的工作。 我还没有完全testing,我很乐意承认,我不能完全解释每一个标签。 有了这些警告,我希望这certificate对他人有用。

虽然animation的TextBlock是一个很好的方法,但它有两个缺点,我想解决。

  1. 首先,正如布伦特的评论所指出的那样,文本被拥有窗口的边界所约束,使得如果无效控制位于窗口的边缘,则文本被切断。 弗雷德里克build议的解决scheme是让它在“窗外”显示。 这对我有意义。
  2. 其次,显示无效控件右侧的TextBlock并不总是最佳的。 例如,假设TextBlock用于指定要打开的特定文件,并且右侧有一个“浏览”button。 如果用户键入一个不存在的文件,错误TextBlock将覆盖浏览button,并可能阻止用户点击它来纠正错误。 对我来说是有意义的是有错误信息斜向上显示在无效控制的右侧。 这完成了两件事。 首先,它避免了隐藏对无效控制权的任何同伴控制。 它还具有toolTipCorner 指向错误消息的视觉效果。

这里是我已经完成了我的发展的对话框。

基本对话框

正如你所看到的,有两个TextBox控件需要validation。 两者都比较靠近窗口的右边缘,所以长时间的错误消息可能会被裁剪。 并注意第二个TextBox有一个浏览button,我不想隐藏在一个错误的事件。

所以这是一个validation错误看起来像使用我的实现。

在这里输入图像描述

在function上,它与Fredrik的实现非常相似。 如果文本框有焦点,错误将是可见的。 一旦失去焦点,错误消失。 如果用户将鼠标hover在toolTipCorner上 ,则无论TextBox是否具有焦点,都会显示错误。 还有一些外观变化,比如toolTipCorner的大小是50%(9像素与6像素)。

当然,明显的区别是我的实现使用Popup来显示错误。 这解决了第一个缺点,因为Popup在其自己的窗口中显示其内容,所以它不受对话框边界的限制。 但是,使用Popup确实带来了一些挑战。

  1. 从testing和在线讨论中可以看出, Popup被认为是最重要的窗口。 所以,即使当我的应用程序被另一个应用程序隐藏时, Popup仍然可见。 这是不理想的行为。
  2. 另一个问题是,如果用户在Popup可见的时候碰巧移动了对话框或者调整了对话框的大小,那么Popup并没有重新定位自己以保持相对于无效控件的位置。

幸运的是,这两个挑战都已经解决了。

这是代码。 欢迎评论和改进!


  • 文件:ErrorTemplateSilverlightStyle.xaml
  • 命名空间:MyApp.Application.UI.Templates
  • 程序集: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> 

  • 文件:RepositionPopupBehavior.cs
  • 命名空间:MyApp.Application.UI.Behaviors
  • 程序集:MyApp.Application.UI.dll

注意:这需要expression式混合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 } } 

  • 文件:ResourceLibrary.xaml
  • 命名空间:MyApp.Application.UI
  • 程序集: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> 

  • 文件:App.xaml
  • 命名空间:MyApp.Application
  • 大会: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> 

  • 文件:NewProjectView.xaml
  • 命名空间:MyApp.Application.Views
  • 大会: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> 

我在其中一个项目中创build了我的自定义错误装饰器,以在我的文本框下面显示错误装饰器,并在其中显示错误消息。 您只需在文本框默认样式中设置属性“Validation.ErrorTemplate”,您可以将其保留在应用程序资源中,以便将其应用于应用程序中的所有文本框。

注意:我在这里使用了一些笔刷,用你自己想要的装饰笔刷代替它。 可能是这样可以得到一些帮助:

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

当我尝试将它应用到正在处理的wpf项目时,遇到了一个问题。 如果在尝试运行项目时遇到以下问题:

“PresentationFramework.dll中出现”System.Windows.Markup.XamlParseException“types的exception,但未在用户代码中处理”

您需要在资源(在app.xaml中)中创buildbooleanOrConverter类的实例:

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

另外不要忘记将命名空间添加到文件的顶部(在应用程序标签中):

的xmlns:validation= “CLR-名称空间:ParcelRatesViewModel.Validators;assembly= ParcelRatesViewModel”