From f982f19c89e8f77cac130b0cea63d1ca0bb019eb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 26 Nov 2023 23:04:29 +0100 Subject: [PATCH] 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. --- .../Animation/AnimatorKeyFrame.cs | 4 +- src/Avalonia.Base/AvaloniaObject.cs | 31 ++++- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 2 +- src/Avalonia.Base/Data/Core/IBinding2.cs | 5 +- src/Avalonia.Base/Data/IndexerBinding.cs | 2 +- src/Avalonia.Base/Data/TemplateBinding.cs | 2 +- src/Avalonia.Base/Styling/Setter.cs | 2 +- .../CompiledBindingExtension.cs | 12 +- .../DynamicResourceExtension.cs | 2 +- src/Markup/Avalonia.Markup/Data/Binding.cs | 12 +- .../Avalonia.Markup/Data/BindingBase.cs | 10 +- .../Avalonia.Markup/Data/MultiBinding.cs | 2 +- .../Animation/StyleAnimationTests.cs | 124 ++++++++++++++++++ 13 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Animation/StyleAnimationTests.cs diff --git a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs index 5e81fb78a8..4db58fac5c 100644 --- a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs +++ b/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); } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e8f96cb0e4..66765ede99 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -418,12 +418,7 @@ namespace Avalonia /// 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); } /// @@ -593,6 +588,30 @@ namespace Avalonia /// The property. public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property); + /// + /// Binds a to an . + /// + /// The property. + /// The binding. + /// + /// 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 parameter + /// can be used to provide this context. + /// + /// + /// The binding expression which represents the binding instance on this object. + /// + 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(); diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index a61cfaf5a9..90465057bd 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/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); } diff --git a/src/Avalonia.Base/Data/Core/IBinding2.cs b/src/Avalonia.Base/Data/Core/IBinding2.cs index aad4f13caa..f82b57367d 100644 --- a/src/Avalonia.Base/Data/Core/IBinding2.cs +++ b/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); } diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index 90f7eb0caa..02bde6f774 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/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); } diff --git a/src/Avalonia.Base/Data/TemplateBinding.cs b/src/Avalonia.Base/Data/TemplateBinding.cs index 2a1133fc9f..3701f6a71c 100644 --- a/src/Avalonia.Base/Data/TemplateBinding.cs +++ b/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(); } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index a92fe74991..a59dd0dba8 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/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; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 0c302dc0ac..ae09a22a28 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/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); } /// @@ -96,6 +99,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions private BindingExpression InstanceCore( AvaloniaObject target, AvaloniaProperty? targetProperty, + object? anchor, bool enableDataValidation) { var nodes = new List(); @@ -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); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 1f4346ec89..0167b3b89a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/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."); diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index c310257199..a1b805c108 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/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); } /// @@ -125,6 +128,7 @@ namespace Avalonia.Data private UntypedBindingExpressionBase InstanceCore( AvaloniaProperty? targetProperty, AvaloniaObject target, + object? anchor, bool enableDataValidation) { var nodes = new List(); @@ -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); diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 598b056165..21cb9a942e 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/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); + } } } diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 6bd53d93a8..044dfb4a4d 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/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); diff --git a/tests/Avalonia.Base.UnitTests/Animation/StyleAnimationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/StyleAnimationTests.cs new file mode 100644 index 0000000000..e13b23a960 --- /dev/null +++ b/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