Подтвердить что ты не робот

Стиль ошибки проверки в WPF, аналогичный Silverlight

По умолчанию Validation.ErrorTemplate в WPF - это небольшая красная рамка без ToolTip.

В Silverlight 4 ошибка проверки корректно оформлена из коробки.

Ниже приведено сравнение ошибки проверки в Silverlight 4 и WPF

Silverlight 4
enter image description here
WPF
enter image description here

Обратите внимание на действительно плоский, скучный вид версии WPF по сравнению с, на мой взгляд, замечательным взглядом в Silverlight.

Существуют ли какие-либо подобные стили/шаблоны валидации в WPF Framework или кто-нибудь создал красиво оформленные шаблоны проверки, такие как версия Silverlight выше? Или мне придется создавать их с нуля?

Если кто-то хочет попробовать, ошибка проверки выше может быть воспроизведена с помощью следующего кода, работает как для Silverlight, так и 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;
    }
}
4b9b3361

Ответ 1

Я изучил версию Silverlight шаблона ошибки проверки и создал версию WPF, которая выглядит так:

enter image description here
Добавил анимированный GIF в нижней части сообщения, но после того, как я его закончил, я заметил, что это может раздражать из-за движущейся мыши в нем. Дайте мне знать, если я его удалю..:)

Я использовал MultiBinding с BooleanOrConverter, чтобы показать "подсказку-ошибку", когда TextBox имеет фокус клавиатуры или мышь находится над верхним правом углом. Для анимации затухания я использовал DoubleAnimation для Opacity и a ThicknessAnimation с 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();
    }
}

enter image description here

Ответ 2

Этот ответ просто расширяет Fredrik Hedblad отличный ответ. Будучи новым для WPF и XAML, ответ Fredrik служил плацдармом для определения того, как я хотел, чтобы ошибки проверки отображались в моем приложении. Хотя XAML ниже работает для меня, это незавершенное производство. Я не полностью его протестировал, и я с готовностью признаю, что не могу полностью объяснить каждый тег. С этими оговорками я надеюсь, что это окажется полезным для других.

В то время как анимированный TextBlock - прекрасный подход, у него есть два недостатка, которые я хотел адресовать.

  • Во-первых, поскольку комментарий Brent, текст ограничен границами окна использования, так что если недопустимый элемент управления находится на краю окна, текст отключен. Фредрик предложил решение, чтобы оно отображалось "за окном". Это имеет смысл для меня.
  • Во-вторых, отображение TextBlock справа от недопустимого элемента управления не всегда оптимально. Например, скажем, TextBlock используется, чтобы указать конкретный файл для открытия и что справа от него кнопка "Обзор". Если пользователь вводит в несуществующий файл, ошибка TextBlock будет охватывать кнопку "Обзор" и ​​потенциально может запретить пользователю щелкнуть по ней исправьте ошибку. Для меня смысл иметь сообщение об ошибке, отображаемое по диагонали вверх и справа от недействительного элемента управления. Это выполняет две вещи. Во-первых, он избегает скрывать любые вспомогательные элементы управления справа от недействительного элемента управления. Он также имеет визуальный эффект, что toolTipCorner указывает на сообщение об ошибке.

Вот диалог, вокруг которого я сделал свое развитие.

Basic Dialog

Как вы можете видеть, есть два элемента TextBox, которые необходимо проверить. Оба они относительно близки к правому краю окна, поэтому длинные сообщения об ошибках, вероятно, будут обрезаны. И обратите внимание, что второй TextBox имеет кнопку "Обзор", которую я не хочу скрывать в случае ошибки.

Итак, как выглядит ошибка проверки, использующая мою реализацию.

enter image description here

Функционально он очень похож на реализацию Fredrik. Если фокус TextBox, ошибка будет видна. Как только он теряет фокус, ошибка исчезает. Если пользователь наводит курсор мыши на toolTipCorner, ошибка будет отображаться независимо от того, имеет ли фокус или нет TextBox. Есть также несколько косметических изменений, таких как toolTipCorner, на 50% больше (9 пикселей против 6 пикселей).

Очевидная разница, конечно, в том, что моя реализация использует Popup для отображения ошибки. Это устраняет первый недостаток, потому что Popup отображает его содержимое в своем собственном окне, поэтому он не ограничен границами диалога. Однако использование Popup действительно представляло пару проблем для преодоления.

  • Из тестирования и онлайн-дискуссий видно, что Popup считается самым верхним окном. Таким образом, даже когда мое приложение было скрыто другим приложением, Popup все еще был видимым. Это поведение было менее желательным.
  • Другой способ заключался в том, что если пользователь переместил или изменил размер диалогового окна, когда был показан 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

( ПРИМЕЧАНИЕ: ЭТОТ ТРЕБУЕТСЯ ВЫРАЖЕНИЯ ЭКСПРЕССИИ 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 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>

Ответ 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>

Ответ 4

Я столкнулся с проблемой при попытке применить его к проекту wpf, над которым я работаю. Если у вас возникла следующая проблема при попытке запустить проект:

"Исключение типа" System.Windows.Markup.XamlParseException "произошло в PresentationFramework.dll, но не было обработано в коде пользователя"

Вам необходимо создать экземпляр класса booleanOrConverter в ваших ресурсах (в приложении app.xaml):

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

Также не забудьте добавить пространство имен в начало файла (в теге приложения):

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