Browse Source

Merge pull request #1085 from AvaloniaUI/feature/1078-findancestor-bindings

Added FindAncestor binding mode.
Steven Kirk 8 years ago
parent
commit
d645e81edd

+ 25 - 0
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<ArgumentNullException>(target != null);
+
+            return new ExpressionObserver(
+                ControlLocator.Track(target, RelativeSource.AncestorType, RelativeSource.AncestorLevel -1),
+                path);
+        }
+
         private ExpressionObserver CreateSourceObserver(
             object source,
             string path,

+ 66 - 1
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
 {
+    /// <summary>
+    /// Defines the mode of a <see cref="RelativeSource"/> object.
+    /// </summary>
     public enum RelativeSourceMode
     {
-        Self,
+        /// <summary>
+        /// The binding will be to the control's data context.
+        /// </summary>
         DataContext,
+
+        /// <summary>
+        /// The binding will be to the control's templated parent.
+        /// </summary>
         TemplatedParent,
+
+        /// <summary>
+        /// The binding will be to the control iself.
+        /// </summary>
+        Self,
+
+        /// <summary>
+        /// The binding will be to an ancestor of the control in the visual tree.
+        /// </summary>
+        FindAncestor,
     }
 
+    /// <summary>
+    /// Describes the the location of a binding source, relative to the binding target.
+    /// </summary>
     public class RelativeSource
     {
+        private int _ancestorLevel = 1;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RelativeSource"/> class.
+        /// </summary>
+        /// <remarks>
+        /// This constructor initializes <see cref="Mode"/> to <see cref="RelativeSourceMode.FindAncestor"/>.
+        /// </remarks>
         public RelativeSource()
         {
+            Mode = RelativeSourceMode.FindAncestor;
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RelativeSource"/> class.
+        /// </summary>
+        /// <param name="mode">The relative source mode.</param>
         public RelativeSource(RelativeSourceMode mode)
         {
             Mode = mode;
         }
 
+        /// <summary>
+        /// Gets the level of ancestor to look for when in <see cref="RelativeSourceMode.FindAncestor"/>  mode.
+        /// </summary>
+        /// <remarks>
+        /// Use the default value of 1 to look for the first ancestor of the specified type.
+        /// </remarks>
+        public int AncestorLevel
+        {
+            get { return _ancestorLevel; }
+            set
+            {
+                if (_ancestorLevel <= 0)
+                {
+                    throw new ArgumentOutOfRangeException("AncestorLevel may not be set to less than 1.");
+                }
+
+                _ancestorLevel = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets the type of ancestor to look for when in <see cref="RelativeSourceMode.FindAncestor"/>  mode.
+        /// </summary>
+        public Type AncestorType { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value that describes the type of relative source lookup.
+        /// </summary>
         public RelativeSourceMode Mode { get; set; }
     }
 }

+ 41 - 0
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();
         }
+
+        /// <summary>
+        /// Tracks a typed visual ancestor control.
+        /// </summary>
+        /// <param name="relativeTo">
+        /// The control relative from which the other control should be found.
+        /// </param>
+        /// <param name="ancestorType">The type of the ancestor to find.</param>
+        /// <param name="ancestorLevel">
+        /// The level of ancestor control to look for. Use 0 for the first ancestor of the
+        /// requested type.
+        /// </param>
+        public static IObservable<IControl> Track(IControl relativeTo, Type ancestorType, int ancestorLevel)
+        {
+            var attached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
+                x => relativeTo.AttachedToVisualTree += x,
+                x => relativeTo.DetachedFromVisualTree += x)
+                .Select(x => true)
+                .StartWith(relativeTo.IsAttachedToVisualTree);
+
+            var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
+                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;
+                }
+            });
+        }
     }
 }

+ 165 - 0
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);
+        }
+    }
+}

+ 129 - 0
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 = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Button Name='button' Content='{Binding Foo, RelativeSource={RelativeSource DataContext}}'/>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                button.DataContext = new { Foo = "foo" };
+                window.ApplyTemplate();
+
+                Assert.Equal("foo", button.Content);
+            }
+        }
+
+        [Fact]
+        public void Binding_To_Self_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Button Name='button' Content='{Binding Name, RelativeSource={RelativeSource Self}}'/>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.ApplyTemplate();
+
+                Assert.Equal("button", button.Content);
+            }
+        }
+
+        [Fact]
+        public void Binding_To_First_Ancestor_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Border Name='border1'>
+      <Border Name='border2'>
+        <Button Name='button' Content='{Binding Name, RelativeSource={RelativeSource AncestorType=Border}}'/>
+      </Border>
+    </Border>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.ApplyTemplate();
+
+                Assert.Equal("border2", button.Content);
+            }
+        }
+
+        [Fact]
+        public void Binding_To_Second_Ancestor_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Border Name='border1'>
+      <Border Name='border2'>
+        <Button Name='button' Content='{Binding Name, RelativeSource={RelativeSource AncestorType=Border, AncestorLevel=2}}'/>
+      </Border>
+    </Border>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.ApplyTemplate();
+
+                Assert.Equal("border1", button.Content);
+            }
+        }
+
+        [Fact]
+        public void Binding_To_Ancestor_With_Namespace_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<local:TestWindow xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'
+        Title='title'>
+  <Button Name='button' Content='{Binding Title, RelativeSource={RelativeSource AncestorType=local:TestWindow}}'/>
+</local:TestWindow>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (TestWindow)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.ApplyTemplate();
+
+                Assert.Equal("title", button.Content);
+            }
+        }
+    }
+
+    public class TestWindow : Window { }
+}