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();