Browse Source

Merge branch 'master' of https://github.com/jkoritzinsky/Perspex into jkoritzinsky-master

pull/448/head
Steven Kirk 10 years ago
parent
commit
232e04bfd3
  1. 13
      src/Markup/Perspex.Markup/Data/CommonPropertyNames.cs
  2. 213
      src/Markup/Perspex.Markup/Data/IndexerNode.cs
  3. 17
      src/Markup/Perspex.Markup/Data/Parsers/ArgumentListParser.cs
  4. 35
      src/Markup/Perspex.Markup/Data/Parsers/LiteralParser.cs
  5. 2
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  6. 27
      tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs
  7. 55
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs

13
src/Markup/Perspex.Markup/Data/CommonPropertyNames.cs

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Perspex.Markup.Data
{
public static class CommonPropertyNames
{
public const string IndexerName = "Item";
}
}

213
src/Markup/Perspex.Markup/Data/IndexerNode.cs

@ -1,10 +1,13 @@
// 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 Perspex.Utilities;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
@ -12,21 +15,12 @@ namespace Perspex.Markup.Data
{
internal class IndexerNode : ExpressionNode
{
private readonly int[] _intArgs;
public IndexerNode(IList<object> arguments)
public IndexerNode(IList<string> arguments)
{
Arguments = arguments;
var intArgs = Arguments.OfType<int>().ToArray();
if (intArgs.Length == arguments.Count)
{
_intArgs = intArgs;
}
}
public IList<object> Arguments { get; }
public IList<string> Arguments { get; }
protected override void SubscribeAndUpdate(object target)
{
@ -38,6 +32,26 @@ namespace Perspex.Markup.Data
{
incc.CollectionChanged += CollectionChanged;
}
var inpc = target as INotifyPropertyChanged;
if(inpc != null)
{
inpc.PropertyChanged += IndexerPropertyChanged;
}
}
private void IndexerPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var typeInfo = sender.GetType().GetTypeInfo();
if (typeInfo.GetDeclaredProperty(e.PropertyName) == null)
{
return;
}
if (typeInfo.GetDeclaredProperty(e.PropertyName).GetIndexParameters().Any())
{
CurrentValue = GetValue(sender);
}
}
protected override void Unsubscribe(object target)
@ -52,29 +66,41 @@ namespace Perspex.Markup.Data
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
bool update = false;
switch (e.Action)
var update = false;
if (sender is IList)
{
object indexObject;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
{
return;
}
var index = (int)indexObject;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
update = index >= e.NewStartingIndex;
break;
case NotifyCollectionChangedAction.Remove:
update = index >= e.OldStartingIndex;
break;
case NotifyCollectionChangedAction.Replace:
update = index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count;
break;
case NotifyCollectionChangedAction.Move:
update = (index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems.Count) ||
(index >= e.OldStartingIndex &&
index < e.OldStartingIndex + e.OldItems.Count);
break;
case NotifyCollectionChangedAction.Reset:
update = true;
break;
}
}
else
{
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;
update = true;
}
if (update)
@ -87,34 +113,129 @@ namespace Perspex.Markup.Data
{
var typeInfo = target.GetType().GetTypeInfo();
var list = target as IList;
if (typeInfo.IsArray && _intArgs != null)
var dictionary = target as IDictionary;
var indexerProperty = GetIndexer(typeInfo);
var indexerParameters = indexerProperty?.GetIndexParameters();
if (indexerProperty != null && indexerParameters.Length == Arguments.Count)
{
var array = (Array)target;
var convertedObjectArray = new object[indexerParameters.Length];
for (int i = 0; i < Arguments.Count; i++)
{
object temp = null;
if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp))
{
return PerspexProperty.UnsetValue;
}
convertedObjectArray[i] = temp;
}
var intArgs = convertedObjectArray.OfType<int>().ToArray();
if (InBounds(_intArgs, array))
// Try special cases where we can validate indicies
if (typeInfo.IsArray)
{
return GetValueFromArray((Array)target, intArgs);
}
else if (Arguments.Count == 1)
{
if (list != null)
{
if (intArgs.Length == Arguments.Count && intArgs[0] >= 0 && intArgs[0] < list.Count)
{
return list[intArgs[0]];
}
return PerspexProperty.UnsetValue;
}
else if (dictionary != null)
{
if (dictionary.Contains(convertedObjectArray[0]))
{
return dictionary[convertedObjectArray[0]];
}
return PerspexProperty.UnsetValue;
}
else
{
// Fallback to unchecked access
return indexerProperty.GetValue(target, convertedObjectArray);
}
}
else
{
return array.GetValue(_intArgs);
// Fallback to unchecked access
return indexerProperty.GetValue(target, convertedObjectArray);
}
}
else if (target is IList && _intArgs?.Length == 1)
// Multidimensional arrays end up here because the indexer search picks up the IList indexer instead of the
// multidimensional indexer, which doesn't take the same number of arguments
else if (typeInfo.IsArray)
{
return GetValueFromArray((Array)target);
}
return PerspexProperty.UnsetValue;
}
private object GetValueFromArray(Array array)
{
int[] intArgs;
if (!ConvertArgumentsToInts(out intArgs))
return PerspexProperty.UnsetValue;
return GetValueFromArray(array, intArgs);
}
private object GetValueFromArray(Array array, int[] indicies)
{
if (ValidBounds(indicies, array))
{
if (_intArgs[0] < list.Count)
return array.GetValue(indicies);
}
return PerspexProperty.UnsetValue;
}
private bool ConvertArgumentsToInts(out int[] intArgs)
{
intArgs = new int[Arguments.Count];
for (int i = 0; i < Arguments.Count; ++i)
{
object value;
if (!TypeUtilities.TryConvert(typeof(int), Arguments[i], CultureInfo.InvariantCulture, out value))
{
return list[_intArgs[0]];
return false;
}
intArgs[i] = (int)value;
}
return true;
}
return PerspexProperty.UnsetValue;
private static PropertyInfo GetIndexer(TypeInfo typeInfo)
{
PropertyInfo indexer;
for (;typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo())
{
// Check for the default indexer name first to make this faster.
// This will only be false when a class in VB has a custom indexer name.
if ((indexer = typeInfo.GetDeclaredProperty(CommonPropertyNames.IndexerName)) != null)
{
return indexer;
}
foreach (var property in typeInfo.DeclaredProperties)
{
if (property.GetIndexParameters().Any())
{
return property;
}
}
}
return null;
}
private bool InBounds(int[] args, Array array)
private bool ValidBounds(int[] indicies, Array array)
{
if (args.Length == array.Rank)
if (indicies.Length == array.Rank)
{
for (var i = 0; i < args.Length; ++i)
for (var i = 0; i < indicies.Length; ++i)
{
if (args[i] >= array.GetLength(i))
if (indicies[i] >= array.GetLength(i))
{
return false;
}

17
src/Markup/Perspex.Markup/Data/Parsers/ArgumentListParser.cs

@ -3,31 +3,32 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Perspex.Markup.Data.Parsers
{
internal static class ArgumentListParser
{
public static IList<object> Parse(Reader r, char open, char close)
public static IList<string> Parse(Reader r, char open, char close)
{
if (r.Peek == open)
{
var result = new List<object>();
var result = new List<string>();
r.Take();
while (!r.End)
{
var literal = LiteralParser.Parse(r);
if (literal != null)
var builder = new StringBuilder();
while (!r.End && r.Peek != ',' && r.Peek != close && !char.IsWhiteSpace(r.Peek))
{
result.Add(literal);
builder.Append(r.Take());
}
else
if (builder.Length == 0)
{
throw new ExpressionParseException(r.Position, "Expected integer.");
throw new ExpressionParseException(r.Position, "Expected indexer argument.");
}
result.Add(builder.ToString());
r.SkipWhitespace();

35
src/Markup/Perspex.Markup/Data/Parsers/LiteralParser.cs

@ -1,35 +0,0 @@
// 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.Globalization;
using System.Text;
namespace Perspex.Markup.Data.Parsers
{
internal static class LiteralParser
{
public static object Parse(Reader r)
{
if (char.IsDigit(r.Peek))
{
StringBuilder result = new StringBuilder();
while (!r.End)
{
if (char.IsDigit(r.Peek))
{
result.Append(r.Take());
}
else
{
break;
}
}
return int.Parse(result.ToString(), CultureInfo.InvariantCulture);
}
return null;
}
}
}

2
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -41,6 +41,7 @@
<Compile Include="..\..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\CommonPropertyNames.cs" />
<Compile Include="Data\ExpressionNodeBuilder.cs" />
<Compile Include="Data\ExpressionParseException.cs" />
<Compile Include="Data\ExpressionSubject.cs" />
@ -52,7 +53,6 @@
<Compile Include="Data\LogicalNotNode.cs" />
<Compile Include="Data\IndexerNode.cs" />
<Compile Include="Data\Parsers\ArgumentListParser.cs" />
<Compile Include="Data\Parsers\LiteralParser.cs" />
<Compile Include="Data\Parsers\IdentifierParser.cs" />
<Compile Include="Data\Parsers\ExpressionParser.cs" />
<Compile Include="Data\Parsers\Reader.cs" />

27
tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs

@ -77,7 +77,18 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 15);
AssertIsIndexer(result[1], "15");
Assert.IsType<IndexerNode>(result[1]);
}
[Fact]
public void Should_Build_Indexed_Property_StringIndex()
{
var result = ToList(ExpressionNodeBuilder.Build("Foo[Key]"));
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], "Key");
Assert.IsType<IndexerNode>(result[1]);
}
@ -88,7 +99,7 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 15, 6);
AssertIsIndexer(result[1], "15", "6");
}
[Fact]
@ -98,7 +109,7 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(2, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 5, 16);
AssertIsIndexer(result[1], "5", "16");
}
[Fact]
@ -108,8 +119,8 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(3, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsIndexer(result[1], 15);
AssertIsIndexer(result[2], 16);
AssertIsIndexer(result[1], "15");
AssertIsIndexer(result[2], "16");
}
[Fact]
@ -120,7 +131,7 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(4, result.Count);
AssertIsProperty(result[0], "Foo");
AssertIsProperty(result[1], "Bar");
AssertIsIndexer(result[2], 5, 6);
AssertIsIndexer(result[2], "5", "6");
AssertIsProperty(result[3], "Baz");
}
@ -132,12 +143,12 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(name, p.PropertyName);
}
private void AssertIsIndexer(ExpressionNode node, params object[] args)
private void AssertIsIndexer(ExpressionNode node, params string[] args)
{
Assert.IsType<IndexerNode>(node);
var e = (IndexerNode)node;
Assert.Equal(e.Arguments.ToArray(), args.ToArray());
Assert.Equal(e.Arguments.ToArray(), args);
}
private List<ExpressionNode> ToList(ExpressionNode node)

