From b038ed55ee5fdbce2186374249e733f62468a85a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 30 Nov 2016 14:25:39 -0600 Subject: [PATCH] Add ability to set the value pointed to by an indexer node. --- .../Avalonia.Markup/Avalonia.Markup.csproj | 1 + .../Data/ExpressionObserver.cs | 4 +- .../Avalonia.Markup/Data/ISettableNode.cs | 15 +++ .../Avalonia.Markup/Data/IndexerNode.cs | 107 +++++++++++++++++- .../Data/PropertyAccessorNode.cs | 2 +- .../Data/BindingExpressionTests.cs | 11 ++ .../Data/ExpressionObserverTests_Indexer.cs | 73 ++++++++++++ 7 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Data/ISettableNode.cs diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 5bdbb5a827..5826346fc9 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -42,6 +42,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index 37226ee74b..7ad8486bdd 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -153,7 +153,7 @@ namespace Avalonia.Markup.Data /// public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue) { - return (Leaf as PropertyAccessorNode)?.SetTargetValue(value, priority) ?? false; + return (Leaf as ISettableNode)?.SetTargetValue(value, priority) ?? false; } /// @@ -170,7 +170,7 @@ namespace Avalonia.Markup.Data /// Gets the type of the expression result or null if the expression could not be /// evaluated. /// - public Type ResultType => (Leaf as PropertyAccessorNode)?.PropertyType; + public Type ResultType => (Leaf as ISettableNode)?.PropertyType; /// /// Gets the leaf node. diff --git a/src/Markup/Avalonia.Markup/Data/ISettableNode.cs b/src/Markup/Avalonia.Markup/Data/ISettableNode.cs new file mode 100644 index 0000000000..8ee4f1de20 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/ISettableNode.cs @@ -0,0 +1,15 @@ +using Avalonia.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Markup.Data +{ + interface ISettableNode + { + bool SetTargetValue(object value, BindingPriority priority); + Type PropertyType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs index 0a6b93bad1..8ab665c4a4 100644 --- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs @@ -11,10 +11,11 @@ using System.Globalization; using System.Linq; using System.Reflection; using System.Reactive.Linq; +using Avalonia.Data; namespace Avalonia.Markup.Data { - internal class IndexerNode : ExpressionNode + internal class IndexerNode : ExpressionNode, ISettableNode { public IndexerNode(IList arguments) { @@ -51,8 +52,110 @@ namespace Avalonia.Markup.Data return Observable.Merge(inputs).StartWith(GetValue(target)); } + public bool SetTargetValue(object value, BindingPriority priority) + { + var typeInfo = Target.Target.GetType().GetTypeInfo(); + var list = Target.Target as IList; + var dictionary = Target.Target as IDictionary; + var indexerProperty = GetIndexer(typeInfo); + var indexerParameters = indexerProperty?.GetIndexParameters(); + + if (indexerProperty != null && indexerParameters.Length == Arguments.Count) + { + 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 false; + } + + convertedObjectArray[i] = temp; + } + + var intArgs = convertedObjectArray.OfType().ToArray(); + + // Try special cases where we can validate indicies + if (typeInfo.IsArray) + { + return SetValueInArray((Array)Target.Target, intArgs, value); + } + else if (Arguments.Count == 1) + { + if (list != null) + { + if (intArgs.Length == Arguments.Count && intArgs[0] >= 0 && intArgs[0] < list.Count) + { + list[intArgs[0]] = value; + return true; + } + + return false; + } + else if (dictionary != null) + { + if (dictionary.Contains(convertedObjectArray[0])) + { + dictionary[convertedObjectArray[0]] = value; + return true; + } + else + { + dictionary.Add(convertedObjectArray[0], value); + return true; + } + } + else + { + // Fallback to unchecked access + indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + return true; + } + } + else + { + // Fallback to unchecked access + indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + return true; + } + } + // 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) + { + SetValueInArray((Array)Target.Target, value); + return true; + } + return false; + } + + private bool SetValueInArray(Array array, object value) + { + int[] intArgs; + if (!ConvertArgumentsToInts(out intArgs)) + return false; + return SetValueInArray(array, intArgs); + } + + + private bool SetValueInArray(Array array, int[] indicies, object value) + { + if (ValidBounds(indicies, array)) + { + array.SetValue(value, indicies); + return true; + } + return false; + } + + public IList Arguments { get; } + public Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + private object GetValue(object target) { var typeInfo = target.GetType().GetTypeInfo(); @@ -238,7 +341,7 @@ namespace Avalonia.Markup.Data } } - return false; + return true; // Implementation defined meaning for the index, so just try to update anyway } private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) diff --git a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs index 7fb3137417..ab80d72215 100644 --- a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs @@ -10,7 +10,7 @@ using Avalonia.Markup.Data.Plugins; namespace Avalonia.Markup.Data { - internal class PropertyAccessorNode : ExpressionNode + internal class PropertyAccessorNode : ExpressionNode, ISettableNode { private readonly bool _enableValidation; private IPropertyAccessor _accessor; diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs index f93caa4adf..8770ae560c 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs @@ -37,6 +37,17 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("bar", data.StringValue); } + [Fact] + public void Should_Set_Indexed_Value() + { + var data = new { Foo = new[] { "foo" } }; + var target = new BindingExpression(new ExpressionObserver(data, "Foo[0]"), typeof(string)); + + target.OnNext("bar"); + + Assert.Equal("bar", data.Foo[0]); + } + [Fact] public async void Should_Convert_Get_String_To_Double() { diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index f6c4540611..9c7d0347a6 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs @@ -192,6 +192,79 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); } + [Fact] + public void Should_SetArrayIndex() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("baz")); + } + + Assert.Equal("baz", data.Foo[1]); + } + + [Fact] + public void Should_Set_ExistingDictionaryEntry() + { + var data = new + { + Foo = new Dictionary + { + {"foo", 1 } + } + }; + + + var target = new ExpressionObserver(data, "Foo[foo]"); + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(4)); + } + + Assert.Equal(4, data.Foo["foo"]); + } + + [Fact] + public void Should_Add_NewDictionaryEntry() + { + var data = new + { + Foo = new Dictionary + { + {"foo", 1 } + } + }; + + + var target = new ExpressionObserver(data, "Foo[bar]"); + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(4)); + } + + Assert.Equal(4, data.Foo["bar"]); + } + + [Fact] + public void Should_Set_NonIntegerIndexer() + { + var data = new { Foo = new NonIntegerIndexer() }; + data.Foo["foo"] = "bar"; + data.Foo["baz"] = "qux"; + + var target = new ExpressionObserver(data, "Foo[foo]"); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("bar2")); + } + + Assert.Equal("bar2", data.Foo["foo"]); + } + private class NonIntegerIndexer : NotifyingBase { private readonly Dictionary _storage = new Dictionary();