diff --git a/src/Markup/Perspex.Markup/Data/CommonPropertyNames.cs b/src/Markup/Perspex.Markup/Data/CommonPropertyNames.cs new file mode 100644 index 0000000000..5866539886 --- /dev/null +++ b/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"; + } +} diff --git a/src/Markup/Perspex.Markup/Data/IndexerNode.cs b/src/Markup/Perspex.Markup/Data/IndexerNode.cs index b791e15eaa..7a6dd39301 100644 --- a/src/Markup/Perspex.Markup/Data/IndexerNode.cs +++ b/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 arguments) + public IndexerNode(IList arguments) { Arguments = arguments; - - var intArgs = Arguments.OfType().ToArray(); - - if (intArgs.Length == arguments.Count) - { - _intArgs = intArgs; - } } - public IList Arguments { get; } + public IList 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().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; } diff --git a/src/Markup/Perspex.Markup/Data/Parsers/ArgumentListParser.cs b/src/Markup/Perspex.Markup/Data/Parsers/ArgumentListParser.cs index 5b67166739..21061d7075 100644 --- a/src/Markup/Perspex.Markup/Data/Parsers/ArgumentListParser.cs +++ b/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 Parse(Reader r, char open, char close) + public static IList Parse(Reader r, char open, char close) { if (r.Peek == open) { - var result = new List(); + var result = new List(); 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(); diff --git a/src/Markup/Perspex.Markup/Data/Parsers/LiteralParser.cs b/src/Markup/Perspex.Markup/Data/Parsers/LiteralParser.cs deleted file mode 100644 index 36e08c3dbe..0000000000 --- a/src/Markup/Perspex.Markup/Data/Parsers/LiteralParser.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index 535fec127f..f5b61e2cea 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -41,6 +41,7 @@ Properties\SharedAssemblyInfo.cs + @@ -52,7 +53,6 @@ - diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs index 33d8a6ff6c..3c2ff39873 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionNodeBuilderTests.cs +++ b/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(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(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(node); var e = (IndexerNode)node; - Assert.Equal(e.Arguments.ToArray(), args.ToArray()); + Assert.Equal(e.Arguments.ToArray(), args); } private List ToList(ExpressionNode node) diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index 1359a853e3..d336afa884 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs +++ b/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 { { "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 { { 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(); + + 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 storage = new Dictionary(); + + public string this[string key] + { + get + { + return storage[key]; + } + set + { + storage[key] = value; + RaisePropertyChanged(CommonPropertyNames.IndexerName); + } + } + } } }