using System; using System.Collections; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests.Data.Core { [InvariantCulture] public class ExpressionObserverTests_DataValidation { ////[Fact] ////public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() ////{ //// var data = new ExceptionTest { MustBePositive = 5 }; //// var observer = ExpressionObserver.Create(data, o => o.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 = ExpressionObserver.Create(data, o => o.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_DataValidation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); observer.Subscribe(_ => { }); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } [Fact] public void Enabled_Indei_Validation_Subscribes() { var data = new IndeiTest { MustBePositive = 5 }; var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var sub = observer.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); sub.Dispose(); // Forces WeakEvent compact Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } [Fact] public void Validation_Plugins_Send_Correct_Notifications() { var data = new IndeiTest(); var observer = ExpressionObserver.Create(data, o => o.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 DataValidationException("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 DataValidationException("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 = ExpressionObserver.Create(data, o => o.Inner.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, data.Inner.ErrorsChangedSubscriptionCount); } [Fact] public void Sends_Correct_Notifications_With_Property_Chain() { var container = new Container(); var observer = ExpressionObserver.Create(container, o => o.Inner.MustBePositive, true); var result = new List(); observer.Subscribe(x => result.Add(x)); Assert.Equal(new[] { new BindingNotification( new MarkupBindingChainException("Null value", "o => o.Inner.MustBePositive", "Inner"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); GC.KeepAlive(container); } 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 IndeiTest _inner; public IndeiTest Inner { get { return _inner; } set { _inner = value; RaisePropertyChanged(); } } public override bool HasErrors => false; public override IEnumerable GetErrors(string propertyName) => null; } } }