Browse Source

Prevent stack overflow in two-way bindings. (#17073)

* Add failing test for #16746

* Always read the value from the target object.

The value must be read from the target object instead of using the value from the event because the value may have changed again between the time the event was raised and now: if that occurs in a two-way binding then we end up with a stack overflow.
release/11.1.4
Steven Kirk 1 year ago
parent
commit
205c51dc40
  1. 6
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  2. 57
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

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

@ -507,8 +507,10 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri
Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource); Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource);
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.PropertyChanged); Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.PropertyChanged);
if (e.Property == TargetProperty) // The value must be read from the target object instead of using the value from the event
WriteValueToSource(e.NewValue); // because the value may have changed again between the time the event was raised and now.
if (e.Property == TargetProperty && TryGetTarget(out var target))
WriteValueToSource(target.GetValue(TargetProperty));
} }
private object? ConvertFallback(object? fallback, string fallbackName) private object? ConvertFallback(object? fallback, string fallbackName)

57
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -667,6 +667,31 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("TwoWay", source.Foo); Assert.Equal("TwoWay", source.Foo);
} }
[Fact]
public void Target_Undoing_Property_Change_During_TwoWay_Binding_Does_Not_Cause_StackOverflow()
{
var source = new TestStackOverflowViewModel { BoolValue = true };
var target = new TwoWayBindingTest();
source.ResetSetterInvokedCount();
// The AlwaysFalse property is set to false in the PropertyChanged callback. Ensure
// that binding it to an initial `true` value with a two-way binding does not cause a
// stack overflow.
target.Bind(
TwoWayBindingTest.AlwaysFalseProperty,
new Binding(nameof(TestStackOverflowViewModel.BoolValue))
{
Mode = BindingMode.TwoWay,
});
target.DataContext = source;
Assert.Equal(1, source.SetterInvokedCount);
Assert.False(source.BoolValue);
Assert.False(target.AlwaysFalse);
}
private class StyledPropertyClass : AvaloniaObject private class StyledPropertyClass : AvaloniaObject
{ {
public static readonly StyledProperty<double> DoubleValueProperty = public static readonly StyledProperty<double> DoubleValueProperty =
@ -725,10 +750,24 @@ namespace Avalonia.Markup.UnitTests.Data
public const int MaxInvokedCount = 1000; public const int MaxInvokedCount = 1000;
private bool _boolValue;
private double _value; private double _value;
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
public bool BoolValue
{
get => _boolValue;
set
{
if (_boolValue != value)
{
_boolValue = value;
SetterInvokedCount++;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BoolValue)));
}
} }
public double Value public double Value
{ {
get => _value; get => _value;
@ -754,20 +793,38 @@ namespace Avalonia.Markup.UnitTests.Data
} }
} }
} }
public void ResetSetterInvokedCount() => SetterInvokedCount = 0;
} }
private class TwoWayBindingTest : Control private class TwoWayBindingTest : Control
{ {
public static readonly StyledProperty<bool> AlwaysFalseProperty =
AvaloniaProperty.Register<StyledPropertyClass, bool>(nameof(AlwaysFalse));
public static readonly StyledProperty<string> TwoWayProperty = public static readonly StyledProperty<string> TwoWayProperty =
AvaloniaProperty.Register<TwoWayBindingTest, string>( AvaloniaProperty.Register<TwoWayBindingTest, string>(
"TwoWay", "TwoWay",
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay);
public bool AlwaysFalse
{
get => GetValue(AlwaysFalseProperty);
set => SetValue(AlwaysFalseProperty, value);
}
public string TwoWay public string TwoWay
{ {
get => GetValue(TwoWayProperty); get => GetValue(TwoWayProperty);
set => SetValue(TwoWayProperty, value); set => SetValue(TwoWayProperty, value);
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == AlwaysFalseProperty)
SetCurrentValue(AlwaysFalseProperty, false);
}
} }
public class Source : INotifyPropertyChanged public class Source : INotifyPropertyChanged

Loading…
Cancel
Save