Explorar o código

Added tests for styled property data validation.

Steven Kirk %!s(int64=2) %!d(string=hai) anos
pai
achega
eabc9493fa

+ 165 - 93
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

@@ -1,115 +1,170 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.UnitTests;
 using Xunit;
 
+#nullable enable
+
 namespace Avalonia.Base.UnitTests
 {
     public class AvaloniaObjectTests_DataValidation
     {
-        [Fact]
-        public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation()
+        public abstract class TestBase<T>
+            where T : AvaloniaProperty<int>
         {
-            var target = new Class1();
-            var source = new Subject<BindingValue<int>>();
+            [Fact]
+            public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
+            {
+                var target = new Class1();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetNonValidatedProperty();
 
-            target.Bind(Class1.NonValidatedProperty, source);
-            source.OnNext(6);
-            source.OnNext(BindingValue<int>.BindingError(new Exception()));
-            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
-            source.OnNext(6);
+                target.Bind(property, source);
+                source.OnNext(6);
+                source.OnNext(BindingValue<int>.BindingError(new Exception()));
+                source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
+                source.OnNext(6);
 
-            Assert.Empty(target.Notifications);
-        }
+                Assert.Empty(target.Notifications);
+            }
 
-        [Fact]
-        public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
-        {
-            var target = new Class1();
-            var source = new Subject<BindingValue<int>>();
+            [Fact]
+            public void Binding_Validated_Property_Calls_UpdateDataValidation()
+            {
+                var target = new Class1();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetProperty();
+                var error1 = new Exception();
+                var error2 = new Exception();
 
-            target.Bind(Class1.NonValidatedDirectProperty, source);
-            source.OnNext(6);
-            source.OnNext(BindingValue<int>.BindingError(new Exception()));
-            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
-            source.OnNext(6);
+                target.Bind(property, source);
+                source.OnNext(6);
+                source.OnNext(BindingValue<int>.DataValidationError(error1));
+                source.OnNext(BindingValue<int>.BindingError(error2));
+                source.OnNext(7);
 
-            Assert.Empty(target.Notifications);
-        }
+                Assert.Equal(new Notification[]
+                {
+                    new(BindingValueType.Value, 6, null),
+                    new(BindingValueType.DataValidationError, 6, error1),
+                    new(BindingValueType.BindingError, 0, error2),
+                    new(BindingValueType.Value, 7, null),
+                }, target.Notifications);
+            }
 
-        [Fact]
-        public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation()
-        {
-            var target = new Class1();
-            var source = new Subject<BindingValue<int>>();
-
-            target.Bind(Class1.ValidatedDirectIntProperty, source);
-            source.OnNext(6);
-            source.OnNext(BindingValue<int>.BindingError(new Exception()));
-            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
-            source.OnNext(7);
-
-            var result = target.Notifications;
-            Assert.Equal(4, result.Count);
-            Assert.Equal(BindingValueType.Value, result[0].type);
-            Assert.Equal(6, result[0].value);
-            Assert.Equal(BindingValueType.BindingError, result[1].type);
-            Assert.Equal(BindingValueType.DataValidationError, result[2].type);
-            Assert.Equal(BindingValueType.Value, result[3].type);
-            Assert.Equal(7, result[3].value);
+            [Fact]
+            public void Binding_Validated_Property_Calls_UpdateDataValidation_Untyped()
+            {
+                var target = new Class1();
+                var source = new Subject<object>();
+                var property = GetProperty();
+                var error1 = new Exception();
+                var error2 = new Exception();
+
+                target.Bind(property, source);
+                source.OnNext(6);
+                source.OnNext(new BindingNotification(error1, BindingErrorType.DataValidationError));
+                source.OnNext(new BindingNotification(error2, BindingErrorType.Error));
+                source.OnNext(7);
+
+                Assert.Equal(new Notification[]
+                {
+                    new(BindingValueType.Value, 6, null),
+                    new(BindingValueType.DataValidationError, 6, error1),
+                    new(BindingValueType.BindingError, 0, error2),
+                    new(BindingValueType.Value, 7, null),
+                }, target.Notifications);
+            }
+
+            [Fact]
+            public void Binding_Overridden_Validated_Property_Calls_UpdateDataValidation()
+            {
+                var target = new Class2();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetNonValidatedProperty();
+
+                // Class2 overrides the non-validated property metadata to enable data validation.
+                target.Bind(property, source);
+                source.OnNext(1);
+
+                Assert.Equal(1, target.Notifications.Count);
+            }
+
+            protected abstract T GetProperty();
+            protected abstract T GetNonValidatedProperty();
         }
 
-        [Fact]
-        public void Binding_Overridden_Validated_Direct_Property_Calls_UpdateDataValidation()
+        public class DirectPropertyTests : TestBase<DirectPropertyBase<int>>
         {
-            var target = new Class2();
-            var source = new Subject<BindingValue<int>>();
+            [Fact]
+            public void Bound_Validated_String_Property_Can_Be_Set_To_Null()
+            {
+                var source = new ViewModel
+                {
+                    StringValue = "foo",
+                };
+
+                var target = new Class1
+                {
+                    [!Class1.ValidatedDirectStringProperty] = new Binding
+                    {
+                        Path = nameof(ViewModel.StringValue),
+                        Source = source,
+                    },
+                };
 
-            // Class2 overrides `NonValidatedDirectProperty`'s metadata to enable data validation.
-            target.Bind(Class1.NonValidatedDirectProperty, source);
-            source.OnNext(1);
+                Assert.Equal("foo", target.ValidatedDirectString);
 
-            Assert.Equal(1, target.Notifications.Count);
+                source.StringValue = null;
+
+                Assert.Null(target.ValidatedDirectString);
+            }
+
+            protected override DirectPropertyBase<int> GetProperty() => Class1.ValidatedDirectIntProperty;
+            protected override DirectPropertyBase<int> GetNonValidatedProperty() => Class1.NonValidatedDirectIntProperty;
         }
 
-        [Fact]
-        public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null()
+        public class StyledPropertyTests : TestBase<StyledProperty<int>>
         {
-            var source = new ViewModel
+            [Fact]
+            public void Bound_Validated_String_Property_Can_Be_Set_To_Null()
             {
-                StringValue = "foo",
-            };
+                var source = new ViewModel
+                {
+                    StringValue = "foo",
+                };
 
-            var target = new Class1
-            {
-                [!Class1.ValidatedDirectStringProperty] = new Binding
+                var target = new Class1
                 {
-                    Path = nameof(ViewModel.StringValue),
-                    Source = source,
-                },
-            };
+                    [!Class1.ValidatedDirectStringProperty] = new Binding
+                    {
+                        Path = nameof(ViewModel.StringValue),
+                        Source = source,
+                    },
+                };
+
+                Assert.Equal("foo", target.ValidatedDirectString);
 
-            Assert.Equal("foo", target.ValidatedDirectString);
+                source.StringValue = null;
 
-            source.StringValue = null;
+                Assert.Null(target.ValidatedDirectString);
+            }
 
-            Assert.Null(target.ValidatedDirectString);
+            protected override StyledProperty<int> GetProperty() => Class1.ValidatedStyledIntProperty;
+            protected override StyledProperty<int> GetNonValidatedProperty() => Class1.NonValidatedStyledIntProperty;
         }
 
