diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs index 273cbd8241..a14802a433 100644 --- a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq.Expressions; +using System.Reflection; using System.Text; using Avalonia.Data; @@ -48,12 +49,21 @@ namespace Avalonia.Data.Core protected override object GetValue(object target) { - return getDelegate.DynamicInvoke(target); + try + { + return getDelegate.DynamicInvoke(target); + } + catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException + || e.InnerException is IndexOutOfRangeException + || e.InnerException is KeyNotFoundException) + { + return AvaloniaProperty.UnsetValue; + } } protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) { - return expression.Indexer.Name == e.PropertyName; + return expression.Indexer == null || expression.Indexer.Name == e.PropertyName; } protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index f1f082174a..dba9078423 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -10,6 +10,7 @@ namespace Avalonia.Data.Core.Parsers { class ExpressionVisitorNodeBuilder : ExpressionVisitor { + private const string MultiDimensionalArrayGetterMethodName = "Get"; private static PropertyInfo AvaloniaObjectIndexer; private static MethodInfo CreateDelegateMethod; @@ -98,7 +99,6 @@ namespace Avalonia.Data.Core.Parsers { if (node.NodeType == ExpressionType.ArrayIndex) { - base.VisitBinary(node); return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); @@ -161,21 +161,13 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitMethodCall(MethodCallExpression node) { - var property = TryGetPropertyFromMethod(node.Method); - - if (property != null) - { - return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); - } - if (node.Method == CreateDelegateMethod) { var visited = Visit(node.Arguments[1]); Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue(node.Object).Name, enableDataValidation)); - return visited; + return node; } - - if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`')) + else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`')) { if (node.Method.IsStatic) { @@ -189,7 +181,18 @@ namespace Avalonia.Data.Core.Parsers return node; } - throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + else if (node.Object.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName) + { + return Visit(Expression.MakeIndex(node.Object, null, node.Arguments)); + } + + throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType.AssemblyQualifiedName}.{node.Method.Name}'."); } private PropertyInfo TryGetPropertyFromMethod(MethodInfo method) diff --git a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs index 907a4ac737..fa8b56765c 100644 --- a/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs +++ b/src/Avalonia.Base/Data/Core/StreamBindingExtensions.cs @@ -14,6 +14,11 @@ namespace Avalonia throw new InvalidOperationException("This should be used only in a binding expression"); } + public static object StreamBinding(this Task @this) + { + throw new InvalidOperationException("This should be used only in a binding expression"); + } + public static T StreamBinding(this IObservable @this) { throw new InvalidOperationException("This should be used only in a binding expression"); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 7141a62cd9..ddbe252fc0 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -6,7 +6,7 @@ using System.Text; namespace Avalonia.Markup.Parsers { - public class ExpressionObserverBuilder + public static class ExpressionObserverBuilder { internal static ExpressionNode Parse(string expression, bool enableValidation = false, Func typeResolver = null) { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 4e595684df..f8e050df85 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); var result = await target.Take(1); Assert.Equal("foo", result); @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); target.OnNext("bar"); @@ -48,7 +48,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Indexed_Value() { var data = new { Foo = new[] { "foo" } }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "Foo[0]"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.Foo[0]), typeof(string)); target.OnNext("bar"); @@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Convert_Get_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); var result = await target.Take(1); Assert.Equal(5.6, result); @@ -73,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); var result = await target.Take(1); Assert.IsType(result); @@ -85,7 +85,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() { var data = new Class1 { StringValue = null }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -97,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Convert_Set_String_To_Double() { var data = new Class1 { StringValue = $"{5.6}" }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "StringValue"), typeof(double)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); target.OnNext(6.7); @@ -110,7 +110,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Convert_Get_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); var result = await target.Take(1); Assert.Equal($"{5.6}", result); @@ -122,7 +122,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Convert_Set_Double_To_String() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext($"{6.7}"); @@ -136,7 +136,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue"), + ExpressionObserver.Create(data, o => o.StringValue), typeof(int), 42, DefaultValueConverter.Instance); @@ -157,7 +157,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue", true), + ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), 42, DefaultValueConverter.Instance); @@ -178,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue"), + ExpressionObserver.Create(data, o => o.StringValue), typeof(int), "bar", DefaultValueConverter.Instance); @@ -200,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "StringValue", true), + ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), "bar", DefaultValueConverter.Instance); @@ -221,7 +221,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Setting_Invalid_Double_String_Should_Not_Change_Target() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext("foo"); @@ -235,7 +235,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), "9.8", DefaultValueConverter.Instance); @@ -251,7 +251,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Coerce_Setting_Null_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext(null); @@ -264,7 +264,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() { var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue"), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); target.OnNext(AvaloniaProperty.UnsetValue); @@ -280,7 +280,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var converter = new Mock(); var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, converterParameter: "foo"); @@ -298,7 +298,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); var target = new BindingExpression( - ExpressionObserverBuilder.Build(data, "DoubleValue"), + ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), converter.Object, converterParameter: "foo"); @@ -315,7 +315,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); - var target = new BindingExpression(ExpressionObserverBuilder.Build(data, "DoubleValue", true), typeof(string)); + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue, true), typeof(string)); var result = new List(); target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs index 112a7fc4d0..7e47e9b1eb 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AttachedProperty.cs @@ -8,25 +8,17 @@ using System.Threading.Tasks; using Avalonia.Diagnostics; using Avalonia.Data.Core; using Xunit; -using Avalonia.Markup.Parsers; namespace Avalonia.Base.UnitTests.Data.Core { public class ExpressionObserverTests_AttachedProperty { - private readonly Func _typeResolver; - - public ExpressionObserverTests_AttachedProperty() - { - var foo = Owner.FooProperty; - _typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null; - } [Fact] public async Task Should_Get_Attached_Property_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,19 +26,6 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); } - [Fact] - public async Task Should_Get_Attached_Property_Value_With_Namespace() - { - var data = new Class1(); - var target = ExpressionObserverBuilder.Build( - data, - "(NS:Owner.Foo)", - typeResolver: (ns, name) => ns == "NS" && name == "Owner" ? typeof(Owner) : null); - var result = await target.Take(1); - Assert.Equal("foo", result); - Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); - } - [Fact] public async Task Should_Get_Chained_Attached_Property_Value() { @@ -58,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -70,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Simple_Attached_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o[Owner.FooProperty]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -94,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(data, o => o.Next[Owner.FooProperty]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -113,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Func> run = () => { var source = new Class1(); - var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver); + var target = ExpressionObserver.Create(source, o => o.Next[Owner.FooProperty]); return Tuple.Create(target, new WeakReference(source)); }; @@ -125,22 +104,6 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Null(result.Item2.Target); } - [Fact] - public void Should_Fail_With_Attached_Property_With_Only_1_Part() - { - var data = new Class1(); - - Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver)); - } - - [Fact] - public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts() - { - var data = new Class1(); - - Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver)); - } - private static class Owner { public static readonly AttachedProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs index b34c1ff8be..8d5510dd20 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_AvaloniaProperty.cs @@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_ClrProperty_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "ClrProperty"); + var target = ExpressionObserver.Create(data, o => o.ClrProperty); var result = await target.Take(1); Assert.Equal("clr-property", result); @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Simple_Property_Value() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -64,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Func> run = () => { var source = new Class1(); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); return Tuple.Create(target, new WeakReference(source)); }; @@ -81,6 +81,8 @@ namespace Avalonia.Base.UnitTests.Data.Core public static readonly StyledProperty FooProperty = AvaloniaProperty.Register("Foo", defaultValue: "foo"); + public string Foo { get => GetValue(FooProperty); set => SetValue(FooProperty, value); } + public string ClrProperty { get; } = "clr-property"; } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index c5f7d1d7bf..b66dd610dd 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -20,7 +20,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() { var data = new ExceptionTest { MustBePositive = 5 }; - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), false); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); var validationMessageFound = false; observer.OfType() @@ -37,7 +37,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Exception_Validation_Sends_DataValidationError() { var data = new ExceptionTest { MustBePositive = 5 }; - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var validationMessageFound = false; observer.OfType() @@ -54,7 +54,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), false); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); observer.Subscribe(_ => { }); @@ -65,7 +65,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Enabled_Indei_Validation_Subscribes() { var data = new IndeiTest { MustBePositive = 5 }; - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var sub = observer.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); @@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Validation_Plugins_Send_Correct_Notifications() { var data = new IndeiTest(); - var observer = ExpressionObserverBuilder.Build(data, nameof(data.MustBePositive), true); + var observer = ExpressionObserver.Create(data, o => o.MustBePositive, true); var result = new List(); var errmsg = string.Empty; @@ -123,10 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Inner = new IndeiTest() }; - var observer = ExpressionObserverBuilder.Build( - data, - $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", - true); + var observer = ExpressionObserver.Create(data, o => o.Inner.MustBePositive, true); observer.Subscribe(_ => { }); @@ -134,19 +131,16 @@ namespace Avalonia.Base.UnitTests.Data.Core // 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); + Assert.Equal(1, data.Inner.ErrorsChangedSubscriptionCount); } [Fact] public void Sends_Correct_Notifications_With_Property_Chain() { var container = new Container(); - var inner = new IndeiTest(); - var observer = ExpressionObserverBuilder.Build( - container, - $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", - true); + var observer = ExpressionObserver.Create(container, o => o.Inner.MustBePositive, true); + var result = new List(); observer.Subscribe(x => result.Add(x)); @@ -154,13 +148,12 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { new BindingNotification( - new MarkupBindingChainException("Null value", "Inner.MustBePositive", "Inner"), + new MarkupBindingChainException("Null value", "o => o.Inner.MustBePositive", "Inner"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); GC.KeepAlive(container); - GC.KeepAlive(inner); } public class ExceptionTest : NotifyingBase @@ -221,9 +214,9 @@ namespace Avalonia.Base.UnitTests.Data.Core private class Container : IndeiBase { - private object _inner; + private IndeiTest _inner; - public object Inner + public IndeiTest Inner { get { return _inner; } set { _inner = value; RaisePropertyChanged(); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index 58f3b22abd..9b587d7679 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -181,6 +181,17 @@ namespace Avalonia.Base.UnitTests.Data.Core } } + [Fact] + public async Task Should_Create_Method_Binding() + { + var data = new Class3(); + var target = ExpressionObserver.Create(data, o => (Action)o.Method); + var value = await target.Take(1); + + Assert.IsAssignableFrom(value); + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _foo; @@ -204,5 +215,10 @@ namespace Avalonia.Base.UnitTests.Data.Core public string ClrProperty { get; } = "clr-property"; } + + private class Class3 + { + public void Method() { } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs index da167e5008..cbbb5f4715 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs @@ -21,7 +21,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Array_Value() { var data = new { Foo = new [] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, x => x.Foo[1]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -29,47 +29,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() - { - var data = new { Foo = new[] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() - { - var data = new { Foo = new Dictionary { { 1, "foo" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() - { - var data = new { Foo = 5 }; - var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - [Fact] public async Task Should_Get_MultiDimensional_Array_Value() { var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1, 1]); var result = await target.Take(1); Assert.Equal("qux", result); @@ -81,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Value_For_String_Indexer() { var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -93,7 +57,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1.0]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -105,19 +69,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() - { - var data = new { Foo = new[] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1,2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -129,7 +81,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); @@ -141,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = await target.Take(1); Assert.Equal("bar", result); @@ -153,7 +105,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Add() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var target = ExpressionObserver.Create(data, o => o.Foo[2]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -171,7 +123,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Remove() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[0]"); + var target = ExpressionObserver.Create(data, o => o.Foo[0]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -189,7 +141,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Replace() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -210,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core // method, but even if it did we need to test with ObservableCollection as well // as AvaloniaList as it implements PropertyChanged as an explicit interface event. var data = new { Foo = new ObservableCollection { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -226,7 +178,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_INCC_Reset() { var data = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -245,7 +197,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); var result = new List(); using (var sub = target.Subscribe(x => result.Add(x))) @@ -264,7 +216,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_SetArrayIndex() { var data = new { Foo = new[] { "foo", "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var target = ExpressionObserver.Create(data, o => o.Foo[1]); using (target.Subscribe(_ => { })) { @@ -286,8 +238,8 @@ namespace Avalonia.Base.UnitTests.Data.Core {"foo", 1 } } }; - - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -308,8 +260,8 @@ namespace Avalonia.Base.UnitTests.Data.Core {"foo", 1 } } }; - - var target = ExpressionObserverBuilder.Build(data, "Foo[bar]"); + + var target = ExpressionObserver.Create(data, o => o.Foo["bar"]); using (target.Subscribe(_ => { })) { Assert.True(target.SetValue(4)); @@ -327,7 +279,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo["foo"] = "bar"; data.Foo["baz"] = "qux"; - var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var target = ExpressionObserver.Create(data, o => o.Foo["foo"]); using (target.Subscribe(_ => { })) { @@ -344,7 +296,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new[] { 1, 2, 3 }; - var target = ExpressionObserverBuilder.Build(data, "[1]"); + var target = ExpressionObserver.Create(data, o => o[1]); var value = await target.Take(1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs index 4f3980e59a..cf151f6244 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs @@ -19,7 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Source_Observable_Completes() { var source = new BehaviorSubject(1); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -32,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Source_Observable_Errors() { var source = new BehaviorSubject(1); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Update_Observable_Completes() { var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update); + var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -58,7 +58,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Complete_When_Update_Observable_Errors() { var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => 1, "Foo", update); + var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; target.Subscribe(_ => { }, () => completed = true); @@ -73,7 +73,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var scheduler = new TestScheduler(); var source = scheduler.CreateColdObservable( OnNext(1, new { Foo = "foo" })); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -92,7 +92,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var scheduler = new TestScheduler(); var update = scheduler.CreateColdObservable(); var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(() => data, "Foo", update); + var target = ExpressionObserver.Create(() => data, o => o.Foo, update); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -107,9 +107,9 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - private Recorded> OnNext(long time, object value) + private Recorded> OnNext(long time, T value) { - return new Recorded>(time, Notification.CreateOnNext(value)); + return new Recorded>(time, Notification.CreateOnNext(value)); } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs index 9ca1d07ee0..54d7e98903 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Negation.cs @@ -17,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Negate_Boolean_Value() { var data = new { Foo = true }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var target = ExpressionObserver.Create(data, o => !o.Foo); var result = await target.Take(1); Assert.False((bool)result); @@ -25,103 +25,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public async Task Should_Negate_0() - { - var data = new { Foo = 0 }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.True((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_1() - { - var data = new { Foo = 1 }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.False((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_False_String() - { - var data = new { Foo = "false" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.True((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Negate_True_String() - { - var data = new { Foo = "True" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.False((bool)result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() - { - var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.Equal( - new BindingNotification( - new InvalidCastException($"Unable to convert 'foo' to bool."), - BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - - [Fact] - public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() - { - var data = new { Foo = new object() }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - var result = await target.Take(1); - - Assert.Equal( - new BindingNotification( - new InvalidCastException($"Unable to convert 'System.Object' to bool."), - BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - - [Fact] - public void SetValue_Should_Return_False_For_Invalid_Value() - { - var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); - target.Subscribe(_ => { }); - - Assert.False(target.SetValue("bar")); - - GC.KeepAlive(data); - } - [Fact] public void Can_SetValue_For_Valid_Value() { var data = new Test { Foo = true }; - var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var target = ExpressionObserver.Create(data, o => !o.Foo); target.Subscribe(_ => { }); Assert.True(target.SetValue(true)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index 2a8ead4e48..701fdbce9c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -16,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core public class ExpressionObserverTests_Observable { [Fact] - public void Should_Not_Get_Observable_Value_Without_Modifier_Char() + public void Should_Not_Get_Observable_Value_Without_Streaming() { using (var sync = UnitTestSynchronizationContext.Begin()) { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -61,7 +61,7 @@ namespace Avalonia.Base.UnitTests.Data.Core using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Next^.Foo"); + var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -84,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var source = new BehaviorSubject("foo"); var data = new { Foo = source }; - var target = ExpressionObserverBuilder.Build(data, "Foo^", true); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -106,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data1 = new Class1(); var data2 = new Class2("foo"); - var target = ExpressionObserverBuilder.Build(data1, "Next^.Foo", true); + var target = ExpressionObserver.Create(data1, o => o.Next.StreamBinding().Foo, true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -128,8 +128,8 @@ namespace Avalonia.Base.UnitTests.Data.Core { using (var sync = UnitTestSynchronizationContext.Begin()) { - var data = new Class2("foo"); - var target = ExpressionObserverBuilder.Build(data, "Foo^", true); + var data = new NotStreamable(); + var target = ExpressionObserver.Create(data, o => o.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -139,7 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core new[] { new BindingNotification( - new MarkupBindingChainException("Stream operator applied to unsupported type", "Foo^", "Foo^"), + new MarkupBindingChainException("Stream operator applied to unsupported type", "o => o.StreamBinding()", "^"), BindingErrorType.Error) }, result); @@ -164,5 +164,10 @@ namespace Avalonia.Base.UnitTests.Data.Core public string Foo { get; } } + + private class NotStreamable + { + public object StreamBinding() { throw new InvalidOperationException(); } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 9381d07322..c90683959e 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -22,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -34,7 +34,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Get_Simple_Property_Value_Type() { var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); target.Subscribe(_ => { }); @@ -47,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_Value_Null() { var data = new { Foo = (string)null }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Null(result); @@ -59,7 +59,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public async Task Should_Get_Simple_Property_From_Base_Class() { var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = await target.Take(1); Assert.Equal("foo", result); @@ -70,76 +70,65 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public async Task Should_Return_BindingNotification_Error_For_Root_Null() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(default(object), "Foo"); + var target = ExpressionObserver.Create(default(Class3), o => o.Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); } [Fact] public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(AvaloniaProperty.UnsetValue, "Foo"); + var target = ExpressionObserver.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); } [Fact] public async Task Should_Return_BindingNotification_Error_For_Observable_Root_Null() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(Observable.Return(default(object)), "Foo"); + var target = ExpressionObserver.Create(Observable.Return(default(Class3)), o => o.Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); } [Fact] public async void Should_Return_BindingNotification_Error_For_Observable_Root_UnsetValue() { - var data = new Class3 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(Observable.Return(AvaloniaProperty.UnsetValue), "Foo"); + var target = ExpressionObserver.Create(Observable.Return(AvaloniaProperty.UnsetValue), o => (o as Class3).Foo); var result = await target.Take(1); Assert.Equal( new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => (o As Class3).Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue), result); - - GC.KeepAlive(data); + } [Fact] public async Task Should_Get_Simple_Property_Chain() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz); var result = await target.Take(1); Assert.Equal("baz", result); @@ -151,7 +140,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Get_Simple_Property_Chain_Type() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar.Baz); target.Subscribe(_ => { }); @@ -160,28 +149,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() - { - var data = new { Foo = new { Bar = 1 } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); - var result = await target.Take(1); - - Assert.IsType(result); - - Assert.Equal( - new BindingNotification( - new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), - result); - - GC.KeepAlive(data); - } - [Fact] public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value() { - var data = new { Foo = default(object) }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + var data = new { Foo = default(Class1) }; + var target = ExpressionObserver.Create(data, o => o.Foo.Foo.Length); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -190,7 +162,7 @@ namespace Avalonia.Base.UnitTests.Data.Core new[] { new BindingNotification( - new MarkupBindingChainException("Null value", "Foo.Bar.Baz", "Foo"), + new MarkupBindingChainException("Null value", "o => o.Foo.Foo.Length", "Foo"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, @@ -199,22 +171,11 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public void Should_Have_Null_ResultType_For_Broken_Chain() - { - var data = new { Foo = new { Bar = 1 } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); - - Assert.Null(target.ResultType); - - GC.KeepAlive(data); - } - [Fact] public void Should_Track_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -233,7 +194,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() { var data = new Class1 { Bar = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Bar"); + var target = ExpressionObserver.Create(data, o => o.Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -263,7 +224,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_End_Of_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -284,7 +245,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Property_Chain_Changing() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -317,7 +278,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Next.Bar"); + var target = ExpressionObserver.Create(data, o => ((o.Next as Class2).Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -330,7 +291,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { "bar", new BindingNotification( - new MarkupBindingChainException("Null value", "Next.Next.Bar", "Next.Next"), + new MarkupBindingChainException("Null value", "o => ((o.Next As Class2).Next As Class2).Bar", "Next.Next"), BindingErrorType.Error, AvaloniaProperty.UnsetValue), "bar" @@ -350,7 +311,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -385,7 +346,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { Foo = "foo" }; var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => data.Foo, "", update); + var target = ExpressionObserver.Create(() => data.Foo, o => o, update); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -405,7 +366,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var source = scheduler.CreateColdObservable( OnNext(1, new Class1 { Foo = "foo" }), OnNext(2, new Class1 { Foo = "bar" })); - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -421,7 +382,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Subscribing_Multiple_Times_Should_Return_Values_To_All() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result1 = new List(); var result2 = new List(); var result3 = new List(); @@ -444,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var sub1 = target.Subscribe(x => { }); var sub2 = target.Subscribe(x => { }); @@ -463,7 +424,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Set_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); using (target.Subscribe(_ => { })) { @@ -479,7 +440,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Set_Property_At_The_End_Of_Chain() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -495,7 +456,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Return_False_For_Missing_Property() { var data = new Class1 { Next = new WithoutBar() }; - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -509,7 +470,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Notify_New_Value_With_Inpc() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -524,7 +485,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Notify_New_Value_Without_Inpc() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Bar"); + var target = ExpressionObserver.Create(data, o => o.Bar); var result = new List(); target.Subscribe(x => result.Add(x)); @@ -539,7 +500,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void SetValue_Should_Return_False_For_Missing_Object() { var data = new Class1(); - var target = ExpressionObserverBuilder.Build(data, "Next.Bar"); + var target = ExpressionObserver.Create(data, o => (o.Next as Class2).Bar); using (target.Subscribe(_ => { })) { @@ -556,7 +517,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var second = new Class1 { Foo = "bar" }; var root = first; var update = new Subject(); - var target = ExpressionObserverBuilder.Build(() => root, "Foo", update); + var target = ExpressionObserver.Create(() => root, o => o.Foo, update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -571,7 +532,7 @@ namespace Avalonia.Base.UnitTests.Data.Core "foo", "bar", new BindingNotification( - new MarkupBindingChainException("Null value", "Foo", string.Empty), + new MarkupBindingChainException("Null value", "o => o.Foo", string.Empty), BindingErrorType.Error, AvaloniaProperty.UnsetValue) }, @@ -590,7 +551,7 @@ namespace Avalonia.Base.UnitTests.Data.Core Func> run = () => { var source = new Class1 { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(source, "Foo"); + var target = ExpressionObserver.Create(source, o => o.Foo); return Tuple.Create(target, new WeakReference(source)); }; @@ -674,9 +635,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { } - private Recorded> OnNext(long time, object value) + private Recorded> OnNext(long time, T value) { - return new Recorded>(time, Notification.CreateOnNext(value)); + return new Recorded>(time, Notification.CreateOnNext(value)); } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs index 974ac77155..99507a2c07 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_SetValue.cs @@ -17,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Simple_Property_Value() { var data = new { Foo = "foo" }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); using (target.Subscribe(_ => { })) { @@ -31,7 +31,8 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Set_Value_On_Simple_Property_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar); + using (target.Subscribe(_ => { })) { @@ -45,14 +46,15 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Not_Try_To_Set_Value_On_Broken_Chain() { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; - var target = ExpressionObserverBuilder.Build(data, "Foo.Bar"); + var target = ExpressionObserver.Create(data, o => o.Foo.Bar); // Ensure the ExpressionObserver's subscriptions are kept active. - target.OfType().Subscribe(x => { }); - - data.Foo = null; + using (target.OfType().Subscribe(x => { })) + { + data.Foo = null; + Assert.False(target.SetValue("foo")); + } - Assert.False(target.SetValue("foo")); } /// @@ -68,13 +70,15 @@ namespace Avalonia.Base.UnitTests.Data.Core { var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var rootObservable = new BehaviorSubject(data); - var target = ExpressionObserverBuilder.Build(rootObservable, "Foo.Bar"); + var target = ExpressionObserver.Create(rootObservable, o => o.Foo.Bar); - target.Subscribe(_ => { }); - rootObservable.OnNext(null); - target.SetValue("baz"); + using (target.Subscribe(_ => { })) + { + rootObservable.OnNext(null); + target.SetValue("baz"); + Assert.Equal("bar", data.Foo.Bar); + } - Assert.Equal("bar", data.Foo.Bar); } private class Class1 : NotifyingBase diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs index 69dec53d0e..9ea0a5e3e1 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Task.cs @@ -16,13 +16,13 @@ namespace Avalonia.Base.UnitTests.Data.Core public class ExpressionObserverTests_Task { [Fact] - public void Should_Not_Get_Task_Result_Without_Modifier_Char() + public void Should_Not_Get_Task_Result_Without_StreamBinding() { using (var sync = UnitTestSynchronizationContext.Begin()) { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = ExpressionObserverBuilder.Build(data, "Foo"); + var target = ExpressionObserver.Create(data, o => o.Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = Task.FromResult("foo") }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -60,7 +60,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var tcs = new TaskCompletionSource(); var data = new Class1(tcs.Task); - var target = ExpressionObserverBuilder.Build(data, "Next^.Foo"); + var target = ExpressionObserver.Create(data, o => o.Next.StreamBinding().Foo); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -80,7 +80,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -106,7 +106,7 @@ namespace Avalonia.Base.UnitTests.Data.Core using (var sync = UnitTestSynchronizationContext.Begin()) { var data = new { Foo = TaskFromException(new NotSupportedException()) }; - var target = ExpressionObserverBuilder.Build(data, "Foo^"); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); @@ -131,7 +131,7 @@ namespace Avalonia.Base.UnitTests.Data.Core { var tcs = new TaskCompletionSource(); var data = new { Foo = tcs.Task }; - var target = ExpressionObserverBuilder.Build(data, "Foo^", true); + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding(), true); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 8bb2381552..212b16965c 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs @@ -137,6 +137,15 @@ namespace Avalonia.Markup.UnitTests.Parsers AssertIsProperty(result[3], "Baz"); } + [Fact] + public void Should_Build_Stream_Node() + { + var result = ToList(ExpressionObserverBuilder.Parse("Foo^")); + + Assert.Equal(2, result.Count); + Assert.IsType(result[1]); + } + private void AssertIsProperty(ExpressionNode node, string name) { Assert.IsType(node); diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs new file mode 100644 index 0000000000..5b97ab7ae6 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs @@ -0,0 +1,165 @@ +// 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.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Diagnostics; +using Avalonia.Data.Core; +using Xunit; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.UnitTests.Parsers +{ + public class ExpressionObserverBuilderTests_AttachedProperty + { + private readonly Func _typeResolver; + + public ExpressionObserverBuilderTests_AttachedProperty() + { + var foo = Owner.FooProperty; + _typeResolver = (_, name) => name == "Owner" ? typeof(Owner) : null; + } + + [Fact] + public async Task Should_Get_Attached_Property_Value() + { + var data = new Class1(); + var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); + var result = await target.Take(1); + + Assert.Equal("foo", result); + + Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public async Task Should_Get_Attached_Property_Value_With_Namespace() + { + var data = new Class1(); + var target = ExpressionObserverBuilder.Build( + data, + "(NS:Owner.Foo)", + typeResolver: (ns, name) => ns == "NS" && name == "Owner" ? typeof(Owner) : null); + var result = await target.Take(1); + Assert.Equal("foo", result); + Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public async Task Should_Get_Chained_Attached_Property_Value() + { + var data = new Class1 + { + Next = new Class1 + { + [Owner.FooProperty] = "bar", + } + }; + + var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); + var result = await target.Take(1); + + Assert.Equal("bar", result); + + Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public void Should_Track_Simple_Attached_Value() + { + var data = new Class1(); + var target = ExpressionObserverBuilder.Build(data, "(Owner.Foo)", typeResolver: _typeResolver); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.SetValue(Owner.FooProperty, "bar"); + + Assert.Equal(new[] { "foo", "bar" }, result); + + sub.Dispose(); + + Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public void Should_Track_Chained_Attached_Value() + { + var data = new Class1 + { + Next = new Class1 + { + [Owner.FooProperty] = "foo", + } + }; + + var target = ExpressionObserverBuilder.Build(data, "Next.(Owner.Foo)", typeResolver: _typeResolver); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Next.SetValue(Owner.FooProperty, "bar"); + + Assert.Equal(new[] { "foo", "bar" }, result); + + sub.Dispose(); + + Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public void Should_Not_Keep_Source_Alive() + { + Func> run = () => + { + var source = new Class1(); + var target = ExpressionObserverBuilder.Build(source, "(Owner.Foo)", typeResolver: _typeResolver); + return Tuple.Create(target, new WeakReference(source)); + }; + + var result = run(); + result.Item1.Subscribe(x => { }); + + GC.Collect(); + + Assert.Null(result.Item2.Target); + } + + [Fact] + public void Should_Fail_With_Attached_Property_With_Only_1_Part() + { + var data = new Class1(); + + Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver)); + } + + [Fact] + public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts() + { + var data = new Class1(); + + Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner.Foo.Bar)", typeResolver: _typeResolver)); + } + + private static class Owner + { + public static readonly AttachedProperty FooProperty = + AvaloniaProperty.RegisterAttached( + "Foo", + typeof(Owner), + defaultValue: "foo"); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty NextProperty = + AvaloniaProperty.Register(nameof(Next)); + + public Class1 Next + { + get { return GetValue(NextProperty); } + set { SetValue(NextProperty, value); } + } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs new file mode 100644 index 0000000000..816185cb64 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs @@ -0,0 +1,59 @@ +// 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.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Diagnostics; +using Avalonia.Data.Core; +using Xunit; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.UnitTests.Parsers +{ + public class ExpressionObserverBuilderTests_AvaloniaProperty + { + public ExpressionObserverBuilderTests_AvaloniaProperty() + { + var foo = Class1.FooProperty; + } + + [Fact] + public async Task Should_Get_AvaloniaProperty_By_Name() + { + var data = new Class1(); + var target = ExpressionObserverBuilder.Build(data, "Foo"); + var result = await target.Take(1); + + Assert.Equal("foo", result); + + Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public void Should_Track_AvaloniaProperty_By_Name() + { + var data = new Class1(); + var target = ExpressionObserverBuilder.Build(data, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.SetValue(Class1.FooProperty, "bar"); + + Assert.Equal(new[] { "foo", "bar" }, result); + + sub.Dispose(); + + Assert.Null(((IAvaloniaObjectDebug)data).GetPropertyChangedSubscribers()); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", defaultValue: "foo"); + + public string ClrProperty { get; } = "clr-property"; + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs new file mode 100644 index 0000000000..39d6152b69 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs @@ -0,0 +1,371 @@ +using Avalonia.Collections; +using Avalonia.Data.Core; +using Avalonia.Diagnostics; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Parsers.Nodes; +using Avalonia.UnitTests; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Parsers +{ + public class ExpressionObserverBuilderTests_Indexer + { + [Fact] + public async Task Should_Get_Array_Value() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = await target.Take(1); + + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() + { + var data = new { Foo = new Dictionary { { 1, "foo" } } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[invalid]"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() + { + var data = new { Foo = 5 }; + var target = ExpressionObserverBuilder.Build(data, "Foo[noindexer]"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Get_MultiDimensional_Array_Value() + { + var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1, 1]"); + var result = await target.Take(1); + + Assert.Equal("qux", result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Get_Value_For_String_Indexer() + { + var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var result = await target.Take(1); + + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Get_Value_For_Non_String_Indexer() + { + var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1.0]"); + var result = await target.Take(1); + + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1,2]"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() + { + var data = new { Foo = new List { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Get_List_Value() + { + var data = new { Foo = new List { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = await target.Take(1); + + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Track_INCC_Add() + { + var data = new { Foo = new AvaloniaList { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[2]"); + var result = new List(); + + using (var sub = target.Subscribe(x => result.Add(x))) + { + data.Foo.Add("baz"); + } + + Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); + Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Track_INCC_Remove() + { + var data = new { Foo = new AvaloniaList { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[0]"); + var result = new List(); + + using (var sub = target.Subscribe(x => result.Add(x))) + { + data.Foo.RemoveAt(0); + } + + Assert.Equal(new[] { "foo", "bar" }, result); + Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Track_INCC_Replace() + { + var data = new { Foo = new AvaloniaList { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = new List(); + + using (var sub = target.Subscribe(x => result.Add(x))) + { + data.Foo[1] = "baz"; + } + + Assert.Equal(new[] { "bar", "baz" }, result); + Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Track_INCC_Move() + { + // Using ObservableCollection here because AvaloniaList does not yet have a Move + // method, but even if it did we need to test with ObservableCollection as well + // as AvaloniaList as it implements PropertyChanged as an explicit interface event. + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo.Move(0, 1); + + Assert.Equal(new[] { "bar", "foo" }, result); + + GC.KeepAlive(sub); + GC.KeepAlive(data); + } + + [Fact] + public void Should_Track_INCC_Reset() + { + var data = new { Foo = new AvaloniaList { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo.Clear(); + + Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue }, result); + + GC.KeepAlive(sub); + GC.KeepAlive(data); + } + + [Fact] + public void Should_Track_NonIntegerIndexer() + { + var data = new { Foo = new NonIntegerIndexer() }; + data.Foo["foo"] = "bar"; + data.Foo["baz"] = "qux"; + + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + var result = new List(); + + using (var sub = target.Subscribe(x => result.Add(x))) + { + data.Foo["foo"] = "bar2"; + } + + var expected = new[] { "bar", "bar2" }; + Assert.Equal(expected, result); + Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_SetArrayIndex() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = ExpressionObserverBuilder.Build(data, "Foo[1]"); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("baz")); + } + + Assert.Equal("baz", data.Foo[1]); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Set_ExistingDictionaryEntry() + { + var data = new + { + Foo = new Dictionary + { + {"foo", 1 } + } + }; + + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(4)); + } + + Assert.Equal(4, data.Foo["foo"]); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Add_NewDictionaryEntry() + { + var data = new + { + Foo = new Dictionary + { + {"foo", 1 } + } + }; + + var target = ExpressionObserverBuilder.Build(data, "Foo[bar]"); + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(4)); + } + + Assert.Equal(4, data.Foo["bar"]); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Set_NonIntegerIndexer() + { + var data = new { Foo = new NonIntegerIndexer() }; + data.Foo["foo"] = "bar"; + data.Foo["baz"] = "qux"; + + var target = ExpressionObserverBuilder.Build(data, "Foo[foo]"); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("bar2")); + } + + Assert.Equal("bar2", data.Foo["foo"]); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_Only_Binding_Works() + { + var data = new[] { 1, 2, 3 }; + + var target = ExpressionObserverBuilder.Build(data, "[1]"); + + var value = await target.Take(1); + + Assert.Equal(data[1], value); + } + + private class NonIntegerIndexer : NotifyingBase + { + private readonly Dictionary _storage = new Dictionary(); + + public string this[string key] + { + get + { + return _storage[key]; + } + set + { + _storage[key] = value; + RaisePropertyChanged(CommonPropertyNames.IndexerName); + } + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs similarity index 96% rename from tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs rename to tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs index 6bb448158e..b0623aa456 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Method.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs @@ -9,9 +9,9 @@ using System.Text; using System.Threading.Tasks; using Xunit; -namespace Avalonia.Base.UnitTests.Data.Core +namespace Avalonia.Markup.UnitTests.Parsers { - public class ExpressionObserverTests_Method + public class ExpressionObserverBuilderTests_Method { private class TestObject { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs new file mode 100644 index 0000000000..24f6407908 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Negation.cs @@ -0,0 +1,112 @@ +// 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.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Data; +using Avalonia.Markup.Parsers; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Parsers +{ + public class ExpressionObserverBuilderTests_Negation + { + [Fact] + public async Task Should_Negate_0() + { + var data = new { Foo = 0 }; + var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var result = await target.Take(1); + + Assert.True((bool)result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Negate_1() + { + var data = new { Foo = 1 }; + var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var result = await target.Take(1); + + Assert.False((bool)result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Negate_False_String() + { + var data = new { Foo = "false" }; + var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var result = await target.Take(1); + + Assert.True((bool)result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Negate_True_String() + { + var data = new { Foo = "True" }; + var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var result = await target.Take(1); + + Assert.False((bool)result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() + { + var data = new { Foo = "foo" }; + var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new InvalidCastException($"Unable to convert 'foo' to bool."), + BindingErrorType.Error), + result); + + GC.KeepAlive(data); + } + + [Fact] + public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() + { + var data = new { Foo = new object() }; + var target = ExpressionObserverBuilder.Build(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new InvalidCastException($"Unable to convert 'System.Object' to bool."), + BindingErrorType.Error), + result); + + GC.KeepAlive(data); + } + + [Fact] + public void SetValue_Should_Return_False_For_Invalid_Value() + { + var data = new { Foo = "foo" }; + var target = ExpressionObserverBuilder.Build(data, "!Foo"); + target.Subscribe(_ => { }); + + Assert.False(target.SetValue("bar")); + + GC.KeepAlive(data); + } + + private class Test + { + public bool Foo { get; set; } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs new file mode 100644 index 0000000000..a97c998264 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Property.cs @@ -0,0 +1,42 @@ +using Avalonia.Data; +using Avalonia.Markup.Parsers; +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Parsers +{ + public class ExpressionObserverBuilderTests_Property + { + [Fact] + public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() + { + var data = new { Foo = new { Bar = 1 } }; + var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + var result = await target.Take(1); + + Assert.IsType(result); + + Assert.Equal( + new BindingNotification( + new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), + result); + + GC.KeepAlive(data); + } + + [Fact] + public void Should_Have_Null_ResultType_For_Broken_Chain() + { + var data = new { Foo = new { Bar = 1 } }; + var target = ExpressionObserverBuilder.Build(data, "Foo.Bar.Baz"); + + Assert.Null(target.ResultType); + + GC.KeepAlive(data); + } + } +}