A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

450 lines
14 KiB

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Avalonia.Data;
using Avalonia.UnitTests;
using Xunit;
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)
{
if (propertyName is not null && _errors.TryGetValue(propertyName, out var result))
return result;
return Array.Empty<string>();
}
}
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) => Array.Empty<string>();
}
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 Array.Empty<string>();
}
}
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);
}
}