developer tip

Silverlight와 유사한 WPF의 유효성 검사 오류 스타일

optionbox 2020. 12. 7. 08:04
반응형

Silverlight와 유사한 WPF의 유효성 검사 오류 스타일


기본적으로 Validation.ErrorTemplatein WPFToolTip.

에서 실버 라이트 4 , 유효성 검사 오류가 멋지게 박스를-의 스타일입니다.

다음은 Silverlight 4 및 WPF에서 발생하는 유효성 검사 오류를 비교 한 것입니다.

Silverlight 4
여기에 이미지 설명 입력
WPF
여기에 이미지 설명 입력

제 생각에 Silverlight의 멋진 모습과 비교했을 때 WPF 버전의 정말 평평하고 지루한 모습에 주목하세요.

WPF 프레임 워크에 유사한 유효성 검사 스타일 / 템플릿이 있습니까? 아니면 위 Silverlight 버전 과 같은 멋진 스타일의 유효성 검사 템플릿을 만든 사람이 있습니까? 아니면 처음부터 만들어야합니까?

누구나 시도해보고 싶다면 위의 유효성 검사 오류를 다음 코드로 재현 할 수 있으며 SilverlightWPF 모두에서 작동합니다.

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

Silverlight 버전의 유효성 검사 오류 템플릿을 연구하고 다음과 같은 WPF 버전을 만들었습니다.

여기에 이미지 설명 입력
게시물 하단에 애니메이션 GIF를 추가했지만 작업을 마친 후 마우스가 움직여서 짜증이 날 수 있음을 알았습니다. 제거해야하는지 알려주세요 .. :)

키보드 포커스가 있거나 마우스가 오른쪽 상단 모서리에 있을 때 "도구 설명 오류"를 표시 MultiBinding하기 BooleanOrConverter위해 a 사용 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();
    }
}

여기에 이미지 설명 입력


이 답변은 Fredrik Hedblad 의 탁월한 답변으로 확장됩니다 . WPF 및 XAML을 처음 접한 Fredrik의 답변은 애플리케이션에 유효성 검사 오류를 표시하는 방법을 정의하는 발판 역할을했습니다. 아래 XAML은 저에게 효과적이지만 진행중인 작업입니다. 나는 그것을 완전히 테스트하지 않았으며 모든 태그를 완전히 설명 할 수 없다는 것을 쉽게 인정할 것입니다. 이러한 경고와 함께 이것이 다른 사람들에게 유용하다는 것이 증명되기를 바랍니다.

애니메이션 TextBlock 은 훌륭한 접근 방식이지만 제가 해결하고 싶은 두 가지 단점이 있습니다.

  1. 첫째, Brent 의 의견이 언급했듯이 텍스트는 소유 창의 테두리에 의해 제한되므로 잘못된 컨트롤이 창의 가장자리에 있으면 텍스트가 잘립니다. Fredrik이 제안한 해결책은 "창 밖에"표시하는 것이 었습니다. 그것은 나에게 의미가 있습니다.
  2. 둘째, 잘못된 컨트롤의 오른쪽에 TextBlock표시하는 것이 항상 최적의 것은 아닙니다. 예를 들어 TextBlock 을 사용하여 열 특정 파일을 지정하고 오른쪽에 찾아보기 버튼이 있다고 가정합니다. 사용자가 존재하지 않는 파일을 입력하면 오류 TextBlock 이 찾아보기 버튼을 덮고 잠재적으로 사용자가 실수를 수정하기 위해 클릭하지 못하게합니다. 나에게 의미가있는 것은 오류 메시지가 유효하지 않은 컨트롤의 오른쪽에 대각선으로 표시되도록하는 것입니다. 이것은 두 가지를 수행합니다. 첫째, 잘못된 컨트롤의 오른쪽에 컴패니언 컨트롤을 숨기지 않습니다. 또한 toolTipCorner오류 메시지를 가리키는 시각적 효과가 있습니다.

여기에 제가 개발 한 대화가 있습니다.

기본 대화 상자

보시다시피 유효성을 검사해야하는 두 개의 TextBox 컨트롤이 있습니다. 둘 다 창의 오른쪽 가장자리에 비교적 가깝기 때문에 긴 오류 메시지가 잘릴 수 있습니다. 두 번째 TextBox 에는 오류 발생시 숨기고 싶지 않은 찾아보기 버튼이 있습니다.

그래서 여기 내 구현을 사용하여 유효성 검사 오류가 어떻게 생겼는지 보여줍니다.

여기에 이미지 설명 입력

기능적으로는 Fredrik의 구현과 매우 유사합니다. TextBox 에 포커스가 있으면 오류가 표시됩니다. 초점을 잃으면 오류가 사라집니다. 사용자가 toolTipCorner 위로 마우스를 가져 가면 TextBox 에 포커스가 있는지 여부에 관계없이 오류가 나타납니다 . toolTipCorner 가 50 % 더 큰 (9 픽셀 대 6 픽셀) 과 같은 몇 가지 외관상의 변경 사항도 있습니다 .

물론 명백한 차이점은 내 구현에서 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

( 참고 : 이것은 EXPRESSION BLEND 4 System.Windows.Interactivity 어셈블리가 필요합니다)

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>

I have created my custom error adorner in one of the projects to show the error adorner just below my textbox with error message in it. You just need to set the property "Validation.ErrorTemplate" in your textbox default style which you can keep in your app resources so that it get applied to all the textboxes in your application.

Note: I have used some brushes here, replace it with your own set of brushes which you want for your adorner messgae. May be this can be of some help :

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

I ran into an issue with this when trying to apply it to a wpf project I'm working on. If you're having the following issue when you try to run the project:

"An exception of type 'System.Windows.Markup.XamlParseException' occurred in PresentationFramework.dll but was not handled in user code"

리소스 (app.xaml 내)에 booleanOrConverter 클래스의 인스턴스를 만들어야합니다.

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

또한 파일 상단 (애플리케이션 태그)에 네임 스페이스를 추가하는 것을 잊지 마십시오.

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

참고 URL : https://stackoverflow.com/questions/7434245/validation-error-style-in-wpf-similar-to-silverlight

반응형