Skip to content

Commit 205c51d

Browse files
committed
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.
1 parent fad06a0 commit 205c51d

File tree

2 files changed

+61
-2
lines changed

2 files changed

+61
-2
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,10 @@ private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEven
507507
Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource);
508508
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.PropertyChanged);
509509

510-
if (e.Property == TargetProperty)
511-
WriteValueToSource(e.NewValue);
510+
// The value must be read from the target object instead of using the value from the event
511+
// because the value may have changed again between the time the event was raised and now.
512+
if (e.Property == TargetProperty && TryGetTarget(out var target))
513+
WriteValueToSource(target.GetValue(TargetProperty));
512514
}
513515

514516
private object? ConvertFallback(object? fallback, string fallbackName)

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,31 @@ public void OneWayToSource_Binding_Does_Not_Override_TwoWay_Binding()
667667
Assert.Equal("TwoWay", source.Foo);
668668
}
669669

670+
[Fact]
671+
public void Target_Undoing_Property_Change_During_TwoWay_Binding_Does_Not_Cause_StackOverflow()
672+
{
673+
var source = new TestStackOverflowViewModel { BoolValue = true };
674+
var target = new TwoWayBindingTest();
675+
676+
source.ResetSetterInvokedCount();
677+
678+
// The AlwaysFalse property is set to false in the PropertyChanged callback. Ensure
679+
// that binding it to an initial `true` value with a two-way binding does not cause a
680+
// stack overflow.
681+
target.Bind(
682+
TwoWayBindingTest.AlwaysFalseProperty,
683+
new Binding(nameof(TestStackOverflowViewModel.BoolValue))
684+
{
685+
Mode = BindingMode.TwoWay,
686+
});
687+
688+
target.DataContext = source;
689+
690+
Assert.Equal(1, source.SetterInvokedCount);
691+
Assert.False(source.BoolValue);
692+
Assert.False(target.AlwaysFalse);
693+
}
694+
670695
private class StyledPropertyClass : AvaloniaObject
671696
{
672697
public static readonly StyledProperty<double> DoubleValueProperty =
@@ -725,10 +750,24 @@ private class TestStackOverflowViewModel : INotifyPropertyChanged
725750

726751
public const int MaxInvokedCount = 1000;
727752

753+
private bool _boolValue;
728754
private double _value;
729755

730756
public event PropertyChangedEventHandler PropertyChanged;
731757

758+
public bool BoolValue
759+
{
760+
get => _boolValue;
761+
set
762+
{
763+
if (_boolValue != value)
764+
{
765+
_boolValue = value;
766+
SetterInvokedCount++;
767+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BoolValue)));
768+
}
769+
} }
770+
732771
public double Value
733772
{
734773
get => _value;
@@ -754,20 +793,38 @@ public double Value
754793
}
755794
}
756795
}
796+
797+
public void ResetSetterInvokedCount() => SetterInvokedCount = 0;
757798
}
758799

759800
private class TwoWayBindingTest : Control
760801
{
802+
public static readonly StyledProperty<bool> AlwaysFalseProperty =
803+
AvaloniaProperty.Register<StyledPropertyClass, bool>(nameof(AlwaysFalse));
761804
public static readonly StyledProperty<string> TwoWayProperty =
762805
AvaloniaProperty.Register<TwoWayBindingTest, string>(
763806
"TwoWay",
764807
defaultBindingMode: BindingMode.TwoWay);
765808

809+
public bool AlwaysFalse
810+
{
811+
get => GetValue(AlwaysFalseProperty);
812+
set => SetValue(AlwaysFalseProperty, value);
813+
}
814+
766815
public string TwoWay
767816
{
768817
get => GetValue(TwoWayProperty);
769818
set => SetValue(TwoWayProperty, value);
770819
}
820+
821+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
822+
{
823+
base.OnPropertyChanged(change);
824+
825+
if (change.Property == AlwaysFalseProperty)
826+
SetCurrentValue(AlwaysFalseProperty, false);
827+
}
771828
}
772829

773830
public class Source : INotifyPropertyChanged

0 commit comments

Comments
 (0)