WPF中的验证错误样式,类似于Silverlight

时间:2011-09-15 16:27:25

标签: c# wpf silverlight styles errortemplate

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

Silverlight 4 中,验证错误很好地开箱即用。

以下是Silverlight 4和WPF中出现的验证错误的比较

Silverlight 4
enter image description here
WPF
enter image description here

请注意WPF版本的平坦,无聊的外观与我认为的Silverlight外观相比。

WPF框架中是否存在任何类似的验证样式/模板,或者是否有人创建了很好的样式验证模板,例如上面的 Silverlight 版本?或者我是否必须从头开始创建它们?

如果有人想尝试一下,可以使用以下代码重现上述验证错误,适用于 Silverlight WPF

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

4 个答案:

答案 0 :(得分:103)

我研究了验证错误模板的 Silverlight 版本并创建了一个 WPF 版本,看起来像这样

enter image description here
在帖子的底部添加了一个动画GIF但是在我完成之后我注意到它可能会因为移动鼠标而烦人。如果我要删除它,请告诉我.. :)

MultiBinding具有键盘焦点或鼠标位于右上角时,我使用BooleanOrConverterTextBox来显示“工具提示错误”。对于淡入动画,我使用了DoubleAnimation OpacityThicknessAnimation BackEase / EaseOut EasingFunction Margin 1}}

可以这样使用

<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

答案 1 :(得分:35)

这个答案只是扩展了Fredrik Hedblad的优秀答案。作为WPF和XAML的新手,Fredrik的答案充当了定义我希望如何在我的应用程序中显示验证错误的跳板。虽然下面的XAML对我有用,但它正在进行中。我还没有对它进行全面测试,我很乐意承认我无法完全解释每个标签。有了这些警告,我希望这对其他人有用。

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

  1. 首先,正如Brent注释所指出的那样,文本受拥有窗口的边界约束,这样如果无效控件位于窗口的边缘,则文本被切断。 Fredrik的建议解决方案是将其显示在“窗外”。这对我来说很有意义。
  2. 其次,显示无效控件右侧的TextBlock并不总是最佳的。例如,假设TextBlock用于指定要打开的特定文件,并且右侧有一个“浏览”按钮。如果用户键入不存在的文件,则错误TextBlock将覆盖“浏览”按钮,并可能阻止用户单击它以更正错误。对我来说有意义的是将错误消息对角显示在无效控件的右侧和右侧。这完成了两件事。首先,它避免将任何伴随控件隐藏在无效控件的右侧。它还具有 toolTipCorner 指向的视觉效果,可用于显示错误消息。
  3. 这是我完成开发的对话框。

    Basic Dialog

    如您所见,有两个TextBox控件需要验证。两者都相对靠近窗口的右边缘,因此可能会裁剪长错误消息。并注意第二个TextBox有一个浏览按钮,我不想在发生错误时隐藏它。

    所以这是使用我的实现的验证错误。

    enter image description here

    从功能上讲,它与Fredrik的实现非常相似。如果TextBox具有焦点,则会显示错误。一旦失去焦点,错误就会消失。如果用户将鼠标悬停在 toolTipCorner 上,则无论TextBox是否具有焦点,都会显示错误。还有一些装饰性的变化,例如 toolTipCorner 大50%(9像素对6像素)。

    当然,明显的区别在于我的实现使用Popup来显示错误。这解决了第一个缺点,因为Popup在其自己的窗口中显示其内容,因此它不受对话框边界的约束。但是,使用Popup确实需要克服几个挑战。

    1. 从测试和在线讨论中可以看出,Popup被认为是最顶层的窗口。因此,即使我的应用程序被另一个应用程序隐藏,Popup仍然可见。这是不太理想的行为。
    2. 另一个问题是,如果用户在Popup可见时碰巧移动或调整对话框大小,Popup没有重新定位自身以保持其相对于无效控件的位置。 / LI>

      幸运的是,这两项挑战都得到了解决。

      这是代码。欢迎提出意见和改进!


      • 文件: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

      注意:这需要表达混合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>
      

答案 2 :(得分:3)

我在其中一个项目中创建了自定义错误广告,以便在文本框下方显示错误广告,并在其中显示错误消息。您只需在文本框默认样式中设置属性“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>

答案 3 :(得分:0)

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

“PresentationFramework.dll中发生了'System.Windows.Markup.XamlParseException'类型的异常,但未在用户代码中处理”

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

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

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

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