diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index d48e58136a..71eb521f9d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/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 + where T : AvaloniaProperty { - var target = new Class1(); - var source = new Subject>(); + [Fact] + public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetNonValidatedProperty(); - target.Bind(Class1.NonValidatedProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(6); + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.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>(); + [Fact] + public void Binding_Validated_Property_Calls_UpdateDataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetProperty(); + var error1 = new Exception(); + var error2 = new Exception(); - target.Bind(Class1.NonValidatedDirectProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(6); + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.DataValidationError(error1)); + source.OnNext(BindingValue.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>(); - - target.Bind(Class1.ValidatedDirectIntProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.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(); + 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>(); + 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> { - var target = new Class2(); - var source = new Subject>(); + [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 GetProperty() => Class1.ValidatedDirectIntProperty; + protected override DirectPropertyBase GetNonValidatedProperty() => Class1.NonValidatedDirectIntProperty; } - [Fact] - public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null() + public class StyledPropertyTests : TestBase> { - 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 GetProperty() => Class1.ValidatedStyledIntProperty; + protected override StyledProperty GetNonValidatedProperty() => Class1.NonValidatedStyledIntProperty; } + private record class Notification(BindingValueType type, object? value, Exception? error); + private class Class1 : AvaloniaObject { - public static readonly StyledProperty NonValidatedProperty = - AvaloniaProperty.Register( - nameof(NonValidated)); - - public static readonly DirectProperty NonValidatedDirectProperty = + public static readonly DirectProperty NonValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( - nameof(NonValidatedDirect), - o => o.NonValidatedDirect, - (o, v) => o.NonValidatedDirect = v); + nameof(NonValidatedDirectInt), + o => o.NonValidatedDirectInt, + (o, v) => o.NonValidatedDirectInt = v); public static readonly DirectProperty ValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( @@ -118,27 +173,30 @@ namespace Avalonia.Base.UnitTests (o, v) => o.ValidatedDirectInt = v, enableDataValidation: true); - public static readonly DirectProperty ValidatedDirectStringProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty ValidatedDirectStringProperty = + AvaloniaProperty.RegisterDirect( nameof(ValidatedDirectString), o => o.ValidatedDirectString, (o, v) => o.ValidatedDirectString = v, enableDataValidation: true); + public static readonly StyledProperty NonValidatedStyledIntProperty = + AvaloniaProperty.Register( + nameof(NonValidatedStyledInt)); + + public static readonly StyledProperty ValidatedStyledIntProperty = + AvaloniaProperty.Register( + 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 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( + NonValidatedDirectIntProperty.OverrideMetadata( new DirectPropertyMetadata(enableDataValidation: true)); + NonValidatedStyledIntProperty.OverrideMetadata( + new StyledPropertyMetadata(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(); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs index 505eddb146..de2d01a2ac 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs +++ b/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 + where T : AvaloniaProperty { - 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(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(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(result); + public class DirectPropertyTests : TestBase> + { + private protected override (DataValidationTestControl, DirectPropertyBase) CreateTarget() + { + return (new ValidatedDirectPropertyClass(), ValidatedDirectPropertyClass.ValueProperty); + } } - [Fact] - public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue() + public class StyledPropertyTests : TestBase> { - 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()) + { + 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(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().Class("foo")) + { + Setters = + { + new Setter(property, binding) + } + } + }, + Child = target, + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + target.Classes.Add("foo"); + + Assert.Equal(200, target.GetValue(property)); + Assert.IsType(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(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) 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 ValueProperty = + AvaloniaProperty.Register( + "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 ValueProperty = + AvaloniaProperty.RegisterDirect( + "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(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? ErrorsChanged; + + public IEnumerable GetErrors(string? propertyName) + { + if (propertyName == nameof(Value) && _value > MaxValue) + yield return "Invalid value."; + } } } }