diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 649596a74e..3f12593197 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -2,11 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using System.Reactive; using System.Reactive.Linq; +using System.Reflection; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Data; +using Avalonia.VisualTree; namespace Avalonia.Markup.Xaml.Data { @@ -123,6 +126,17 @@ namespace Avalonia.Markup.Xaml.Data { observer = CreateTemplatedParentObserver(target, pathInfo.Path); } + else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) + { + if (RelativeSource.AncestorType == null) + { + throw new InvalidOperationException("AncestorType must be set for RelativeSourceModel.FindAncestor."); + } + + observer = CreateFindAncestorObserver( + (target as IControl) ?? (anchor as IControl), + pathInfo.Path); + } else { throw new NotSupportedException(); @@ -251,6 +265,17 @@ namespace Avalonia.Markup.Xaml.Data return result; } + private ExpressionObserver CreateFindAncestorObserver( + IControl target, + string path) + { + Contract.Requires(target != null); + + return new ExpressionObserver( + ControlLocator.Track(target, RelativeSource.AncestorType, RelativeSource.AncestorLevel -1), + path); + } + private ExpressionObserver CreateSourceObserver( object source, string path, diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs index c0eb581af2..f77df6853b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs @@ -1,26 +1,91 @@ // 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; + namespace Avalonia.Markup.Xaml.Data { + /// + /// Defines the mode of a object. + /// public enum RelativeSourceMode { - Self, + /// + /// The binding will be to the control's data context. + /// DataContext, + + /// + /// The binding will be to the control's templated parent. + /// TemplatedParent, + + /// + /// The binding will be to the control iself. + /// + Self, + + /// + /// The binding will be to an ancestor of the control in the visual tree. + /// + FindAncestor, } + /// + /// Describes the the location of a binding source, relative to the binding target. + /// public class RelativeSource { + private int _ancestorLevel = 1; + + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor initializes to . + /// public RelativeSource() { + Mode = RelativeSourceMode.FindAncestor; } + /// + /// Initializes a new instance of the class. + /// + /// The relative source mode. public RelativeSource(RelativeSourceMode mode) { Mode = mode; } + /// + /// Gets the level of ancestor to look for when in mode. + /// + /// + /// Use the default value of 1 to look for the first ancestor of the specified type. + /// + public int AncestorLevel + { + get { return _ancestorLevel; } + set + { + if (_ancestorLevel <= 0) + { + throw new ArgumentOutOfRangeException("AncestorLevel may not be set to less than 1."); + } + + _ancestorLevel = value; + } + } + + /// + /// Gets the type of ancestor to look for when in mode. + /// + public Type AncestorType { get; set; } + + /// + /// Gets or sets a value that describes the type of relative source lookup. + /// public RelativeSourceMode Mode { get; set; } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/ControlLocator.cs b/src/Markup/Avalonia.Markup/ControlLocator.cs index a10fd7e283..de8415d6db 100644 --- a/src/Markup/Avalonia.Markup/ControlLocator.cs +++ b/src/Markup/Avalonia.Markup/ControlLocator.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using System.Reactive.Linq; +using System.Reflection; using Avalonia.Controls; using Avalonia.LogicalTree; +using Avalonia.VisualTree; namespace Avalonia.Markup { @@ -59,5 +61,44 @@ namespace Avalonia.Markup } }).Switch(); } + + /// + /// Tracks a typed visual ancestor control. + /// + /// + /// The control relative from which the other control should be found. + /// + /// The type of the ancestor to find. + /// + /// The level of ancestor control to look for. Use 0 for the first ancestor of the + /// requested type. + /// + public static IObservable Track(IControl relativeTo, Type ancestorType, int ancestorLevel) + { + var attached = Observable.FromEventPattern( + x => relativeTo.AttachedToVisualTree += x, + x => relativeTo.DetachedFromVisualTree += x) + .Select(x => true) + .StartWith(relativeTo.IsAttachedToVisualTree); + + var detached = Observable.FromEventPattern( + x => relativeTo.DetachedFromVisualTree += x, + x => relativeTo.DetachedFromVisualTree += x) + .Select(x => false); + + return attached.Merge(detached).Select(isAttachedToVisualTree => + { + if (isAttachedToVisualTree) + { + return relativeTo.GetVisualAncestors() + .Where(x => ancestorType.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo())) + .ElementAtOrDefault(ancestorLevel) as IControl; + } + else + { + return null; + } + }); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs new file mode 100644 index 0000000000..e912770470 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs @@ -0,0 +1,165 @@ +// 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.Controls; +using Avalonia.Markup.Xaml.Data; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Data +{ + public class BindingTests_RelativeSource + { + [Fact] + public void Should_Bind_To_First_Ancestor() + { + TextBlock target; + var root = new TestRoot + { + Child = new Decorator + { + Name = "decorator", + Child = target = new TextBlock(), + }, + }; + + var binding = new Binding + { + Path = "Name", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("decorator", target.Text); + } + + [Fact] + public void Should_Bind_To_Second_Ancestor() + { + TextBlock target; + var root = new TestRoot + { + Child = new Decorator + { + Name = "decorator1", + Child = new Decorator + { + Name = "decorator2", + Child = target = new TextBlock(), + } + }, + }; + + var binding = new Binding + { + Path = "Name", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + AncestorLevel = 2, + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("decorator1", target.Text); + } + + [Fact] + public void Should_Bind_To_Derived_Ancestor_Type() + { + TextBlock target; + var root = new TestRoot + { + Child = new Border + { + Name = "border", + Child = target = new TextBlock(), + }, + }; + + var binding = new Binding + { + Path = "Name", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("border", target.Text); + } + + [Fact] + public void Should_Produce_Null_If_Ancestor_Not_Found() + { + TextBlock target; + var root = new TestRoot + { + Child = new Decorator + { + Name = "decorator", + Child = target = new TextBlock(), + }, + }; + + var binding = new Binding + { + Path = "Name", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + AncestorLevel = 2, + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal(null, target.Text); + } + + [Fact] + public void Should_Update_When_Detached_And_Attached_To_Visual_Tree() + { + TextBlock target; + Decorator decorator1; + Decorator decorator2; + var root1 = new TestRoot + { + Child = decorator1 = new Decorator + { + Name = "decorator1", + Child = target = new TextBlock(), + }, + }; + + var root2 = new TestRoot + { + Child = decorator2 = new Decorator + { + Name = "decorator2", + }, + }; + + var binding = new Binding + { + Path = "Name", + RelativeSource = new RelativeSource + { + AncestorType = typeof(Decorator), + } + }; + + target.Bind(TextBox.TextProperty, binding); + Assert.Equal("decorator1", target.Text); + + decorator1.Child = null; + Assert.Null(target.Text); + + decorator2.Child = target; + Assert.Equal("decorator2", target.Text); + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs new file mode 100644 index 0000000000..9125e1e643 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -0,0 +1,129 @@ +// 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.Controls; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class BindingTests_RelativeSource + { + [Fact] + public void Binding_To_DataContext_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +