Этот ответ просто расширяется на Отличный ответ Фредрика Хедблада . Будучи новичком в WPF и XAML, ответ Фредрика послужил трамплином для определения того, как я хотел, чтобы ошибки валидации отображались в моем приложении. Хотя приведенный ниже XAML работает для меня, он находится в стадии разработки. Я не полностью проверил это, и я с готовностью признаю, что я не могу полностью объяснить каждый тег. С этими предостережениями я надеюсь, что это окажется полезным для других.
Хотя анимированный TextBlock - это прекрасный подход, у него есть два недостатка, которые я хотел бы устранить.
- Во-первых, как отмечается в комментарии Brent , текст ограничен границами окна-владельца, так что если недопустимый элемент управления находится на краю окна, текст обрезается. Предложенное Фредриком решение состояло в том, чтобы показать его «за окном». Это имеет смысл для меня.
- Во-вторых, показ TextBlock справа от недействительного элемента управления не всегда оптимален. Например, скажем, TextBlock используется для указания конкретного файла, который нужно открыть, и что справа от него есть кнопка Обзор. Если пользователь вводит несуществующий файл, ошибка TextBlock будет закрывать кнопку «Обзор» и, возможно, не позволит пользователю щелкнуть ее, чтобы исправить ошибку. Для меня имеет смысл выводить сообщение об ошибке по диагонали вверх и справа от недействительного элемента управления. Это выполняет две вещи. Во-первых, он избегает скрытия любых сопутствующих элементов управления справа от недействительного элемента управления. Он также имеет визуальный эффект, что toolTipCorner указывает на сообщение об ошибке.
Вот диалог, вокруг которого я сделал свою разработку.
Как видите, есть два элемента управления TextBox , которые необходимо проверить. Оба относительно близки к правому краю окна, поэтому длинные сообщения об ошибках, вероятно, будут обрезаны. И обратите внимание, что вторая TextBox имеет кнопку обзора, которую я не хочу скрывать в случае ошибки.
Итак, вот как выглядит ошибка проверки при использовании моей реализации.
Функционально это очень похоже на реализацию Фредрика. Если у TextBox есть фокус, ошибка будет видна. Как только он теряет фокус, ошибка исчезает. Если пользователь наводит указатель мыши на toolTipCorner , ошибка будет отображаться независимо от того, имеет ли фокус TextBox или нет. Также есть несколько косметических изменений, таких как toolTipCorner , который на 50% больше (9 пикселей против 6 пикселей).
Очевидное отличие, конечно, состоит в том, что моя реализация использует 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.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>