From e3b6ce41863b61643a3485dfdb5c94e8be4ffecf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Oct 2015 01:08:37 +0200 Subject: [PATCH] Add support for indexers in binding expressions. --- .../Binding/ElementAccessorNode.cs | 107 +++++++++++++++++ .../Perspex.Markup/Binding/ExpressionNode.cs | 10 +- .../Binding/ExpressionNodeBuilder.cs | 36 ++++++ .../Perspex.Markup/Binding/LogicalNotNode.cs | 9 -- .../Binding/PropertyAccessorNode.cs | 16 +-- .../Perspex.Markup/Perspex.Markup.csproj | 1 + .../Binding/ExpressionNodeBuilderTests.cs | 70 +++++++++-- .../ExpressionObserverTests_Indexer.cs | 113 ++++++++++++++++++ .../Perspex.Markup.UnitTests.csproj | 1 + 9 files changed, 334 insertions(+), 29 deletions(-) create mode 100644 src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs create mode 100644 tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs diff --git a/src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs new file mode 100644 index 0000000000..d8d4e9979c --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs @@ -0,0 +1,107 @@ +// Copyright (c) The Perspex 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.Collections.Specialized; +using System.Linq; +using System.Reflection; + +namespace Perspex.Markup.Binding +{ + internal class ElementAccessorNode : ExpressionNode + { + private int[] _intArgs; + + public ElementAccessorNode(ExpressionNode next, IList arguments) + : base(next) + { + Arguments = arguments; + + var intArgs = Arguments.OfType().ToArray(); + + if (intArgs.Length == arguments.Count) + { + _intArgs = intArgs; + } + } + + public IList Arguments { get; } + + protected override void SubscribeAndUpdate(object target) + { + CurrentValue = GetValue(target); + + var incc = target as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged += CollectionChanged; + } + } + + protected override void Unsubscribe(object target) + { + var incc = target as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged -= CollectionChanged; + } + } + + private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + bool update = false; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + update = _intArgs[0] >= e.NewStartingIndex; + break; + case NotifyCollectionChangedAction.Remove: + update = _intArgs[0] >= e.OldStartingIndex; + break; + case NotifyCollectionChangedAction.Replace: + update = _intArgs[0] >= e.NewStartingIndex && + _intArgs[0] < e.NewStartingIndex + e.NewItems.Count; + break; + case NotifyCollectionChangedAction.Move: + update = (_intArgs[0] >= e.NewStartingIndex && + _intArgs[0] < e.NewStartingIndex + e.NewItems.Count) || + (_intArgs[0] >= e.OldStartingIndex && + _intArgs[0] < e.OldStartingIndex + e.OldItems.Count); + break; + case NotifyCollectionChangedAction.Reset: + update = true; + break; + } + + if (update) + { + CurrentValue = GetValue(sender); + } + } + + private ExpressionValue GetValue(object target) + { + var typeInfo = target.GetType().GetTypeInfo(); + var list = target as IList; + + if (typeInfo.IsArray && _intArgs != null) + { + return new ExpressionValue(((Array)target).GetValue(_intArgs)); + } + else if (target is IList && _intArgs?.Length == 1) + { + if (_intArgs[0] < list.Count) + { + return new ExpressionValue(list[_intArgs[0]]); + } + } + + return ExpressionValue.None; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index ff40507e7e..4bb4356c30 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -95,9 +95,13 @@ namespace Perspex.Markup.Binding } } - protected abstract void SubscribeAndUpdate(object target); - - protected abstract void Unsubscribe(object target); + protected virtual void SubscribeAndUpdate(object target) + { + CurrentValue = new ExpressionValue(target); + } + protected virtual void Unsubscribe(object target) + { + } } } diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs index b37892be4c..abcba10452 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -39,6 +39,7 @@ namespace Perspex.Markup.Binding var identifier = syntax as IdentifierNameSyntax; var memberAccess = syntax as MemberAccessExpressionSyntax; var unaryExpression = syntax as PrefixUnaryExpressionSyntax; + var elementAccess = syntax as ElementAccessExpressionSyntax; if (expressionStatement != null) { @@ -58,6 +59,11 @@ namespace Perspex.Markup.Binding next = Build(expression, unaryExpression.Operand, next); next = new LogicalNotNode(next); } + else if (elementAccess != null) + { + next = Build(expression, elementAccess, next); + next = Build(expression, elementAccess.Expression, next); + } else { throw new Exception($"Invalid expression: {expression}"); @@ -65,5 +71,35 @@ namespace Perspex.Markup.Binding return next; } + + private static ExpressionNode Build(string expression, ElementAccessExpressionSyntax syntax, ExpressionNode next) + { + var argList = syntax.ArgumentList as BracketedArgumentListSyntax; + + if (argList != null) + { + var args = new List(); + + foreach (var arg in argList.Arguments) + { + var literal = arg.Expression as LiteralExpressionSyntax; + + if (literal != null) + { + args.Add(literal.Token.Value); + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + } + + return new ElementAccessorNode(next, args); + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + } } } diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs index ca35ea5f12..6ea29deffa 100644 --- a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -24,15 +24,6 @@ namespace Perspex.Markup.Binding return Next.Select(x => Negate(x)).Subscribe(observer); } - protected override void SubscribeAndUpdate(object target) - { - CurrentValue = new ExpressionValue(target); - } - - protected override void Unsubscribe(object target) - { - } - private ExpressionValue Negate(ExpressionValue v) { if (v.HasValue) diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index da54212165..0a4b4b737e 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -65,14 +65,6 @@ namespace Perspex.Markup.Binding CurrentValue = result; } - private void PropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == PropertyName) - { - CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target)); - } - } - protected override void Unsubscribe(object target) { var inpc = target as INotifyPropertyChanged; @@ -82,5 +74,13 @@ namespace Perspex.Markup.Binding inpc.PropertyChanged -= PropertyChanged; } } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PropertyName) + { + CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target)); + } + } } } diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index fd08d6e577..7cd19704b7 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -37,6 +37,7 @@ + diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs index a6759d4a1f..18cd6c3578 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using System.Linq; using Perspex.Markup.Binding; using Xunit; @@ -24,9 +25,9 @@ namespace Perspex.Markup.UnitTests.Binding var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz")); Assert.Equal(3, result.Count); - Assert.IsType(result[0]); - Assert.IsType(result[1]); - Assert.IsType(result[2]); + AssertIsProperty(result[0], "Foo"); + AssertIsProperty(result[1], "Bar"); + AssertIsProperty(result[2], "Baz"); } [Fact] @@ -36,9 +37,9 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal(4, result.Count); Assert.IsType(result[0]); - Assert.IsType(result[1]); - Assert.IsType(result[2]); - Assert.IsType(result[3]); + AssertIsProperty(result[1], "Foo"); + AssertIsProperty(result[2], "Bar"); + AssertIsProperty(result[3], "Baz"); } [Fact] @@ -49,9 +50,60 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal(5, result.Count); Assert.IsType(result[0]); Assert.IsType(result[1]); - Assert.IsType(result[2]); - Assert.IsType(result[3]); - Assert.IsType(result[4]); + AssertIsProperty(result[2], "Foo"); + AssertIsProperty(result[3], "Bar"); + AssertIsProperty(result[4], "Baz"); + } + + [Fact] + public void Should_Build_Indexed_Property() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo[5]")); + + Assert.Equal(2, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsIndexer(result[1], 5); + Assert.IsType(result[1]); + Assert.Equal(new object[] { 5 }, ((ElementAccessorNode)result[1]).Arguments.ToArray()); + } + + [Fact] + public void Should_Build_Multiple_Indexed_Property() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 6]")); + + Assert.Equal(2, result.Count); + AssertIsProperty(result[0], "Foo"); + Assert.IsType(result[1]); + Assert.Equal(new object[] { 5, 6 }, ((ElementAccessorNode)result[1]).Arguments.ToArray()); + } + + [Fact] + public void Should_Build_Indexed_Property_In_Chain() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz")); + + Assert.Equal(4, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsProperty(result[1], "Bar"); + Assert.IsType(result[2]); + AssertIsProperty(result[3], "Baz"); + } + + private void AssertIsProperty(ExpressionNode node, string name) + { + Assert.IsType(node); + + var p = (PropertyAccessorNode)node; + Assert.Equal(name, p.PropertyName); + } + + private void AssertIsIndexer(ExpressionNode node, params object[] args) + { + Assert.IsType(node); + + var e = (ElementAccessorNode)node; + Assert.Equal(e.Arguments.ToArray(), args.ToArray()); } private List ToList(ExpressionNode node) diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs new file mode 100644 index 0000000000..f544652c52 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs @@ -0,0 +1,113 @@ +// Copyright (c) The Perspex 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.Collections.ObjectModel; +using System.Reactive.Linq; +using Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionObserverTests_Indexer + { + [Fact] + public async void Should_Get_Array_Value() + { + var data = new { Foo = new [] { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = await target.Take(1); + + Assert.True(result.HasValue); + Assert.Equal("bar", result.Value); + } + + [Fact] + public async void Should_Get_MultiDimensional_Array_Value() + { + var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; + var target = new ExpressionObserver(data, "Foo[1, 1]"); + var result = await target.Take(1); + + Assert.True(result.HasValue); + Assert.Equal("qux", result.Value); + } + + [Fact] + public async void Should_Get_List_Value() + { + var data = new { Foo = new List { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = await target.Take(1); + + Assert.True(result.HasValue); + Assert.Equal("bar", result.Value); + } + + [Fact] + public void Should_Track_INCC_Add() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[2]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + data.Foo.Add("baz"); + + Assert.Equal(new[] { null, "baz" }, result); + } + + [Fact] + public void Should_Track_INCC_Remove() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[0]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + data.Foo.RemoveAt(0); + + Assert.Equal(new[] { "foo", "bar" }, result); + } + + [Fact] + public void Should_Track_INCC_Replace() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + data.Foo[1] = "baz"; + + Assert.Equal(new[] { "bar", "baz" }, result); + } + + [Fact] + public void Should_Track_INCC_Move() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + data.Foo.Move(0, 1); + + Assert.Equal(new[] { "bar", "foo" }, result); + } + + [Fact] + public void Should_Track_INCC_Reset() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + data.Foo.Clear(); + + Assert.Equal(new[] { "bar", null }, result); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 04c42cba55..eae2018e22 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -72,6 +72,7 @@ +