+        private record class Notification(BindingValueType type, object? value, Exception? error);
+
         private class Class1 : AvaloniaObject
         {
-            public static readonly StyledProperty<int> NonValidatedProperty =
-                AvaloniaProperty.Register<Class1, int>(
-                    nameof(NonValidated));
-
-            public static readonly DirectProperty<Class1, int> NonValidatedDirectProperty =
+            public static readonly DirectProperty<Class1, int> NonValidatedDirectIntProperty =
                 AvaloniaProperty.RegisterDirect<Class1, int>(
-                    nameof(NonValidatedDirect),
-                    o => o.NonValidatedDirect,
-                    (o, v) => o.NonValidatedDirect = v);
+                    nameof(NonValidatedDirectInt),
+                    o => o.NonValidatedDirectInt,
+                    (o, v) => o.NonValidatedDirectInt = v);
 
             public static readonly DirectProperty<Class1, int> ValidatedDirectIntProperty =
                 AvaloniaProperty.RegisterDirect<Class1, int>(
@@ -118,27 +173,30 @@ namespace Avalonia.Base.UnitTests
                     (o, v) => o.ValidatedDirectInt = v,
                     enableDataValidation: true);
 
-            public static readonly DirectProperty<Class1, string> ValidatedDirectStringProperty =
-                AvaloniaProperty.RegisterDirect<Class1, string>(
+            public static readonly DirectProperty<Class1, string?> ValidatedDirectStringProperty =
+                AvaloniaProperty.RegisterDirect<Class1, string?>(
                     nameof(ValidatedDirectString),
                     o => o.ValidatedDirectString,
                     (o, v) => o.ValidatedDirectString = v,
                     enableDataValidation: true);
 
+            public static readonly StyledProperty<int> NonValidatedStyledIntProperty =
+                AvaloniaProperty.Register<Class1, int>(
+                    nameof(NonValidatedStyledInt));
+
+            public static readonly StyledProperty<int> ValidatedStyledIntProperty =
+                AvaloniaProperty.Register<Class1, int>(
+                    nameof(ValidatedStyledInt),
+                    enableDataValidation: true);
+
             private int _nonValidatedDirect;
             private int _directInt;
-            private string _directString;
-
-            public int NonValidated
-            {
-                get { return GetValue(NonValidatedProperty); }
-                set { SetValue(NonValidatedProperty, value); }
-            }
+            private string? _directString;
 
-            public int NonValidatedDirect
+            public int NonValidatedDirectInt
             {
                 get { return _directInt; }
-                set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); }
+                set { SetAndRaise(NonValidatedDirectIntProperty, ref _nonValidatedDirect, value); }
             }
 
             public int ValidatedDirectInt
