Стиль ошибки проверки в WPF, аналогичный Silverlight - PullRequest
59 голосов
/ 15 сентября 2011

По умолчанию 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;
    }
}

Ответы [ 4 ]

103 голосов
/ 16 сентября 2011

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

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

Я использовал MultiBinding с BooleanOrConverter, чтобы показать «ошибку всплывающей подсказки», когда TextBox имеет фокус клавиатуры или мышь находится в верхнем правом углу. Для постепенной анимации я использовал DoubleAnimation для Opacity и 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

35 голосов
/ 05 декабря 2013

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

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

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

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

Basic Dialog

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

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

enter image description here

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

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

  1. Из тестирования и онлайн-дискуссий следует, что Popup считается самым верхним окном. Таким образом, даже когда мое приложение было скрыто другим приложением, Popup все еще было видно. Это было нежелательное поведение.
  2. Другая проблема заключалась в том, что если пользователь перемещал или изменял размер диалогового окна, пока было отображено всплывающее окно , всплывающее окно не перемещалось, чтобы сохранить свою позицию относительно неверный контроль.

К счастью, обе эти проблемы были решены.

Вот код. Комментарии и уточнения приветствуются!


  • Файл: 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.Interacctivity 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="/839344/kak-ya-mogu-peremestit-vsplyvayschee-okno-wpf-pri-peremeschenii-ego-elementa-privyazki">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>
3 голосов
/ 15 сентября 2011

Я создал свой собственный обработчик ошибок в одном из проектов, чтобы отобразить его как раз под моим текстовым полем с сообщением об ошибке. Вам просто нужно установить свойство 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>
0 голосов
/ 31 мая 2017

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

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

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

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

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

xmlns: validators = "clr-namespace: ParcelRatesViewModel.Validators; сборка = ParcelRatesViewModel"

...