Browse Source

Add BindingOperations.GetBindingExpressionBase. (#16214)

With basic unit tests.

Co-authored-by: Max Katz <[email protected]>
Steven Kirk 1 year ago
parent
commit
640a9dd854

+ 19 - 0
src/Avalonia.Base/Data/BindingOperations.cs

@@ -101,6 +101,25 @@ namespace Avalonia.Data
             return Apply(target, property, binding);
         }
 
+        /// <summary>
+        /// Retrieves the <see cref="BindingExpressionBase"/> that is currently active on the
+        /// specified property.
+        /// </summary>
+        /// <param name="target">
+        /// The <see cref="AvaloniaObject"/> from which to retrieve the binding expression.
+        /// </param>
+        /// <param name="property">
+        /// The binding target property from which to retrieve the binding expression.
+        /// </param>
+        /// <returns>
+        /// The <see cref="BindingExpressionBase"/> object that is active on the given property or
+        /// null if no binding expression is active on the given property.
+        /// </returns>
+        public static BindingExpressionBase? GetBindingExpressionBase(AvaloniaObject target, AvaloniaProperty property)
+        {
+            return target.GetValueStore().GetExpression(property);
+        }
+
         private sealed class TwoWayBindingDisposable : IDisposable
         {
             private readonly IDisposable _toTargetSubscription;

+ 36 - 0
src/Avalonia.Base/PropertyStore/ValueStore.cs

@@ -292,6 +292,42 @@ namespace Avalonia.PropertyStore
             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<T> CastEffectiveValue<T>(EffectiveValue value)
         {

+ 147 - 0
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<object, string>(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<Control>())
+        {
+            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);
+    }
+}