diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index a232a06383..b57a9a0a9e 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -24,6 +24,9 @@ + diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ce40b3e517..c2e5c8e4f3 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -8,9 +8,13 @@ namespace Avalonia.Data.Core public abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); + protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); + protected static readonly WeakReference NullReference = + new WeakReference(null); + private WeakReference _target = UnsetReference; private Action _subscriber; private bool _listening; @@ -98,7 +102,7 @@ namespace Avalonia.Data.Core if (notification == null) { - LastValue = new WeakReference(value); + LastValue = value != null ? new WeakReference(value) : NullReference; if (Next != null) { @@ -111,7 +115,7 @@ namespace Avalonia.Data.Core } else { - LastValue = new WeakReference(notification.Value); + LastValue = notification.Value != null ? new WeakReference(notification.Value) : NullReference; if (Next != null) { @@ -136,8 +140,8 @@ namespace Avalonia.Data.Core } else if (target != AvaloniaProperty.UnsetValue) { - StartListeningCore(_target); _listening = true; + StartListeningCore(_target); } else { diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 4716b45340..cbceb58204 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -103,8 +103,8 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - SendCurrentValue(); SubscribeToChanges(); + SendCurrentValue(); } protected override void UnsubscribeCore() diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index eb98b9e8d6..d0a918dc88 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -29,6 +29,11 @@ namespace Avalonia.Data.Core if (!isLastValueAlive) { + if (value == null && LastValue == NullReference) + { + return true; + } + return false; } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 0ba06980af..7e053392c7 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -154,6 +154,18 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("bar", source.Foo); } + [Fact] + public void OneTime_Binding_Releases_Subscription_If_DataContext_Set_Later() + { + var target = new TextBlock(); + var source = new Source { Foo = "foo" }; + + target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime)); + target.DataContext = source; + + Assert.Equal(0, source.SubscriberCount); + } + [Fact] public void OneWayToSource_Binding_Should_Be_Set_Up() { @@ -196,6 +208,30 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", target.Text); } + [Fact] + public void OneWayToSource_Binding_Should_Not_StackOverflow_With_Null_Value() + { + // Issue #2912 + var target = new TextBlock { Text = null }; + var binding = new Binding + { + Path = "Foo", + Mode = BindingMode.OneWayToSource, + }; + + target.Bind(TextBox.TextProperty, binding); + + var source = new Source { Foo = "foo" }; + target.DataContext = source; + + Assert.Null(source.Foo); + + // When running tests under NCrunch, NCrunch replaces the standard StackOverflowException + // with its own, which will be caught by our code. Detect the stackoverflow anyway, by + // making sure the target property was only set once. + Assert.Equal(2, source.FooSetCount); + } + [Fact] public void Default_BindingMode_Should_Be_Used() { @@ -543,6 +579,23 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(expected, child.DoubleValue); } + [Fact] + public void Combined_OneTime_And_OneWayToSource_Bindings_Should_Release_Subscriptions() + { + var target1 = new TextBlock(); + var target2 = new TextBlock(); + var root = new Panel { Children = { target1, target2 } }; + var source = new Source { Foo = "foo" }; + + using (target1.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime))) + using (target2.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneWayToSource))) + { + root.DataContext = source; + } + + Assert.Equal(0, source.SubscriberCount); + } + private class StyledPropertyClass : AvaloniaObject { public static readonly StyledProperty DoubleValueProperty = @@ -622,6 +675,7 @@ namespace Avalonia.Markup.UnitTests.Data public class Source : INotifyPropertyChanged { + private PropertyChangedEventHandler _propertyChanged; private string _foo; public string Foo @@ -630,15 +684,25 @@ namespace Avalonia.Markup.UnitTests.Data set { _foo = value; + ++FooSetCount; RaisePropertyChanged(); } } - public event PropertyChangedEventHandler PropertyChanged; + public int FooSetCount { get; private set; } + + + public int SubscriberCount { get; private set; } + + public event PropertyChangedEventHandler PropertyChanged + { + add { _propertyChanged += value; ++SubscriberCount; } + remove { _propertyChanged += value; --SubscriberCount; } + } private void RaisePropertyChanged([CallerMemberName] string prop = "") { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); } }