Repository: AvaloniaUI/Avalonia

Issue:
Allow accessing BindingExpressions to make proper use of `UpdateSourceTrigger=Explicit` 
### Is your feature request related to a problem? Please describe.

I'm trying to use the new `UpdateSourceTrigger=Explicit` parameter for some bindings.

At the moment, I can't seem to find a way to access BindingExpressions for existing bindings defined in XAML, but I need that to call `BindingExpressionBase.UpdateSource`.

### Describe the solution you'd like

WPF has a public API to retrieve binding expressions:
```
control.GetBindingExpression(Control.TargetProperty)
```
Maybe something similar could be implemented.

### Describe alternatives you've considered

--

### Additional context

#13734 also requested this functionality, and although it was closed, there doesn't seem to be any public API for that particular use case yet.


Hints:
@ThePBone which version did you test? 11.1 beta may have that API now
I tested 11.1 beta-1.
 `UpdateSourceTrigger.Explicit` and `BindingExpressionBase.UpdateSource` are available as mentioned in PR #13970.
However, I don't think there is currently a way to obtain a reference to a BindingExpressionBase for an existing binding using the public API, so the `UpdateSource` method can't actually be accessed, as far as I can see.
@ThePBone I think you are right. Most likely an oversight 

/cc @grokys 
Any idea when this will be added? I find myself in need of being able to access the binding(s) of a control

Patch:
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);
         }
 
+        /// <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;
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<T>(StyledProperty<T> 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<T> CastEffectiveValue<T>(EffectiveValue value)
         {


Test patch:
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<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);
+    }
+}