55
tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs

@ -32,6 +32,26 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal("qux", result);
}
[Fact]
public async void Should_Get_Value_For_String_Indexer()
{
var data = new { Foo = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "qux" } } };
var target = new ExpressionObserver(data, "Foo[foo]");
var result = await target.Take(1);
Assert.Equal("bar", result);
}
[Fact]
public async void Should_Get_Value_For_Non_String_Indexer()
{
var data = new { Foo = new Dictionary<double, string> { { 1.0, "bar" }, { 2.0, "qux" } } };
var target = new ExpressionObserver(data, "Foo[1.0]");
var result = await target.Take(1);
Assert.Equal("bar", result);
}
[Fact]
public async void Array_Out_Of_Bounds_Should_Return_UnsetValue()
{
@ -136,5 +156,40 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue }, result);
}
[Fact]
public void Should_Track_NonIntegerIndexer()
{
var data = new { Foo = new NonIntegerIndexer() };
data.Foo["foo"] = "bar";
data.Foo["baz"] = "qux";
var target = new ExpressionObserver(data, "Foo[foo]");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x));
data.Foo["foo"] = "bar2";
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
}
private class NonIntegerIndexer : NotifyingBase
{
private Dictionary<string, string> storage = new Dictionary<string, string>();
public string this[string key]
{
get
{
return storage[key];
}
set
{
storage[key] = value;
RaisePropertyChanged(CommonPropertyNames.IndexerName);
}
}
}
}
}

Loading…
Cancel
Save