Browse Source

Merge pull request #1358 from sdoroff/data-validation-error

Created a shared DataValidationErrors framework
pull/1363/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
316da842bd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 135
      src/Avalonia.Controls/DataValidationErrors.cs
  2. 41
      src/Avalonia.Controls/TextBox.cs
  3. 38
      src/Avalonia.Themes.Default/DataValidationErrors.xaml
  4. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  5. 50
      src/Avalonia.Themes.Default/TextBox.xaml
  6. 34
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

135
src/Avalonia.Controls/DataValidationErrors.cs

@ -0,0 +1,135 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
namespace Avalonia.Controls
{
/// <summary>
/// A control which displays an error notifier when there is a DataValidationError.
/// Provides attached properties to track errors on a control
/// </summary>
/// <remarks>
/// You will probably only want to create instances inside of control templates.
/// </remarks>
public class DataValidationErrors : ContentControl
{
/// <summary>
/// Defines the DataValidationErrors.Errors attached property.
/// </summary>
public static readonly AttachedProperty<IEnumerable<Exception>> ErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, IEnumerable<Exception>>("Errors");
/// <summary>
/// Defines the DataValidationErrors.HasErrors attached property.
/// </summary>
public static readonly AttachedProperty<bool> HasErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, bool>("HasErrors");
public static readonly StyledProperty<IDataTemplate> ErrorTemplateProperty =
AvaloniaProperty.Register<DataValidationErrors, IDataTemplate>(nameof(ErrorTemplate));
private Control _owner;
public static readonly DirectProperty<DataValidationErrors, Control> OwnerProperty =
AvaloniaProperty.RegisterDirect<DataValidationErrors, Control>(
nameof(Owner),
o => o.Owner,
(o, v) => o.Owner = v);
public Control Owner
{
get { return _owner; }
set { SetAndRaise(OwnerProperty, ref _owner, value); }
}
/// <summary>
/// Initializes static members of the <see cref="DataValidationErrors"/> class.
/// </summary>
static DataValidationErrors()
{
ErrorsProperty.Changed.Subscribe(ErrorsChanged);
HasErrorsProperty.Changed.Subscribe(HasErrorsChanged);
TemplatedParentProperty.Changed.AddClassHandler<DataValidationErrors>(x => x.OnTemplatedParentChange);
}
private void OnTemplatedParentChange(AvaloniaPropertyChangedEventArgs e)
{
if (Owner == null)
{
Owner = (e.NewValue as Control);
}
}
public IDataTemplate ErrorTemplate
{
get { return GetValue(ErrorTemplateProperty); }
set { SetValue(ErrorTemplateProperty, value); }
}
private static void ErrorsChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
var errors = (IEnumerable<Exception>)e.NewValue;
var hasErrors = false;
if (errors != null && errors.Any())
hasErrors = true;
control.SetValue(HasErrorsProperty, hasErrors);
}
private static void HasErrorsChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
var classes = (IPseudoClasses)control.Classes;
classes.Set(":error", (bool)e.NewValue);
}
public static IEnumerable<Exception> GetErrors(Control control)
{
return control.GetValue(ErrorsProperty);
}
public static void SetErrors(Control control, IEnumerable<Exception> errors)
{
control.SetValue(ErrorsProperty, errors);
}
public static void SetError(Control control, Exception error)
{
SetErrors(control, UnpackException(error));
}
public static void ClearErrors(Control control)
{
SetErrors(control, null);
}
public static bool GetHasErrors(Control control)
{
return control.GetValue(HasErrorsProperty);
}
private static IEnumerable<Exception> UnpackException(Exception exception)
{
if (exception != null)
{
var aggregate = exception as AggregateException;
var exceptions = aggregate == null ?
(IEnumerable<Exception>)new[] { exception } :
aggregate.InnerExceptions;
var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList();
if (filtered.Count > 0)
{
return filtered;
}
}
return null;
}
}
}

41
src/Avalonia.Controls/TextBox.cs

@ -30,12 +30,7 @@ namespace Avalonia.Controls
nameof(CaretIndex),
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly DirectProperty<TextBox, IEnumerable<Exception>> DataValidationErrorsProperty =
AvaloniaProperty.RegisterDirect<TextBox, IEnumerable<Exception>>(
nameof(DataValidationErrors),
o => o.DataValidationErrors);
public static readonly StyledProperty<bool> IsReadOnlyProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(IsReadOnly));
@ -91,7 +86,6 @@ namespace Avalonia.Controls
private TextPresenter _presenter;
private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _ignoreTextChanges;
private IEnumerable<Exception> _dataValidationErrors;
private static readonly string[] invalidCharacters = new String[1]{"\u007f"};
static TextBox()
@ -142,13 +136,7 @@ namespace Avalonia.Controls
_undoRedoHelper.UpdateLastState();
}
}
public IEnumerable<Exception> DataValidationErrors
{
get { return _dataValidationErrors; }
private set { SetAndRaise(DataValidationErrorsProperty, ref _dataValidationErrors, value); }
}
public bool IsReadOnly
{
get { return GetValue(IsReadOnlyProperty); }
@ -553,31 +541,10 @@ namespace Avalonia.Controls
{
if (property == TextProperty)
{
var classes = (IPseudoClasses)Classes;
DataValidationErrors = UnpackException(status.Error);
classes.Set(":error", DataValidationErrors != null);
}
}
private static IEnumerable<Exception> UnpackException(Exception exception)
{
if (exception != null)
{
var aggregate = exception as AggregateException;
var exceptions = aggregate == null ?
(IEnumerable<Exception>)new[] { exception } :
aggregate.InnerExceptions;
var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList();
if (filtered.Count > 0)
{
return filtered;
}
DataValidationErrors.SetError(this, status.Error);
}
return null;
}
private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0);
private int CoerceCaretIndex(int value, int length)

