diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs new file mode 100644 index 0000000000..a55bd63aa8 --- /dev/null +++ b/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 +{ + /// + /// A control which displays an error notifier when there is a DataValidationError. + /// Provides attached properties to track errors on a control + /// + /// + /// You will probably only want to create instances inside of control templates. + /// + public class DataValidationErrors : ContentControl + { + /// + /// Defines the DataValidationErrors.Errors attached property. + /// + public static readonly AttachedProperty> ErrorsProperty = + AvaloniaProperty.RegisterAttached>("Errors"); + + /// + /// Defines the DataValidationErrors.HasErrors attached property. + /// + public static readonly AttachedProperty HasErrorsProperty = + AvaloniaProperty.RegisterAttached("HasErrors"); + + public static readonly StyledProperty ErrorTemplateProperty = + AvaloniaProperty.Register(nameof(ErrorTemplate)); + + + private Control _owner; + + public static readonly DirectProperty OwnerProperty = + AvaloniaProperty.RegisterDirect( + nameof(Owner), + o => o.Owner, + (o, v) => o.Owner = v); + + public Control Owner + { + get { return _owner; } + set { SetAndRaise(OwnerProperty, ref _owner, value); } + } + + /// + /// Initializes static members of the class. + /// + static DataValidationErrors() + { + ErrorsProperty.Changed.Subscribe(ErrorsChanged); + HasErrorsProperty.Changed.Subscribe(HasErrorsChanged); + TemplatedParentProperty.Changed.AddClassHandler(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)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 GetErrors(Control control) + { + return control.GetValue(ErrorsProperty); + } + public static void SetErrors(Control control, IEnumerable 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 UnpackException(Exception exception) + { + if (exception != null) + { + var aggregate = exception as AggregateException; + var exceptions = aggregate == null ? + (IEnumerable)new[] { exception } : + aggregate.InnerExceptions; + var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList(); + + if (filtered.Count > 0) + { + return filtered; + } + } + + return null; + } + } +} diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 8b37689591..7366ff3f91 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/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> DataValidationErrorsProperty = - AvaloniaProperty.RegisterDirect>( - nameof(DataValidationErrors), - o => o.DataValidationErrors); - + public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register(nameof(IsReadOnly)); @@ -91,7 +86,6 @@ namespace Avalonia.Controls private TextPresenter _presenter; private UndoRedoHelper _undoRedoHelper; private bool _ignoreTextChanges; - private IEnumerable _dataValidationErrors; private static readonly string[] invalidCharacters = new String[1]{"\u007f"}; static TextBox() @@ -142,13 +136,7 @@ namespace Avalonia.Controls _undoRedoHelper.UpdateLastState(); } } - - public IEnumerable 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 UnpackException(Exception exception) - { - if (exception != null) - { - var aggregate = exception as AggregateException; - var exceptions = aggregate == null ? - (IEnumerable)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) diff --git a/src/Avalonia.Themes.Default/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/DataValidationErrors.xaml new file mode 100644 index 0000000000..f7f28d90d0 --- /dev/null +++ b/src/Avalonia.Themes.Default/DataValidationErrors.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index ae00f0757a..6250282c2a 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -1,6 +1,7 @@ + diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 9d9f322150..e228aebf58 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -28,34 +28,26 @@ - - - - - - - - + - - - - - + + + + + + - - @@ -69,14 +61,4 @@ - - - \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index daf2715d84..851d741eae 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/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(target.DataValidationErrors.Single()); + + IEnumerable errors = DataValidationErrors.GetErrors(target); + Assert.Single(errors); + Assert.IsType(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)); } }