Browse Source

Reintroduce support for binding anchors.

Turns out these were needed by animations, just our animation system has no unit tests so I missed that fact earlier. Add a basic animation unit test that fails without anchor support, and add binding anchors back in. Currently a private API as I suspect this feature shouldn't be needed outside the framework.
pull/13970/head
Steven Kirk 3 years ago
parent
commit
f982f19c89
  1. 4
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  2. 31
      src/Avalonia.Base/AvaloniaObject.cs
  3. 2
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  4. 5
      src/Avalonia.Base/Data/Core/IBinding2.cs
  5. 2
      src/Avalonia.Base/Data/IndexerBinding.cs
  6. 2
      src/Avalonia.Base/Data/TemplateBinding.cs
  7. 2
      src/Avalonia.Base/Styling/Setter.cs
  8. 12
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  9. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  10. 12
      src/Markup/Avalonia.Markup/Data/Binding.cs
  11. 10
      src/Markup/Avalonia.Markup/Data/BindingBase.cs
  12. 2
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  13. 124
      tests/Avalonia.Base.UnitTests/Animation/StyleAnimationTests.cs

4
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@ -59,11 +59,11 @@ namespace Avalonia.Animation
if (value is IBinding binding)
{
return this.Bind(ValueProperty, binding);
return Bind(ValueProperty, binding, targetControl);
}
else
{
return this.Bind(ValueProperty, Observable.SingleValue(value).ToBinding());
return Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl);
}
}

31
src/Avalonia.Base/AvaloniaObject.cs

@ -418,12 +418,7 @@ namespace Avalonia
/// </returns>
public BindingExpressionBase Bind(AvaloniaProperty property, IBinding binding)
{
if (binding is not IBinding2 b)
throw new NotSupportedException($"Unsupported IBinding implementation '{binding}'.");
if (b.Instance(this, property) is not UntypedBindingExpressionBase expression)
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");
return GetValueStore().AddBinding(property, expression);
return Bind(property, binding, null);
}
/// <summary>
@ -593,6 +588,30 @@ namespace Avalonia
/// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an <see cref="IBinding"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="binding">The binding.</param>
/// <param name="anchor">
/// An optional anchor from which to locate required context. When binding to objects that
/// are not in the logical tree, certain types of binding need an anchor into the tree in
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provide this context.
/// </param>
/// <returns>
/// The binding expression which represents the binding instance on this object.
/// </returns>
internal BindingExpressionBase Bind(AvaloniaProperty property, IBinding binding, object? anchor)
{
if (binding is not IBinding2 b)
throw new NotSupportedException($"Unsupported IBinding implementation '{binding}'.");
if (b.Instance(this, property, anchor) is not UntypedBindingExpressionBase expression)
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");
return GetValueStore().AddBinding(property, expression);
}
internal void AddInheritanceChild(AvaloniaObject child)
{
_inheritanceChildren ??= new List<AvaloniaObject>();

2
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -375,7 +375,7 @@ namespace Avalonia
return new InstancedBinding(expression, BindingMode.OneWay, BindingPriority.LocalValue);
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
{
return new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue);
}

5
src/Avalonia.Base/Data/Core/IBinding2.cs

@ -2,5 +2,8 @@
internal interface IBinding2 : IBinding
{
BindingExpressionBase Instance(AvaloniaObject target, AvaloniaProperty targetProperty);
BindingExpressionBase Instance(
AvaloniaObject target,
AvaloniaProperty targetProperty,
object? anchor);
}

2
src/Avalonia.Base/Data/IndexerBinding.cs

@ -31,7 +31,7 @@ namespace Avalonia.Data
return new InstancedBinding(expression, Mode, BindingPriority.LocalValue);
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty, object? anchor)
{
return new IndexerBindingExpression(Source, Property, target, targetProperty, Mode);
}

2
src/Avalonia.Base/Data/TemplateBinding.cs

@ -79,7 +79,7 @@ namespace Avalonia.Data
return new(target, InstanceCore(), Mode, BindingPriority.Template);
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
{
return InstanceCore();
}

2
src/Avalonia.Base/Styling/Setter.cs

