// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests.Data.Core { public class ExpressionObserverTests_DataValidation : IClassFixture { [Fact] public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() { var data = new ExceptionTest { MustBePositive = 5 }; var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); var validationMessageFound = false; observer.OfType() .Where(x => x.ErrorType == BindingErrorType.DataValidationError) .Subscribe(_ => validationMessageFound = true); observer.SetValue(-5); Assert.False(validationMessageFound); GC.KeepAlive(data); } [Fact] public void Exception_Validation_Sends_DataValidationError() { var data = new ExceptionTest { MustBePositive = 5 }; var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); var validationMessageFound = false; observer.OfType() .Where(x => x.ErrorType == BindingErrorType.DataValidationError) .Subscribe(_ => validationMessageFound = true); observer.SetValue(-5); Assert.True(validationMessageFound); GC.KeepAlive(data); } [Fact] public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); observer.Subscribe(_ => { }); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } [Fact] public void Enabled_Indei_Validation_Subscribes() { var data = new IndeiTest { MustBePositive = 5 }; var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); var sub = observer.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); sub.Dispose(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } [Fact] public void Validation_Plugins_Send_Correct_Notifications() { var data = new IndeiTest(); var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); var result = new List(); var errmsg = string.Empty; try { typeof(IndeiTest).GetProperty(nameof(IndeiTest.MustBePositive)).SetValue(data, "foo"); } catch(Exception e) { errmsg = e.Message; } observer.Subscribe(x => result.Add(x)); observer.SetValue(5); observer.SetValue(-5); observer.SetValue("foo"); observer.SetValue(5); Assert.Equal(new[] { new BindingNotification(0), // Value is notified twice as ErrorsChanged is always called by IndeiTest. new BindingNotification(5), new BindingNotification(5), // Value is first signalled without an error as validation hasn't been updated. new BindingNotification(-5), new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5), // Exception is thrown by trying to set value to "foo". new BindingNotification( new ArgumentException(errmsg), BindingErrorType.DataValidationError), // Value is set then validation is updated. new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5), new BindingNotification(5), }, result); GC.KeepAlive(data); } [Fact] public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain() { var data = new Container { Inner = new IndeiTest() }; var observer = new ExpressionObserver( data, $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", true); observer.Subscribe(_ => { }); // 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, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount); } [Fact] public void Sends_Correct_Notifications_With_Property_Chain() { var container = new Container(); var inner = new IndeiTest(); var observer = new ExpressionObserver( container, $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", true); var result = new List(); observer.Subscribe(x => result.Add(x)); Assert.Equal(new[] { new BindingNotification( new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); GC.KeepAlive(container); GC.KeepAlive(inner); } public class ExceptionTest : NotifyingBase { private int _mustBePositive; public int MustBePositive { get { return _mustBePositive; } set { if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value)); } _mustBePositive = value; RaisePropertyChanged(); } } } private class IndeiTest : IndeiBase { private int _mustBePositive; private Dictionary> _errors = new Dictionary>(); 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 result; _errors.TryGetValue(propertyName, out result); return result; } } private class Container : IndeiBase { private object _inner; public object Inner { get { return _inner; } set { _inner = value; RaisePropertyChanged(); } } public override bool HasErrors => false; public override IEnumerable GetErrors(string propertyName) => null; } } }