Browse Source

Add ability to set the value pointed to by an indexer node.

pull/813/head
Jeremy Koritzinsky 9 years ago
parent
commit
b038ed55ee
  1. 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  2. 4
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  3. 15
      src/Markup/Avalonia.Markup/Data/ISettableNode.cs
  4. 107
      src/Markup/Avalonia.Markup/Data/IndexerNode.cs
  5. 2
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  6. 11
      tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
  7. 73
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs

1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -42,6 +42,7 @@
<Compile Include="..\..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\ISettableNode.cs" />
<Compile Include="Data\StreamNode.cs" />
<Compile Include="Data\MarkupBindingChainException.cs" />
<Compile Include="Data\CommonPropertyNames.cs" />

4
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@ -153,7 +153,7 @@ namespace Avalonia.Markup.Data
/// </returns>
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;
}
/// <summary>
@ -170,7 +170,7 @@ namespace Avalonia.Markup.Data
/// Gets the type of the expression result or null if the expression could not be
/// evaluated.
/// </summary>
public Type ResultType => (Leaf as PropertyAccessorNode)?.PropertyType;
public Type ResultType => (Leaf as ISettableNode)?.PropertyType;
/// <summary>
/// Gets the leaf node.

15
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; }
}
}

107
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<string> 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<int>().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<string> 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)

2
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;

11
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()
{

73
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<string, int>
{
{"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<string, int>
{
{"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<string, string> _storage = new Dictionary<string, string>();

Loading…
Cancel
Save