@ -109,7 +109,7 @@ namespace Avalonia.Styling
{
if (!Property!.IsDirect)
{
var expression = binding.Instance(target, Property);
var expression = binding.Instance(target, Property, null);
expression.Attach(target.GetValueStore(), target, Property, instance.Priority);
return expression;
}

12
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@ -53,14 +53,17 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
object? anchor = null,
bool enableDataValidation = false)
{
var expression = InstanceCore(target, targetProperty, enableDataValidation);
var expression = InstanceCore(target, targetProperty, anchor, enableDataValidation);
return new InstancedBinding(target, expression, Mode, Priority);
}
private protected override BindingExpressionBase Instance(AvaloniaProperty targetProperty, AvaloniaObject target)
private protected override BindingExpressionBase Instance(
AvaloniaProperty targetProperty,
AvaloniaObject target,
object? anchor)
{
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false;
return InstanceCore(target, targetProperty, enableDataValidation);
return InstanceCore(target, targetProperty, anchor, enableDataValidation);
}
/// <summary>
@ -96,6 +99,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
private BindingExpression InstanceCore(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor,
bool enableDataValidation)
{
var nodes = new List<ExpressionNode>();
@ -111,7 +115,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
// If the first node is an ISourceNode then allow it to select the source; otherwise
// use the binding source if specified, falling back to the target.
var source = nodes.Count > 0 && nodes[0] is SourceNode sn
? sn.SelectSource(Source, target, DefaultAnchor?.Target)
? sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target)
: Source != AvaloniaProperty.UnsetValue ? Source : target;
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty);

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@ -54,7 +54,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
return new InstancedBinding(target, expression, BindingMode.OneWay, _priority);
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty, object? anchor)
{
if (ResourceKey is null)
throw new InvalidOperationException("DynamicResource must have a ResourceKey.");

12
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -67,14 +67,17 @@ namespace Avalonia.Data
object? anchor = null,
bool enableDataValidation = false)
{
var expression = InstanceCore(targetProperty, target, enableDataValidation);
var expression = InstanceCore(targetProperty, target, anchor, enableDataValidation);
return new InstancedBinding(target, expression, Mode, Priority);
}
private protected override BindingExpressionBase Instance(AvaloniaProperty targetProperty, AvaloniaObject target)
private protected override BindingExpressionBase Instance(
AvaloniaProperty targetProperty,
AvaloniaObject target,
object? anchor)
{
var enableDataValidation = targetProperty.GetMetadata(target.GetType()).EnableDataValidation ?? false;
return InstanceCore(targetProperty, target, enableDataValidation);
return InstanceCore(targetProperty, target, anchor, enableDataValidation);
}
/// <summary>
@ -125,6 +128,7 @@ namespace Avalonia.Data
private UntypedBindingExpressionBase InstanceCore(
AvaloniaProperty? targetProperty,
AvaloniaObject target,
object? anchor,
bool enableDataValidation)
{
var nodes = new List<ExpressionNode>();
@ -153,7 +157,7 @@ namespace Avalonia.Data
// If the first node is an ISourceNode then allow it to select the source; otherwise
// use the binding source if specified, falling back to the target.
var source = nodes.Count > 0 && nodes[0] is SourceNode sn ?
sn.SelectSource(Source, target, DefaultAnchor?.Target) :
sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target) :
Source != AvaloniaProperty.UnsetValue ? Source : target;
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty);

10
src/Markup/Avalonia.Markup/Data/BindingBase.cs

@ -94,7 +94,10 @@ namespace Avalonia.Data
object? anchor = null,
bool enableDataValidation = false);
private protected abstract BindingExpressionBase Instance(AvaloniaProperty targetProperty, AvaloniaObject target);
private protected abstract BindingExpressionBase Instance(
AvaloniaProperty targetProperty,
AvaloniaObject target,
object? anchor);
private protected (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata(
AvaloniaObject target,
@ -115,6 +118,9 @@ namespace Avalonia.Data
return (mode, trigger);
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property) => Instance(property, target);
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
{
return Instance(property, target, anchor);
}
}
}

2
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -89,7 +89,7 @@ namespace Avalonia.Data
}
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property)
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
{
// TODO: Implement MultiBindingExpression instead of wrapping an observable.
var o = InstanceCore(target, property);

124
tests/Avalonia.Base.UnitTests/Animation/StyleAnimationTests.cs

@ -0,0 +1,124 @@
using System;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Animation;
using Animation = global::Avalonia.Animation.Animation;
public class StyleAnimationTests
{
[Fact]
public void Application_ControlTheme_Applies_Animation()
{
using var app = new AnimationTestApplication
{
Resources =
{
{ typeof(Button), CreateControlTheme<Button>(CreateOpacityAnimation()) },
},
};
var target = new Button();
var root = CreateRoot(target, app);
app.Clock.Pulse(TimeSpan.Zero);
Assert.Equal(1, target.Opacity);
app.Clock.Pulse(TimeSpan.FromSeconds(0.5));
Assert.Equal(0.5, target.Opacity);
app.Clock.Pulse(TimeSpan.FromSeconds(1));
Assert.Equal(0, target.Opacity);
}
[Fact]
public void Application_ControlTheme_Applies_Animation_With_Bound_KeyFrame_Values()
{
var fromBinding = new Binding("From");
var toBinding = new Binding("To");
using var app = new AnimationTestApplication
{
Resources =
{
{ typeof(Button), CreateControlTheme<Button>(CreateOpacityAnimation(fromBinding, toBinding)) },
},
};
var target = new Button { DataContext = new KeyFrameValues(1.0, 0.0) };
var root = CreateRoot(target, app);
app.Clock.Pulse(TimeSpan.Zero);
Assert.Equal(1, target.Opacity);
app.Clock.Pulse(TimeSpan.FromSeconds(0.5));
Assert.Equal(0.5, target.Opacity);
app.Clock.Pulse(TimeSpan.FromSeconds(1));
Assert.Equal(0, target.Opacity);
}
private static ControlTheme CreateControlTheme<T>(Animation animation)
{
return new ControlTheme(typeof(T))
{
Animations = { animation }
};
}
private static object CreateRoot(Button child, Application app)
{
var root = new TestRoot{ StylingParent = app };
root.Child = child;
root.LayoutManager.ExecuteInitialLayoutPass();
return root;
}
private static Animation CreateOpacityAnimation()
{
return CreateOpacityAnimation(1.0, 0.0);
}
private static Animation CreateOpacityAnimation(object from, object to)
{
return new Animation
{
Duration = TimeSpan.FromSeconds(1),
FillMode = FillMode.Both,
Children =
{
new KeyFrame
{
KeyTime = TimeSpan.FromSeconds(0),
Setters = { new Setter(Button.OpacityProperty, from) }
},
new KeyFrame
{
KeyTime = TimeSpan.FromSeconds(1),
Setters = {new Setter(Button.OpacityProperty, to) }
},
}
};
}
private class AnimationTestApplication : Application, IDisposable
{
private readonly IDisposable _lifetime;
public AnimationTestApplication()
{
var services = new TestServices(globalClock: Clock);
_lifetime = UnitTestApplication.Start(services);
}
public MockGlobalClock Clock { get; } = new MockGlobalClock();
public void Dispose() => _lifetime.Dispose();
}
private record KeyFrameValues(double From, double To);
}
Loading…
Cancel
Save