diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs index 6836dbf9df..bc07da8b14 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs @@ -1,20 +1,64 @@ -// Copyright (c) The Avalonia Project. All rights reserved. +// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Reflection; using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Data; +using System; namespace Avalonia.Markup.Xaml.Templates { public class MemberSelector : IMemberSelector { - public string MemberName { get; set; } + public string MemberName + { + get { return _memberName; } + set + { + if (_memberName != value) + { + _memberName = value; + _expressionNode = null; + _memberValueNode = null; + } + } + } public object Select(object o) { - // TODO: Handle nested property paths, changing values etc. - var property = o.GetType().GetRuntimeProperty(MemberName); - return property?.GetValue(o); + if (string.IsNullOrEmpty(MemberName)) + { + return o; + } + + if (_expressionNode == null) + { + _expressionNode = ExpressionNodeBuilder.Build(MemberName); + + _memberValueNode = _expressionNode; + + while (_memberValueNode.Next != null) + _memberValueNode = _memberValueNode.Next; + } + + _expressionNode.Target = new WeakReference(o); + + object result = _memberValueNode.CurrentValue.Target; + + if (result == AvaloniaProperty.UnsetValue) + { + return null; + } + else if (result is BindingError) + { + return null; + } + + return result; } + + private ExpressionNode _expressionNode; + private string _memberName; + private ExpressionNode _memberValueNode; } -} +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs index 4d6ccdf31a..d74ccf00a1 100644 --- a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs +++ b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs @@ -7,4 +7,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("Avalonia.Markup")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup")] -[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")] +[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml")] \ No newline at end of file diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index a2f2d3db56..7f8c790492 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -1,4 +1,4 @@ - + @@ -103,6 +103,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs new file mode 100644 index 0000000000..4903ca7b35 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs @@ -0,0 +1,168 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Markup.Xaml.Templates; +using System; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Templates +{ + public class MemberSelectorTests + { + [Fact] + public void Should_Not_Hold_Reference_To_Object() + { + WeakReference dataRef = null; + + var selector = new MemberSelector() { MemberName = "Child.StringValue" }; + + Action run = () => + { + var data = new Item() + { + Child = new Item() { StringValue = "Value1" } + }; + + Assert.Same("Value1", selector.Select(data)); + + dataRef = new WeakReference(data); + }; + + run(); + + GC.Collect(); + + Assert.False(dataRef.IsAlive); + } + + [Fact] + public void Should_Select_Child_Property_Value() + { + var selector = new MemberSelector() { MemberName = "Child.StringValue" }; + + var data = new Item() + { + Child = new Item() { StringValue = "Value1" } + }; + + Assert.Same("Value1", selector.Select(data)); + } + + [Fact] + public void Should_Select_Child_Property_Value_In_Multiple_Items() + { + var selector = new MemberSelector() { MemberName = "Child.StringValue" }; + + var data = new Item[] + { + new Item() { Child = new Item() { StringValue = "Value1" } }, + new Item() { Child = new Item() { StringValue = "Value2" } }, + new Item() { Child = new Item() { StringValue = "Value3" } } + }; + + Assert.Same("Value1", selector.Select(data[0])); + Assert.Same("Value2", selector.Select(data[1])); + Assert.Same("Value3", selector.Select(data[2])); + } + + [Fact] + public void Should_Select_MoreComplex_Property_Value() + { + var selector = new MemberSelector() { MemberName = "Child.Child.Child.StringValue" }; + + var data = new Item() + { + Child = new Item() + { + Child = new Item() + { + Child = new Item() { StringValue = "Value1" } + } + } + }; + + Assert.Same("Value1", selector.Select(data)); + } + + [Fact] + public void Should_Select_Null_Value_On_Null_Object() + { + var selector = new MemberSelector() { MemberName = "StringValue" }; + + Assert.Equal(null, selector.Select(null)); + } + + [Fact] + public void Should_Select_Null_Value_On_Wrong_MemberName() + { + var selector = new MemberSelector() { MemberName = "WrongProperty" }; + + var data = new Item() { StringValue = "Value1" }; + + Assert.Same(null, selector.Select(data)); + } + + [Fact] + public void Should_Select_Simple_Property_Value() + { + var selector = new MemberSelector() { MemberName = "StringValue" }; + + var data = new Item() { StringValue = "Value1" }; + + Assert.Same("Value1", selector.Select(data)); + } + + [Fact] + public void Should_Select_Simple_Property_Value_In_Multiple_Items() + { + var selector = new MemberSelector() { MemberName = "StringValue" }; + + var data = new Item[] + { + new Item() { StringValue = "Value1" }, + new Item() { StringValue = "Value2" }, + new Item() { StringValue = "Value3" } + }; + + Assert.Same("Value1", selector.Select(data[0])); + Assert.Same("Value2", selector.Select(data[1])); + Assert.Same("Value3", selector.Select(data[2])); + } + + [Fact] + public void Should_Select_Target_On_Empty_MemberName() + { + var selector = new MemberSelector(); + + var data = new Item() { StringValue = "Value1" }; + + Assert.Same(data, selector.Select(data)); + } + + [Fact] + public void Should_Support_Change_Of_MemberName() + { + var selector = new MemberSelector() { MemberName = "StringValue" }; + + var data = new Item() + { + StringValue = "Value1", + IntValue = 1 + }; + + Assert.Same("Value1", selector.Select(data)); + + selector.MemberName = "IntValue"; + + Assert.Equal(1, selector.Select(data)); + } + + private class Item + { + public Item Child { get; set; } + public int IntValue { get; set; } + + public string StringValue { get; set; } + } + } +} \ No newline at end of file