Browse Source

Added tests for styled property data validation.

pull/10423/head
Steven Kirk 4 years ago
parent
commit
eabc9493fa
  1. 258
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  2. 293
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs

258
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

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

293
tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs

@ -1,72 +1,289 @@
using System; using System;
using System.Reactive.Linq; using System.Collections;
using System.ComponentModel;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Styling;
using Avalonia.Markup.Data; using Avalonia.UnitTests;
using Xunit; using Xunit;
#nullable enable
namespace Avalonia.Markup.UnitTests.Data namespace Avalonia.Markup.UnitTests.Data
{ {
public class BindingTests_DataValidation public class BindingTests_DataValidation
{ {
[Fact] public abstract class TestBase<T>
public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_LocalValue() 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)); Assert.Equal(10, target.GetValue(property));
var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); Assert.Null(target.DataValidationError);
var subject = (BindingExpression)instanced.Source; }
object result = null;
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 class StyledPropertyTests : TestBase<StyledProperty<int>>
public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue()
{ {
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)); internal class DataValidationTestControl : Control
var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); {
var subject = (BindingExpression)instanced.Source; public Exception? DataValidationError { get; protected set; }
object result = null; }
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] private class ValidatedDirectPropertyClass : DataValidationTestControl
public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_TemplatedParent()
{ {
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 }; protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); {
var subject = (BindingExpression)instanced.Source; if (property == ValueProperty)
object result = null; {
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.";
}
} }
} }
} }

Loading…
Cancel
Save