| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.ComponentModel.DataAnnotations;
- using Avalonia.Data;
- using Avalonia.UnitTests;
- using Xunit;
- #nullable enable
- namespace Avalonia.Base.UnitTests.Data.Core;
- public partial class BindingExpressionTests
- {
- [Fact]
- public void Root_Null_Should_Update_Data_Validation()
- {
- var target = CreateTargetWithSource<ViewModel?, string?>(
- null,
- o => o!.StringValue,
- enableDataValidation: true);
- AssertBindingError(
- target,
- TargetClass.StringProperty,
- new BindingChainException("Binding Source is null.", "StringValue", "(source)"),
- BindingErrorType.Error);
- }
- [Fact]
- public void Null_Value_In_Path_Should_Update_Data_Validation()
- {
- var data = new { Foo = default(ViewModel) };
- var target = CreateTargetWithSource(
- data,
- o => o.Foo!.StringValue!.Length,
- enableDataValidation: true);
- AssertBindingError(
- target,
- TargetClass.IntProperty,
- new BindingChainException("Value is null.", "Foo.StringValue.Length", "Foo"),
- BindingErrorType.Error);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Invalid_Double_String_Should_Update_Data_Validation()
- {
- var data = new ViewModel { StringValue = "foo" };
- var target = CreateTargetWithSource(
- data,
- o => o.StringValue,
- enableDataValidation: true,
- targetProperty: TargetClass.DoubleProperty);
- AssertBindingError(
- target,
- TargetClass.DoubleProperty,
- new InvalidCastException("Could not convert 'foo' (System.String) to 'System.Double'."),
- BindingErrorType.Error);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Invalid_Double_String_Should_Revert_To_FallbackValue()
- {
- var data = new ViewModel { StringValue = "foo" };
- var target = CreateTargetWithSource(
- data,
- o => o.StringValue,
- enableDataValidation: true,
- fallbackValue: 42.0,
- targetProperty: TargetClass.DoubleProperty);
- Assert.Equal(42.0, target.Double);
- AssertBindingError(
- target,
- TargetClass.DoubleProperty,
- new InvalidCastException("Could not convert 'foo' (System.String) to 'System.Double'."),
- BindingErrorType.Error);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Setter_Exception_Does_Not_Cause_DataValidationError_When_Data_Validation_Not_Enabled()
- {
- var data = new ExceptionViewModel { MustBePositive = 5 };
- var target = CreateTargetWithSource(
- data,
- o => o.MustBePositive,
- enableDataValidation: false,
- mode: BindingMode.TwoWay);
- target.Int = -5;
- // TODO: Should this be 5?
- Assert.Equal(-5, target.Int);
- Assert.Equal(5, data.MustBePositive);
- AssertNoError(target, TargetClass.IntProperty);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Setter_Exception_Updates_Data_Validation()
- {
- var data = new ExceptionViewModel { MustBePositive = 5 };
- var target = CreateTargetWithSource(
- data,
- o => o.MustBePositive,
- enableDataValidation: true,
- mode: BindingMode.TwoWay);
- target.Int = -5;
- // TODO: Should this be 5?
- Assert.Equal(-5, target.Int);
- Assert.Equal(5, data.MustBePositive);
- AssertBindingError(
- target,
- TargetClass.IntProperty,
- new ArgumentOutOfRangeException("value"),
- BindingErrorType.DataValidationError);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Indei_Validation_Does_Not_Subscribe_When_DataValidation_Not_Enabled()
- {
- var data = new IndeiViewModel { MustBePositive = 5 };
- var target = CreateTargetWithSource(
- data,
- o => o.MustBePositive,
- enableDataValidation: false,
- mode: BindingMode.TwoWay);
- Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
- }
- [Fact]
- public void Indei_Validation_Subscribes_And_Unsubscribes()
- {
- var data = new IndeiViewModel { MustBePositive = 5 };
- var (target, expression) = CreateTargetAndExpression<IndeiViewModel, int>(
- o => o.MustBePositive,
- enableDataValidation: true,
- mode: BindingMode.TwoWay,
- source: data);
- Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
- expression.Dispose();
- Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
- }
- [Fact]
- public void Conversion_Errors_Update_Data_Validation_When_Writing_To_Source()
- {
- var data = new ViewModel { DoubleValue = 5.6 };
- var target = CreateTargetWithSource(
- data,
- o => o.DoubleValue,
- enableDataValidation: true,
- mode: BindingMode.TwoWay,
- targetProperty: TargetClass.TagProperty);
- // Can write a double value.
- target.Tag = 1.2;
- Assert.Equal(1.2, data.DoubleValue);
- AssertNoError(target, TargetClass.StringProperty);
- // Can write a string value and it gets converted to double.
- target.Tag = "3.4";
- Assert.Equal(3.4, data.DoubleValue);
- AssertNoError(target, TargetClass.StringProperty);
- // An invalid string value should result in an error. Not sure why this is considered
- // a data validation error rather than a binding error, but preserving semantics.
- target.Tag = "bar";
- Assert.Equal(3.4, data.DoubleValue);
- AssertBindingError(
- target,
- TargetClass.TagProperty,
- new InvalidCastException("Could not convert 'bar' (System.String) to System.Double."),
- BindingErrorType.DataValidationError);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Indei_Validation_Updates_Data_Validation_When_Writing_To_Source()
- {
- var data = new IndeiViewModel();
- var target = CreateTargetWithSource(
- data,
- o => o.MustBePositive,
- enableDataValidation: true,
- mode: BindingMode.TwoWay);
- Assert.Equal(0, target.Int);
- Assert.Equal(0, data.MustBePositive);
- AssertNoError(target, TargetClass.IntProperty);
- target.Int = 5;
- Assert.Equal(5, target.Int);
- Assert.Equal(5, data.MustBePositive);
- AssertNoError(target, TargetClass.IntProperty);
- target.Int = -5;
- Assert.Equal(-5, target.Int);
- Assert.Equal(-5, data.MustBePositive);
- AssertBindingError(target, TargetClass.IntProperty, new DataValidationException("Must be positive"), BindingErrorType.DataValidationError);
- target.Int = 5;
- Assert.Equal(5, target.Int);
- Assert.Equal(5, data.MustBePositive);
- AssertNoError(target, TargetClass.IntProperty);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Does_Not_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain()
- {
- var data = new IndeiContainerViewModel { Inner = new() };
- var target = CreateTargetWithSource(
- data,
- o => o.Inner!.MustBePositive,
- enableDataValidation: true,
- mode: BindingMode.TwoWay);
- // We may want to change this but I've never seen an example of data validation on an
- // intermediate object in a chain so for the moment I'm not sure what the result of
- // validating such a thing should look like.
- Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
- Assert.Equal(1, data.Inner.ErrorsChangedSubscriptionCount);
- }
- [Fact]
- public void Updates_Data_Validation_For_Null_Value_In_Property_Chain()
- {
- var data = new IndeiContainerViewModel();
- var target = CreateTargetWithSource(
- data,
- o => o.Inner!.MustBePositive,
- enableDataValidation: true,
- mode: BindingMode.TwoWay);
- AssertBindingError(
- target,
- TargetClass.IntProperty,
- new BindingChainException("Value is null.", "Inner.MustBePositive", "Inner"),
- BindingErrorType.Error);
- GC.KeepAlive(data);
- }
- [Fact]
- public void Updates_Data_Validation_For_Required_DataAnnotation()
- {
- var data = new DataAnnotationsViewModel();
- var target = CreateTargetWithSource(
- data,
- o => o.RequiredString,
- enableDataValidation: true);
- AssertBindingError(
- target,
- TargetClass.StringProperty,
- new DataValidationException("String is required!"),
- BindingErrorType.DataValidationError);
- }
- [Fact]
- public void Handles_Indei_And_DataAnnotations_On_Same_Class()
- {
- // Issue #15201
- var data = new IndeiDataAnnotationsViewModel();
- var target = CreateTargetWithSource(
- data,
- o => o.RequiredString,
- enableDataValidation: true);
- AssertBindingError(
- target,
- TargetClass.StringProperty,
- new DataValidationException("String is required!"),
- BindingErrorType.DataValidationError);
- }
- [Fact]
- public void Setting_Valid_Value_Should_Clear_Binding_Error()
- {
- var data = new ViewModel { DoubleValue = 5.6 };
- var target = CreateTargetWithSource(
- data,
- o => o.DoubleValue,
- enableDataValidation: true,
- mode: BindingMode.TwoWay,
- targetProperty: TargetClass.StringProperty);
- target.String = "5.6";
- target.String = "5.6a";
- target.String = "5.6";
- AssertNoError(target, TargetClass.StringProperty);
- GC.KeepAlive(data);
- }
- public class ExceptionViewModel : NotifyingBase
- {
- private int _mustBePositive;
- public int MustBePositive
- {
- get { return _mustBePositive; }
- set
- {
- if (value <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
- _mustBePositive = value;
- RaisePropertyChanged();
- }
- }
- }
- private class IndeiViewModel : IndeiBase
- {
- private int _mustBePositive;
- private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
- public int MustBePositive
- {
- get { return _mustBePositive; }
- set
- {
- _mustBePositive = value;
- RaisePropertyChanged();
- if (value >= 0)
- {
- _errors.Remove(nameof(MustBePositive));
- RaiseErrorsChanged(nameof(MustBePositive));
- }
- else
- {
- _errors[nameof(MustBePositive)] = new[] { "Must be positive" };
- RaiseErrorsChanged(nameof(MustBePositive));
- }
- }
- }
- public override bool HasErrors => _mustBePositive >= 0;
- public override IEnumerable? GetErrors(string propertyName)
- {
- IList<string>? result;
- _errors.TryGetValue(propertyName, out result);
- return result;
- }
- }
- private class IndeiContainerViewModel : IndeiBase
- {
- private IndeiViewModel? _inner;
- public IndeiViewModel? Inner
- {
- get { return _inner; }
- set { _inner = value; RaisePropertyChanged(); }
- }
- public override bool HasErrors => false;
- public override IEnumerable? GetErrors(string propertyName) => null;
- }
- private class DataAnnotationsViewModel : NotifyingBase
- {
- private string? _requiredString;
- [Required(ErrorMessage = "String is required!")]
- public string? RequiredString
- {
- get { return _requiredString; }
- set { _requiredString = value; RaisePropertyChanged(); }
- }
- }
- private class IndeiDataAnnotationsViewModel : IndeiBase
- {
- private string? _requiredString;
- [Required(ErrorMessage = "String is required!")]
- public string? RequiredString
- {
- get { return _requiredString; }
- set { _requiredString = value; RaisePropertyChanged(); }
- }
- public override bool HasErrors => RequiredString is null;
-
- public override IEnumerable? GetErrors(string propertyName)
- {
- if (propertyName == nameof(RequiredString) && RequiredString is null)
- {
- return new[] { "String is required!" };
- }
- return null;
- }
- }
- private static void AssertNoError(TargetClass target, AvaloniaProperty property)
- {
- Assert.False(target.BindingNotifications.TryGetValue(property, out var notification));
- }
- private static void AssertBindingError(
- TargetClass target,
- AvaloniaProperty property,
- Exception expectedException,
- BindingErrorType errorType)
- {
- Assert.True(target.BindingNotifications.TryGetValue(property, out var notification));
- Assert.Equal(errorType, notification.ErrorType);
- Assert.NotNull(notification.Error);
- Assert.IsType(expectedException.GetType(), notification.Error);
- Assert.Equal(expectedException.Message, notification.Error.Message);
- }
- }
|