diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index cfb6338384..5319014901 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -502,6 +502,102 @@ namespace Avalonia.Base.UnitTests Assert.Equal(0, source.Subscriptions[0].Unsubscribe); } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Observable_Is_Unsubscribed_When_New_Binding_Of_Same_Priority_Is_Added(BindingPriority priority) + { + var scheduler = new TestScheduler(); + var source1 = scheduler.CreateColdObservable>(); + var source2 = scheduler.CreateColdObservable>(); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source1, priority); + Assert.Equal(1, source1.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source1.Subscriptions[0].Unsubscribe); + + target.Bind(Class1.FooProperty, source2, priority); + Assert.Equal(1, source2.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source2.Subscriptions[0].Unsubscribe); + Assert.Equal(1, source1.Subscriptions.Count); + Assert.Equal(0, source1.Subscriptions[0].Unsubscribe); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Observable_Is_Unsubscribed_When_New_Binding_Of_Higher_Priority_Is_Added(BindingPriority priority) + { + var scheduler = new TestScheduler(); + var source1 = scheduler.CreateColdObservable>(); + var source2 = scheduler.CreateColdObservable>(); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source1, priority); + Assert.Equal(1, source1.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source1.Subscriptions[0].Unsubscribe); + + target.Bind(Class1.FooProperty, source2, priority - 1); + Assert.Equal(1, source2.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source2.Subscriptions[0].Unsubscribe); + Assert.Equal(1, source1.Subscriptions.Count); + Assert.Equal(0, source1.Subscriptions[0].Unsubscribe); + } + + [Theory] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Observable_Is_Unsubscribed_When_New_Value_Of_Same_Priority_Is_Added(BindingPriority priority) + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable>(); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source, priority); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + + target.SetValue(Class1.FooProperty, "foo", priority); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(0, source.Subscriptions[0].Unsubscribe); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Observable_Is_Unsubscribed_When_New_Value_Of_Higher_Priority_Is_Added(BindingPriority priority) + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable>(); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source, priority); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + + target.SetValue(Class1.FooProperty, "foo", priority - 1); + Assert.Equal(0, source.Subscriptions.Count); + Assert.Equal(0, source.Subscriptions[0].Unsubscribe); + } + + [Fact] + public void LocalValue_Binding_Is_Not_Unsubscribed_When_LocalValue_Is_Set() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable>(); + var target = new Class1(); + + target.Bind(Class1.FooProperty, source); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + + target.SetValue(Class1.FooProperty, "foo"); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + } + [Fact] public void Two_Way_Separate_Binding_Works() { @@ -935,6 +1031,20 @@ namespace Avalonia.Base.UnitTests Assert.True(target.IsAnimating(Class1.FooProperty)); } + [Fact] + public void TwoWay_Binding_Should_Update_Source() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + + target.DoubleValue = 123.4; + + Assert.True(source.SetterCalled); + Assert.Equal(source.Value, 123.4); + } + [Fact] public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation() { @@ -975,6 +1085,20 @@ namespace Avalonia.Base.UnitTests target.Bind(TextBlock.TextProperty, new Binding("[0]", BindingMode.TwoWay)); } + [Fact] + public void TwoWay_LocalValue_Binding_Should_Not_Update_Source_When_Animated_Value_Set() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + target.SetValue(Class1.DoubleValueProperty, 123.4, BindingPriority.Animation); + + // Setter should not be called because the TwoWay binding with LocalValue priority + // should be overridden by the animated value and the binding made inactive. + Assert.False(source.SetterCalled); + } + [Fact] public void Disposing_Completed_Binding_Does_Not_Throw() { diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index 8a335d24dc..79d4817dfd 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs @@ -347,6 +347,54 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(Brushes.Green, data.Bar); } + [Fact] + public void Non_Active_Styled_Property_Binding_Should_Be_Unsubscribed() + { + var data = new Data { Bar = Brushes.Red }; + var control = new Border + { + DataContext = data, + }; + + var style1 = new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = new Binding("Bar"), + } + }, + }; + + var style2 = new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter + { + Property = Border.BackgroundProperty, + Value = Brushes.Green, + } + }, + }; + + Apply(style1, control); + Apply(style2, control); + + // `style1` is initially active. + Assert.Equal(Brushes.Red, control.Background); + Assert.Equal(1, data.PropertyChangedSubscriptionCount); + + // Activate `style2`. + control.Classes.Add("foo"); + Assert.Equal(Brushes.Green, control.Background); + + // The binding from `style1` is now inactive and so should be unsubscribed. + Assert.Equal(0, data.PropertyChangedSubscriptionCount); + } + [Fact] public void Non_Active_Styled_Property_Setter_With_TwoWay_Binding_Should_Not_Update_Source() { @@ -414,7 +462,7 @@ namespace Avalonia.Base.UnitTests.Styling Apply(style, control); } - private class Data + private class Data : NotifyingBase { public string? Foo { get; set; } public IBrush? Bar { get; set; }