38
src/Avalonia.Themes.Default/DataValidationErrors.xaml

@ -0,0 +1,38 @@
<Style xmlns="https://github.com/avaloniaui"
Selector="DataValidationErrors">
<Setter Property="Template">
<ControlTemplate>
<DockPanel LastChildFill="True">
<ContentControl DockPanel.Dock="Right"
ContentTemplate="{TemplateBinding ErrorTemplate}"
DataContext="{TemplateBinding Owner}"
Content="{Binding (DataValidationErrors.Errors)}"
IsVisible="{Binding (DataValidationErrors.HasErrors)}"/>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</DockPanel>
</ControlTemplate>
</Setter>
<Setter Property="ErrorTemplate">
<DataTemplate>
<Canvas Width="14" Height="14" Margin="4 0 1 0"
Background="#00FFFFFF">
<Canvas.Styles>
<Style Selector="ToolTip">
<Setter Property="Background" Value="{DynamicResource ErrorBrushLight}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/>
</Style>
</Canvas.Styles>
<ToolTip.Tip>
<ItemsControl Items="{Binding}" MemberSelector="Message"/>
</ToolTip.Tip>
<Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/>
</Canvas>
</DataTemplate>
</Setter>
</Style>

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -1,6 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui">
<!-- Define ToolTip first so its styles can be overriden by other controls (e.g. TextBox) -->
<StyleInclude Source="resm:Avalonia.Themes.Default.ToolTip.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.DataValidationErrors.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.FocusAdorner.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Button.xaml?assembly=Avalonia.Themes.Default"/>

50
src/Avalonia.Themes.Default/TextBox.xaml

@ -28,34 +28,26 @@
</TextBlock.IsVisible>
</TextBlock>
<DockPanel LastChildFill="True">
<Canvas Name="error" DockPanel.Dock="Right" Width="14" Height="14" Margin="4 0 1 0">
<ToolTip.Tip>
<ItemsControl Items="{TemplateBinding DataValidationErrors}" MemberSelector="Message"/>
</ToolTip.Tip>
<Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/>
</Canvas>
<DataValidationErrors>
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<Panel>
<TextBlock Name="watermark"
Opacity="0.5"
Text="{TemplateBinding Watermark}"
IsVisible="{TemplateBinding Path=Text, Converter={x:Static StringConverters.NullOrEmpty}}"/>
<TextPresenter Name="PART_TextPresenter"
Text="{TemplateBinding Text, Mode=TwoWay}"
CaretIndex="{TemplateBinding CaretIndex}"
SelectionStart="{TemplateBinding SelectionStart}"
SelectionEnd="{TemplateBinding SelectionEnd}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"/>
</Panel>
</ScrollViewer>
<Panel>
<TextBlock Name="watermark"
Opacity="0.5"
Text="{TemplateBinding Watermark}"
IsVisible="{TemplateBinding Path=Text, Converter={x:Static StringConverters.NullOrEmpty}}"/>
<TextPresenter Name="PART_TextPresenter"
Text="{TemplateBinding Text, Mode=TwoWay}"
CaretIndex="{TemplateBinding CaretIndex}"
SelectionStart="{TemplateBinding SelectionStart}"
SelectionEnd="{TemplateBinding SelectionEnd}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"/>
</Panel>
</ScrollViewer>
</DataValidationErrors>
</DockPanel>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
@ -69,14 +61,4 @@
<Style Selector="TextBox:error /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/>
</Style>
<Style Selector="TextBox /template/ Canvas#error">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="TextBox:error /template/ Canvas#error">
<Setter Property="IsVisible" Value="True"/>
</Style>
<Style Selector="TextBox /template/ ToolTip">
<Setter Property="Background" Value="{DynamicResource ErrorBrushLight}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/>
</Style>
</Styles>

34
tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

@ -42,7 +42,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Setter_Exceptions_Should_Set_DataValidationErrors()
public void Setter_Exceptions_Should_Set_DataValidationErrors_Errors()
{
using (UnitTestApplication.Start(Services))
{
@ -55,12 +55,36 @@ namespace Avalonia.Controls.UnitTests
target.ApplyTemplate();
Assert.Null(target.DataValidationErrors);
Assert.Null(DataValidationErrors.GetErrors(target));
target.Text = "20";
Assert.Single(target.DataValidationErrors);
Assert.IsType<InvalidOperationException>(target.DataValidationErrors.Single());
IEnumerable<Exception> errors = DataValidationErrors.GetErrors(target);
Assert.Single(errors);
Assert.IsType<InvalidOperationException>(errors.Single());
target.Text = "1";
Assert.Null(DataValidationErrors.GetErrors(target));
}
}
[Fact]
public void Setter_Exceptions_Should_Set_DataValidationErrors_HasErrors()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
DataContext = new ExceptionTest(),
[!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay),
Template = CreateTemplate(),
};
target.ApplyTemplate();
Assert.False(DataValidationErrors.GetHasErrors(target));
target.Text = "20";
Assert.True(DataValidationErrors.GetHasErrors(target));
target.Text = "1";
Assert.Null(target.DataValidationErrors);
Assert.False(DataValidationErrors.GetHasErrors(target));
}
}

Loading…
Cancel
Save