diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 281e9f6b61..14c592b94c 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -31,6 +31,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I private readonly List _nodes; private readonly TargetTypeConverter? _targetTypeConverter; private readonly UncommonFields? _uncommon; + private int _updateTargetDepth; private bool _shouldUpdateOneTimeBindingTarget; /// @@ -167,10 +168,19 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I var source = _nodes[0].Source; - for (var i = 0; i < _nodes.Count; ++i) - _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null); + ++_updateTargetDepth; + + try + { + for (var i = 0; i < _nodes.Count; ++i) + _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null); - _nodes[0].SetSource(source, null); + _nodes[0].SetSource(source, null); + } + finally + { + --_updateTargetDepth; + } } /// @@ -206,7 +216,9 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I new BindingError(dataValidationError, BindingErrorType.DataValidationError) : null; - var forceUpdate = _mode == BindingMode.OneWay; + // UpdateTarget must reapply the source value even if this expression already + // has it cached: a two-way target may contain an uncommitted local value. + var forceUpdate = _mode == BindingMode.OneWay || _updateTargetDepth > 0; ConvertAndPublishValue(value, error, forceUpdate); } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.UpdateSourceTrigger.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.UpdateSourceTrigger.cs index 434a0c38ae..83d887e3a8 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.UpdateSourceTrigger.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.UpdateSourceTrigger.cs @@ -111,5 +111,31 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal("bar", target.String); Assert.Equal("bar", data.StringValue); } + + [Fact] + public void TwoWay_Explicit_Should_Update_Target_On_Call_To_UpdateTarget() + { + var data = new ViewModel { StringValue = "foo" }; + var (target, expression) = CreateTargetAndExpression( + o => o.StringValue, + mode: BindingMode.TwoWay, + source: data, + updateSourceTrigger: UpdateSourceTrigger.Explicit); + + Assert.Equal("foo", target.String); + Assert.Equal("foo", data.StringValue); + + target.String = "bar"; + + Assert.Equal("bar", target.String); + Assert.Equal("foo", data.StringValue); + + // UpdateTarget forces a transfer from source to target, discarding the + // value set on the target. + expression.UpdateTarget(); + + Assert.Equal("foo", target.String); + Assert.Equal("foo", data.StringValue); + } } }