diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index d19accb0ad..0ba06980af 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -60,6 +60,80 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", source.Foo); } + [Fact] + public void TwoWay_Binding_Should_Be_Set_Up_GC_Collect() + { + var source = new WeakRefSource { Foo = null }; + var target = new TestControl { DataContext = source }; + + var binding = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + }; + + target.Bind(TestControl.ValueProperty, binding); + + var ref1 = AssignValue(target, "ref1"); + + Assert.Equal(ref1.Target, source.Foo); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + var ref2 = AssignValue(target, "ref2"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + target.Value = null; + + Assert.Null(source.Foo); + } + + private class DummyObject : ICloneable + { + private readonly string _val; + + public DummyObject(string val) + { + _val = val; + } + + public object Clone() + { + return new DummyObject(_val); + } + + protected bool Equals(DummyObject other) + { + return string.Equals(_val, other._val); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DummyObject) obj); + } + + public override int GetHashCode() + { + return (_val != null ? _val.GetHashCode() : 0); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private WeakReference AssignValue(TestControl source, string val) + { + var obj = new DummyObject(val); + + source.Value = obj; + + return new WeakReference(obj); + } + [Fact] public void OneTime_Binding_Should_Be_Set_Up() { @@ -568,12 +642,70 @@ namespace Avalonia.Markup.UnitTests.Data } } + public class WeakRefSource : INotifyPropertyChanged + { + private WeakReference _foo; + + public object Foo + { + get + { + if (_foo == null) + { + return null; + } + + if (_foo.TryGetTarget(out object target)) + { + if (target is ICloneable cloneable) + { + return cloneable.Clone(); + } + + return target; + } + + return null; + } + set + { + _foo = new WeakReference(value); + + RaisePropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged([CallerMemberName] string prop = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); + } + } + private class OldDataContextViewModel { public int Foo { get; set; } = 1; public int Bar { get; set; } = 2; } + private class TestControl : Control + { + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(Value), + o => o.Value, + (o, v) => o.Value = v); + + private object _value; + + public object Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + } + private class OldDataContextTest : Control { public static readonly StyledProperty FooProperty =