@@ -147,20 +205,32 @@ namespace Avalonia.Base.UnitTests
                 set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); }
             }
 
-            public string ValidatedDirectString
+            public string? ValidatedDirectString
             {
                 get { return _directString; }
                 set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); }
             }
 
-            public List<(BindingValueType type, object value)> Notifications { get; } = new();
+            public int NonValidatedStyledInt
+            {
+                get { return GetValue(NonValidatedStyledIntProperty); }
+                set { SetValue(NonValidatedStyledIntProperty, value); }
+            }
+
+            public int ValidatedStyledInt
+            {
+                get => GetValue(ValidatedStyledIntProperty);
+                set => SetValue(ValidatedStyledIntProperty, value);
+            }
+
+            public List<Notification> Notifications { get; } = new();
 
             protected override void UpdateDataValidation(
                 AvaloniaProperty property,
                 BindingValueType state,
-                Exception error)
+                Exception? error)
             {
-                Notifications.Add((state, GetValue(property)));
+                Notifications.Add(new(state, GetValue(property), error));
             }
         }
 
@@ -168,16 +238,18 @@ namespace Avalonia.Base.UnitTests
         {
             static Class2()
             {
-                NonValidatedDirectProperty.OverrideMetadata<Class2>(
+                NonValidatedDirectIntProperty.OverrideMetadata<Class2>(
                     new DirectPropertyMetadata<int>(enableDataValidation: true));
+                NonValidatedStyledIntProperty.OverrideMetadata<Class2>(
+                    new StyledPropertyMetadata<int>(enableDataValidation: true));
             }
         }
 
         public class ViewModel : NotifyingBase
         {
-            private string _stringValue;
+            private string? _stringValue;
 
-            public string StringValue
+            public string? StringValue
             {
                 get { return _stringValue; }
                 set { _stringValue = value; RaisePropertyChanged(); }

+ 255 - 38
tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs

@@ -1,72 +1,289 @@
 using System;
-using System.Reactive.Linq;
+using System.Collections;
+using System.ComponentModel;
 using Avalonia.Controls;
 using Avalonia.Data;
-using Avalonia.Data.Core;
-using Avalonia.Markup.Data;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
 using Xunit;
 
+#nullable enable
+
 namespace Avalonia.Markup.UnitTests.Data
 {
     public class BindingTests_DataValidation
     {
-        [Fact]
-        public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_LocalValue()
+        public abstract class TestBase<T>
+            where T : AvaloniaProperty<int>
         {
-            var textBlock = new TextBlock
+            [Fact]
+            public void Setter_Exception_Causes_DataValidation_Error()
+            {
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(ExceptionValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                target.DataContext = new ExceptionValidatingModel();
+                target.Bind(property, binding);
+
+                Assert.Equal(20, target.GetValue(property));
+
+                target.SetValue(property, 200);
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<ArgumentOutOfRangeException>(target.DataValidationError);
+
+                target.SetValue(property, 10);
+
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
+
+            [Fact]
+            public void Indei_Error_Causes_DataValidation_Error()
             {
-                DataContext = new Class1(),
-            };
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(IndeiValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                target.DataContext = new IndeiValidatingModel();
+                target.Bind(property, binding);
+
+                Assert.Equal(20, target.GetValue(property));
+
+                target.SetValue(property, 200);
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value.", target.DataValidationError?.Message);
+
+                target.SetValue(property, 10);
 
-            var target = new Binding(nameof(Class1.Foo));
-            var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false);
-            var subject = (BindingExpression)instanced.Source;
-            object result = null;
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
 
-            subject.Subscribe(x => result = x);
+            private protected abstract (DataValidationTestControl, T) CreateTarget();
+        }
 
-            Assert.IsType<string>(result);
+        public class DirectPropertyTests : TestBase<DirectPropertyBase<int>>
+        {
+            private protected override (DataValidationTestControl, DirectPropertyBase<int>) CreateTarget()
+            {
+                return (new ValidatedDirectPropertyClass(), ValidatedDirectPropertyClass.ValueProperty);
+            }
         }
 
-        [Fact]
-        public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue()
+        public class StyledPropertyTests : TestBase<StyledProperty<int>>
         {
-            var textBlock = new TextBlock
+            [Fact]
+            public void Style_Binding_Supports_Indei_Data_Validation()
+            {
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(IndeiValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                var root = new TestRoot
+                {
+                    DataContext = new IndeiValidatingModel(),
+                    Styles =
+                    {
+                        new Style(x => x.Is<DataValidationTestControl>())
+                        {
+                            Setters =
+                            {
+                                new Setter(property, binding)
+                            }
+                        }
+                    },
+                    Child = target,
+                };
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                Assert.Equal(20, target.GetValue(property));
+
+                target.SetValue(property, 200);
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value.", target.DataValidationError?.Message);
+
+                target.SetValue(property, 10);
+
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
+
+            [Fact]
+            public void Style_With_Activator_Binding_Supports_Indei_Data_Validation()
+            {
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(IndeiValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                var model = new IndeiValidatingModel
+                {
+                    Value = 200,
+                };
+
+                var root = new TestRoot
+                {
+                    DataContext = model,
+                    Styles =
+                    {
+                        new Style(x => x.Is<DataValidationTestControl>().Class("foo"))
+                        {
+                            Setters =
+                            {
+                                new Setter(property, binding)
+                            }
+                        }
+                    },
+                    Child = target,
+                };
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+                target.Classes.Add("foo");
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value.", target.DataValidationError?.Message);
+
+                target.Classes.Remove("foo");
+                Assert.Equal(0, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+
+                target.Classes.Add("foo");
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value.", target.DataValidationError?.Message);
+
+                target.SetValue(property, 10);
+
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
+
+            private protected override (DataValidationTestControl, StyledProperty<int>) CreateTarget()
             {
-                DataContext = new Class1(),
-            };
+                return (new ValidatedStyledPropertyClass(), ValidatedStyledPropertyClass.ValueProperty);
+            }
+        }
 
-            var target = new Binding(nameof(Class1.Foo));
-            var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
-            var subject = (BindingExpression)instanced.Source;
-            object result = null;
+        internal class DataValidationTestControl : Control
+        {
+            public Exception? DataValidationError { get; protected set; }
+        }
 
-            subject.Subscribe(x => result = x);
+        private class ValidatedStyledPropertyClass : DataValidationTestControl
+        {
+            public static readonly StyledProperty<int> ValueProperty =
+                AvaloniaProperty.Register<ValidatedStyledPropertyClass, int>(
+                    "Value",
+                    enableDataValidation: true);
+
+            public int Value
+            {
+                get => GetValue(ValueProperty);
+                set => SetValue(ValueProperty, value);
+            }
 
-            Assert.Equal(new BindingNotification("foo"), result);
+            protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
+            {
+                if (property == ValueProperty)
+                {
+                    DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null;
+                }
+            }
         }
 
-        [Fact]
-        public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_TemplatedParent()
+        private class ValidatedDirectPropertyClass : DataValidationTestControl
         {
-            var textBlock = new TextBlock
+            public static readonly DirectProperty<ValidatedDirectPropertyClass, int> ValueProperty =
+                AvaloniaProperty.RegisterDirect<ValidatedDirectPropertyClass, int>(
+                    "Value",
+                    o => o.Value,
+                    (o, v) => o.Value = v,
+                    enableDataValidation: true);
+
+            private int _value;
+
+            public int Value
             {
-                DataContext = new Class1(),
-            };
+                get => _value;
+                set => SetAndRaise(ValueProperty, ref _value, value);
+            }
 
-            var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template };
-            var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
-            var subject = (BindingExpression)instanced.Source;
-            object result = null;
+            protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
+            {
+                if (property == ValueProperty)
+                {
+                    DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null;
+                }
+            }
+        }
 
-            subject.Subscribe(x => result = x);
+        private class ExceptionValidatingModel
+        {
+            public const int MaxValue = 100;
+            private int _value = 20;
 
-            Assert.IsType<string>(result);
+            public int Value
+            {
+                get => _value;
+                set
+                {
+                    if (value > MaxValue)
+                        throw new ArgumentOutOfRangeException(nameof(value));
+                    _value = value;
+                }
+            }
         }
 
-        private class Class1
+        private class IndeiValidatingModel : INotifyDataErrorInfo
         {
-            public string Foo { get; set; } = "foo";
+            public const int MaxValue = 100;
+            private bool _hasErrors;
+            private int _value = 20;
+
+            public int Value
+            {
+                get => _value;
+                set
+                {
+                    _value = value;
+                    HasErrors = value > MaxValue;
+                }
+            }
+
+            public bool HasErrors 
+            {
+                get => _hasErrors;
+                private set
+                {
+                    if (_hasErrors != value)
+                    {
+                        _hasErrors = value;
+                        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+                    }
+                }
+            }
+
+            public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
+
+            public IEnumerable GetErrors(string? propertyName)
+            {
+                if (propertyName == nameof(Value) && _value > MaxValue)
+                    yield return "Invalid value.";
+            }
         }
     }
 }