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