Browse Source

Add support for indexers in binding expressions.

pull/237/head
Steven Kirk 11 years ago
parent
commit
e3b6ce4186
  1. 107
      src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs
  2. 10
      src/Markup/Perspex.Markup/Binding/ExpressionNode.cs
  3. 36
      src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs
  4. 9
      src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs
  5. 16
      src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs
  6. 1
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  7. 70
      tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs
  8. 113
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs
  9. 1
      tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

107
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<object> arguments)
: base(next)
{
Arguments = arguments;
var intArgs = Arguments.OfType<int>().ToArray();
if (intArgs.Length == arguments.Count)
{
_intArgs = intArgs;
}
}
public IList<object> 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;
}
}
}

10
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)
{
}
}
}

36
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<object>();
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}");
}
}
}
}

9
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)

16
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));
}
}
}
}

1
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -37,6 +37,7 @@
<Compile Include="Binding\ExpressionNodeBuilder.cs" />
<Compile Include="Binding\ExpressionValue.cs" />
<Compile Include="Binding\LogicalNotNode.cs" />
<Compile Include="Binding\ElementAccessorNode.cs" />
<Compile Include="Binding\PropertyAccessorNode.cs" />
<Compile Include="Binding\ExpressionNode.cs" />
<Compile Include="Binding\ExpressionObserver.cs" />

70
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<PropertyAccessorNode>(result[0]);
Assert.IsType<PropertyAccessorNode>(result[1]);
Assert.IsType<PropertyAccessorNode>(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<LogicalNotNode>(result[0]);
Assert.IsType<PropertyAccessorNode>(result[1]);
Assert.IsType<PropertyAccessorNode>(result[2]);
Assert.IsType<PropertyAccessorNode>(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<LogicalNotNode>(result[0]);
Assert.IsType<LogicalNotNode>(result[1]);
Assert.IsType<PropertyAccessorNode>(result[2]);
Assert.IsType<PropertyAccessorNode>(result[3]);
Assert.IsType<PropertyAccessorNode>(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<ElementAccessorNode>(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<ElementAccessorNode>(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<ElementAccessorNode>(result[2]);
AssertIsProperty(result[3], "Baz");
}
private void AssertIsProperty(ExpressionNode node, string name)
{
Assert.IsType<PropertyAccessorNode>(node);
var p = (PropertyAccessorNode)node;
Assert.Equal(name, p.PropertyName);
}
private void AssertIsIndexer(ExpressionNode node, params object[] args)
{
Assert.IsType<ElementAccessorNode>(node);
var e = (ElementAccessorNode)node;
Assert.Equal(e.Arguments.ToArray(), args.ToArray());
}
private List<ExpressionNode> ToList(ExpressionNode node)

113
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<string> { "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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[2]");
var result = new List<object>();
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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[0]");
var result = new List<object>();
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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = new List<object>();
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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = new List<object>();
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<string> { "foo", "bar" } };
var target = new ExpressionObserver(data, "Foo[1]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo.Clear();
Assert.Equal(new[] { "bar", null }, result);
}
}
}

1
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@ -72,6 +72,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Binding\ExpressionObserverTests_Indexer.cs" />
<Compile Include="Binding\ExpressionObserverTests_Negation.cs" />
<Compile Include="Binding\ExpressionObserverTests_Property.cs" />
<Compile Include="Binding\ExpressionNodeBuilderTests.cs" />

Loading…
Cancel
Save