Browse Source

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

Added FindAncestor binding mode.
pull/1090/head
Steven Kirk 9 years ago
committed by GitHub
parent
commit
d645e81edd
  1. 25
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  2. 67
      src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs
  3. 41
      src/Markup/Avalonia.Markup/ControlLocator.cs
  4. 165
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs
  5. 129
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

25
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,

67
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
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
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
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 { }
}
Loading…
Cancel
Save