Переглянути джерело

Created a shared DataValidationErrors framework

Moved Validation Error tracking to attached an attached property
Created an shared error indicator that can be simply added to control
templates and re-styled across all controls
This should reduce the need for code and markup duplication while
allowing for a consistent visual interface
sdoroff 7 роки тому
батько
коміт
5dce65f7b6

+ 135 - 0
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;
+        }
+    }
+}

+ 4 - 37
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 - 0
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 - 0
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"/>

+ 16 - 34
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>

+ 29 - 5
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));
             }
         }