diff --git a/src/Avalonia.Base/Data/BindingExpressionBase.cs b/src/Avalonia.Base/Data/BindingExpressionBase.cs
index ad411130cf..24bed9ddd5 100644
--- a/src/Avalonia.Base/Data/BindingExpressionBase.cs
+++ b/src/Avalonia.Base/Data/BindingExpressionBase.cs
@@ -14,8 +14,14 @@ public abstract class BindingExpressionBase : IDisposable, ISetterInstance
public virtual void Dispose()
{
+ GC.SuppressFinalize(this);
}
+ ///
+ /// Forces a data transfer from the binding source to the binding target.
+ ///
+ public virtual void UpdateTarget() { }
+
///
/// When overridden in a derived class, attaches the binding expression to a value store but
/// does not start it.
diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs
index 1203b3d0e3..f4fd076f90 100644
--- a/src/Avalonia.Base/Data/Core/BindingExpression.cs
+++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs
@@ -131,6 +131,19 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
public ExpressionNode LeafNode => _nodes[_nodes.Count - 1];
public string? StringFormat => _uncommon?._stringFormat;
+ public override void UpdateTarget()
+ {
+ if (_nodes.Count == 0)
+ return;
+
+ var source = _nodes[0].Source;
+
+ for (var i = 0; i < _nodes.Count; ++i)
+ _nodes[i].SetSource(null, null);
+
+ _nodes[0].SetSource(source, null);
+ }
+
///
/// Creates an from an expression tree.
///
diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs
index fc86213373..150502d67a 100644
--- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs
+++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs
@@ -127,6 +127,9 @@ internal abstract class ExpressionNode
if (source == AvaloniaProperty.UnsetValue)
source = null;
+ if (source == oldSource)
+ return;
+
if (oldSource is not null)
Unsubscribe(oldSource);
@@ -139,7 +142,7 @@ internal abstract class ExpressionNode
// binding change.
_value = BindingExpression._nullReference;
}
- else if (source != oldSource)
+ else
{
try { OnSourceChanged(source, dataValidationError); }
catch (Exception e) { SetError(e); }
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
index 094c3fc708..a7707c526d 100644
--- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Reactive.Linq;
using System.Threading.Tasks;
+using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
@@ -271,6 +272,22 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
+ [Fact]
+ public void Can_Use_UpdateTarget_To_Update_From_Non_INPC_Data()
+ {
+ var data = new NonInpcClass { StringValue = "foo" };
+ var target = new Control { DataContext = data };
+ var expression = target.Bind(Control.TagProperty, new Binding(nameof(data.StringValue)));
+
+ Assert.Equal("foo", target.Tag);
+
+ data.StringValue = "bar";
+ Assert.Equal("foo", target.Tag);
+
+ expression.UpdateTarget();
+ Assert.Equal("bar", target.Tag);
+ }
+
private class Class1 : NotifyingBase
{
private string _stringValue;
@@ -289,6 +306,11 @@ namespace Avalonia.Base.UnitTests.Data.Core
}
}
+ private class NonInpcClass
+ {
+ public string StringValue { get; set; }
+ }
+
private class TargetProperties : AvaloniaObject
{
public static readonly StyledProperty DoubleProperty =