Browse Source

fix(binding): reapply cached source values during UpdateTarget (#21756)

* Added failing test for #21746.

* fix(binding): reapply cached source values during UpdateTarget

UpdateTarget did not restore an uncommitted explicit two-way target value when the source still matched the binding expression's cached value. Equal-value suppression then prevented ValueStore from receiving the refresh.\n\nTrack the synchronous UpdateTarget rebuild and force publication only for that path, so the source value is reapplied without changing normal binding notification deduplication. The preceding regression test covers the observable behavior in both reflection and compiled binding paths.

---------

Co-authored-by: grokys <grokys@users.noreply.github.com>
pull/21812/head
Nathan Nguyen 2 months ago
committed by GitHub
parent
commit
c3a5c49699
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 20
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  2. 26
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.UpdateSourceTrigger.cs

20
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -31,6 +31,7 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
private readonly List<ExpressionNode> _nodes;
private readonly TargetTypeConverter? _targetTypeConverter;
private readonly UncommonFields? _uncommon;
private int _updateTargetDepth;
private bool _shouldUpdateOneTimeBindingTarget;
/// <summary>
@ -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;
}
}
/// <summary>
@ -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);
}
}

26
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<ViewModel, string?>(
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);
}
}
}

Loading…
Cancel
Save