From e9fa5c48859801631f28b32ab5e2d79ab18d7e23 Mon Sep 17 00:00:00 2001 From: Martim Claudino Date: Wed, 15 Apr 2026 10:11:11 +0100 Subject: [PATCH] Fix #17674: OneWay binding fails to update target after local change (#20964) * Fix #20816: OneWay binding fails to update target after local change When a OneWay binding target is modified locally (e.g., via user interaction on a ToggleButton), subsequent PropertyChanged events from the source were being ignored. This occurred because the binding expression incorrectly suppressed the update. This patch modifies UntypedBindingExpressionBase to ensure that OneWay bindings always propagate changes from the source to the target, overriding any previous local changes. Unit tests were also added to prevent future regressions on this behavior. Signed-off-by: Martim Claudino * fix: scope forceUpdate to OneWay mode, use SetCurrentValue in tests and clean usings Signed-off-by: Martim Claudino --------- Signed-off-by: Martim Claudino --- .../Data/Core/BindingExpression.cs | 8 ++++--- .../Data/Core/UntypedBindingExpressionBase.cs | 5 +++-- .../Data/Core/BindingExpressionTests.Mode.cs | 21 +++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 81a21955c4..281e9f6b61 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -205,7 +205,9 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I var error = dataValidationError is not null ? new BindingError(dataValidationError, BindingErrorType.DataValidationError) : null; - ConvertAndPublishValue(value, error); + + var forceUpdate = _mode == BindingMode.OneWay; + ConvertAndPublishValue(value, error, forceUpdate); } } else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null) @@ -402,7 +404,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I error); } - private void ConvertAndPublishValue(object? value, BindingError? error) + private void ConvertAndPublishValue(object? value, BindingError? error, bool forceUpdate = false) { var isTargetNullValue = false; @@ -450,7 +452,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I value = ConvertFallback(FallbackValue, nameof(FallbackValue)); // Publish the value. - PublishValue(value, error); + PublishValue(value, error, forceUpdate); } private void WriteTargetValueToSource() diff --git a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs index e45c88c457..874c3c2e13 100644 --- a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs +++ b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs @@ -406,7 +406,8 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, /// /// The new value, or . /// The new binding or data validation error. - private protected void PublishValue(object? value, BindingError? error = null) + /// If true, forces the value to be published even if it hasn't changed. + private protected void PublishValue(object? value, BindingError? error = null, bool forceUpdate = false) { Debug.Assert(value is not BindingNotification); Debug.Assert(value != BindingOperations.DoNothing); @@ -424,7 +425,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, value = null; } - var hasValueChanged = value != UnchangedValue && !TypeUtilities.IdentityEquals(value, GetValue(), TargetType); + var hasValueChanged = forceUpdate || (value != UnchangedValue && !TypeUtilities.IdentityEquals(value, GetValue(), TargetType)); var hasErrorChanged = error is not null || _error is not null; if (hasValueChanged) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs index cf80b86386..c933eb5316 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.Mode.cs @@ -1,3 +1,5 @@ +using System.Reflection; +using System.Runtime.InteropServices; using Avalonia.Data; using Xunit; @@ -201,4 +203,23 @@ public partial class BindingExpressionTests Assert.Equal("foo", data.StringValue); } + + [Fact] + public void OneWay_Binding_Updates_Target_When_Changes_And_Source_Raises_PropertyChanged() + { + var data = new ViewModel { StringValue = "foo" }; + var target = CreateTarget( + x => x.StringValue, + dataContext: data, + mode: BindingMode.OneWay); + + Assert.Equal("foo", target.String); + + target.SetCurrentValue(TargetClass.StringProperty, "bar"); + Assert.Equal("bar", target.String); + + data.RaisePropertyChanged(nameof(data.StringValue)); + + Assert.Equal("foo", target.String); + } }