From 640a9dd854d0a48e3a7dfc8d93ac4331b713445f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Jul 2024 12:03:17 +0200 Subject: [PATCH] Add BindingOperations.GetBindingExpressionBase. (#16214) With basic unit tests. Co-authored-by: Max Katz --- src/Avalonia.Base/Data/BindingOperations.cs | 19 +++ src/Avalonia.Base/PropertyStore/ValueStore.cs | 36 +++++ .../Data/BindingOperationsTests.cs | 147 ++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 tests/Avalonia.Base.UnitTests/Data/BindingOperationsTests.cs diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 9170fbfaa06..b0a97e5c000 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -101,6 +101,25 @@ public static IDisposable Apply( return Apply(target, property, binding); } + /// + /// Retrieves the that is currently active on the + /// specified property. + /// + /// + /// The from which to retrieve the binding expression. + /// + /// + /// The binding target property from which to retrieve the binding expression. + /// + /// + /// The object that is active on the given property or + /// null if no binding expression is active on the given property. + /// + public static BindingExpressionBase? GetBindingExpressionBase(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetValueStore().GetExpression(property); + } + private sealed class TwoWayBindingDisposable : IDisposable { private readonly IDisposable _toTargetSubscription; diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 67d176eceac..789383b860a 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -292,6 +292,42 @@ public T GetValue(StyledProperty property) return property.GetDefaultValue(Owner); } + public BindingExpressionBase? GetExpression(AvaloniaProperty property) + { + var evaluatedLocalValue = false; + + bool TryGetLocalValue(out BindingExpressionBase? result) + { + if (!evaluatedLocalValue) + { + evaluatedLocalValue = true; + + if (_localValueBindings?.TryGetValue(property.Id, out var o) == true) + { + result = o as BindingExpressionBase; + return true; + } + } + + result = null; + return false; + } + + for (var i = _frames.Count - 1; i >= 0; --i) + { + var frame = _frames[i]; + + if (frame.Priority > BindingPriority.LocalValue && TryGetLocalValue(out var localExpression)) + return localExpression; + + if (frame.TryGetEntryIfActive(property, out var entry, out _)) + return entry as BindingExpressionBase; + } + + TryGetLocalValue(out var e); + return e; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static EffectiveValue CastEffectiveValue(EffectiveValue value) { diff --git a/tests/Avalonia.Base.UnitTests/Data/BindingOperationsTests.cs b/tests/Avalonia.Base.UnitTests/Data/BindingOperationsTests.cs new file mode 100644 index 00000000000..ac39e2b56d3 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Data/BindingOperationsTests.cs @@ -0,0 +1,147 @@ +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Base.UnitTests.Data; + +public class BindingOperationsTests +{ + [Fact] + public void GetBindingExpressionBase_Returns_Null_When_Not_Bound() + { + var target = new Control(); + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.Null(expression); + } + + [Theory] + [InlineData(BindingPriority.Animation)] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.StyleTrigger)] + public void GetBindingExpressionBase_Returns_Expression_When_Bound(BindingPriority priority) + { + var data = new { Tag = "foo" }; + var target = new Control { DataContext = data }; + var binding = new Binding("Tag") { Priority = priority }; + target.Bind(Control.TagProperty, binding); + + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.NotNull(expression); + } + + [Fact] + public void GetBindingExpressionBase_Returns_Expression_When_Bound_Locally_With_Binding_Error() + { + // Target has no data context so binding will fail. + var target = new Control(); + var binding = new Binding("Tag"); + target.Bind(Control.TagProperty, binding); + + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.NotNull(expression); + } + + [Fact] + public void GetBindingExpressionBase_Returns_Expression_When_Bound_To_MultiBinding() + { + var data = new { Tag = "foo" }; + var target = new Control { DataContext = data }; + var binding = new MultiBinding + { + Converter = new FuncMultiValueConverter(x => string.Join(',', x)), + Bindings = + { + new Binding("Tag"), + new Binding("Tag"), + } + }; + + target.Bind(Control.TagProperty, binding); + + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.NotNull(expression); + } + + [Fact] + public void GetBindingExpressionBase_Returns_Binding_When_Bound_Via_ControlTheme() + { + var target = new Control(); + var binding = new Binding("Tag"); + var theme = new ControlTheme(typeof(Control)) + { + Setters = { new Setter(Control.TagProperty, binding) }, + }; + + target.Theme = theme; + var root = new TestRoot(target); + root.UpdateLayout(); + + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.NotNull(expression); + } + + [Fact] + public void GetBindingExpressionBase_Returns_Binding_When_Bound_Via_ControlTheme_TemplateBinding() + { + var target = new Control(); + var binding = new TemplateBinding(Control.TagProperty); + var theme = new ControlTheme(typeof(Control)) + { + Setters = { new Setter(Control.TagProperty, binding) }, + }; + + target.Theme = theme; + var root = new TestRoot(target); + root.UpdateLayout(); + + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.NotNull(expression); + } + + [Fact] + public void GetBindingExpressionBase_Returns_Binding_When_Bound_Via_ControlTheme_Style() + { + var target = new Control { Classes = { "foo" } }; + var binding = new Binding("Tag"); + var theme = new ControlTheme(typeof(Control)) + { + Children = + { + new Style(x => x.Nesting().Class("foo")) + { + Setters = { new Setter(Control.TagProperty, binding) }, + }, + } + }; + + target.Theme = theme; + var root = new TestRoot(target); + root.UpdateLayout(); + + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.NotNull(expression); + } + + [Fact] + public void GetBindingExpressionBase_Returns_Binding_When_Bound_Via_Style() + { + var target = new Control(); + var binding = new Binding("Tag"); + var style = new Style(x => x.OfType()) + { + Setters = { new Setter(Control.TagProperty, binding) }, + }; + + var root = new TestRoot(); + root.Styles.Add(style); + root.Child = target; + root.UpdateLayout(); + + var expression = BindingOperations.GetBindingExpressionBase(target, Control.TagProperty); + Assert.NotNull(expression); + } +}