diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index ddc3d8d081..b0ff591682 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -478,7 +478,13 @@ namespace Avalonia } } - void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { } + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) + { + ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default); + } /// /// Called for each inherited property when the changes. diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index 09a0f169df..3249b31d66 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -48,10 +48,10 @@ namespace Avalonia.PropertyStore { _subscription?.Dispose(); _subscription = null; - _sink.Completed(Property, this); + _sink.Completed(Property, this, Value); } - public void OnCompleted() => _sink.Completed(Property, this); + public void OnCompleted() => _sink.Completed(Property, this, Value); public void OnError(Exception error) { diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs index 223b0058c1..9012a985ac 100644 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ b/src/Avalonia.Base/PropertyStore/IValueSink.cs @@ -15,6 +15,9 @@ namespace Avalonia.PropertyStore Optional oldValue, BindingValue newValue); - void Completed(AvaloniaProperty property, IPriorityValueEntry entry); + void Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue); } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 2785dc6840..4ef8f650fa 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -117,7 +117,10 @@ namespace Avalonia.PropertyStore UpdateEffectiveValue(); } - void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) { _entries.Remove((IPriorityValueEntry)entry); UpdateEffectiveValue(); diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 58ebc48652..22cded565a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -148,7 +148,7 @@ namespace Avalonia _values.Remove(property); _sink.ValueChanged( property, - BindingPriority.LocalValue, + BindingPriority.Unset, old, BindingValue.Unset); } @@ -190,13 +190,17 @@ namespace Avalonia _sink.ValueChanged(property, priority, oldValue, newValue); } - void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) + void IValueSink.Completed( + StyledPropertyBase property, + IPriorityValueEntry entry, + Optional oldValue) { if (_values.TryGetValue(property, out var slot)) { if (slot == entry) { _values.Remove(property); + _sink.Completed(property, entry, oldValue); } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 437b24dd9e..a438d7380b 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -347,8 +347,7 @@ namespace Avalonia.Controls !AcceptsReturn && Text?.Length > 0) { - SelectionStart = 0; - SelectionEnd = Text.Length; + SelectAll(); } else { @@ -704,8 +703,7 @@ namespace Avalonia.Controls SelectionEnd = StringUtils.NextWord(text, index); break; case 3: - SelectionStart = 0; - SelectionEnd = text.Length; + SelectAll(); break; } } @@ -927,7 +925,10 @@ namespace Avalonia.Controls CaretIndex = caretIndex; } - private void SelectAll() + /// + /// Select all text in the TextBox + /// + public void SelectAll() { SelectionStart = 0; SelectionEnd = Text?.Length ?? 0; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 0c37858223..6554237b3a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -529,7 +529,7 @@ namespace Avalonia.Controls { var sizeToContent = SizeToContent; var clientSize = ClientSize; - Size constraint = clientSize; + var constraint = availableSize; if ((sizeToContent & SizeToContent.Width) != 0) { diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 5768a25435..120a53c664 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -573,9 +573,12 @@ namespace Avalonia element._dataContextUpdating = true; element.OnDataContextBeginUpdate(); - foreach (var child in element.LogicalChildren) + var logicalChildren = element.LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - if (child is StyledElement s && + if (element.LogicalChildren[i] is StyledElement s && s.InheritanceParent == element && !s.IsSet(DataContextProperty)) { @@ -646,9 +649,15 @@ namespace Avalonia AttachedToLogicalTree?.Invoke(this, e); } - foreach (var child in LogicalChildren.OfType()) + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - child.OnAttachedToLogicalTreeCore(e); + if (logicalChildren[i] is StyledElement child) + { + child.OnAttachedToLogicalTreeCore(e); + } } } @@ -661,9 +670,15 @@ namespace Avalonia OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); - foreach (var child in LogicalChildren.OfType()) + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) { - child.OnDetachedFromLogicalTreeCore(e); + if (logicalChildren[i] is StyledElement child) + { + child.OnDetachedFromLogicalTreeCore(e); + } } #if DEBUG diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 9244232ff5..b880ecb01c 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -78,8 +78,6 @@ namespace Avalonia.Styling { Contract.Requires(control != null); - var description = style?.ToString(); - if (Property == null) { throw new InvalidOperationException("Setter.Property must be set."); @@ -107,6 +105,8 @@ namespace Avalonia.Styling } else { + var description = style?.ToString(); + var activated = new ActivatedValue(activator, value, description); return control.Bind(Property, activated, BindingPriority.StyleTrigger); } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 99ee8d8563..22db7adfe4 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -116,13 +116,21 @@ namespace Avalonia.Styling if (match.IsMatch) { var controlSubscriptions = GetSubscriptions(control); - - var subs = new CompositeDisposable(Setters.Count + Animations.Count); - if (control is Animatable animatable) + var animatable = control as Animatable; + + var setters = Setters; + var settersCount = setters.Count; + var animations = Animations; + var animationsCount = animations.Count; + + var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1); + + if (animatable != null) { - foreach (var animation in Animations) + for (var i = 0; i < animationsCount; i++) { + var animation = animations[i]; var obsMatch = match.Activator; if (match.Result == SelectorMatchResult.AlwaysThisType || @@ -133,17 +141,19 @@ namespace Avalonia.Styling var sub = animation.Apply(animatable, null, obsMatch); subs.Add(sub); - } + } } - foreach (var setter in Setters) + for (var i = 0; i < settersCount; i++) { + var setter = setters[i]; var sub = setter.Apply(this, control, match.Activator); subs.Add(sub); } + subs.Add(Disposable.Create((subs, Subscriptions) , state => state.Subscriptions.Remove(state.subs))); + controlSubscriptions.Add(subs); - controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); } @@ -151,18 +161,23 @@ namespace Avalonia.Styling } else if (control == container) { + var setters = Setters; + var settersCount = setters.Count; + var controlSubscriptions = GetSubscriptions(control); - var subs = new CompositeDisposable(Setters.Count); + var subs = new CompositeDisposable(settersCount + 1); - foreach (var setter in Setters) + for (var i = 0; i < settersCount; i++) { + var setter = setters[i]; var sub = setter.Apply(this, control, null); subs.Add(sub); } + subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs))); + controlSubscriptions.Add(subs); - controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); return true; } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 0226288998..fd38c39650 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -239,8 +239,10 @@ namespace Avalonia.Styling /// public bool Remove(IStyle item) => _styles.Remove(item); + public AvaloniaList.Enumerator GetEnumerator() => _styles.GetEnumerator(); + /// - public IEnumerator GetEnumerator() => _styles.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index ef7a254f86..bd2ab6e614 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -386,11 +386,18 @@ namespace Avalonia AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - if (VisualChildren != null) + var visualChildren = VisualChildren; + + if (visualChildren != null) { - foreach (Visual child in VisualChildren.OfType()) + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) { - child.OnAttachedToVisualTreeCore(e); + if (visualChildren[i] is Visual child) + { + child.OnAttachedToVisualTreeCore(e); + } } } } @@ -415,11 +422,18 @@ namespace Avalonia DetachedFromVisualTree?.Invoke(this, e); e.Root?.Renderer?.AddDirty(this); - if (VisualChildren != null) + var visualChildren = VisualChildren; + + if (visualChildren != null) { - foreach (Visual child in VisualChildren.OfType()) + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) { - child.OnDetachedFromVisualTreeCore(e); + if (visualChildren[i] is Visual child) + { + child.OnDetachedFromVisualTreeCore(e); + } } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 4c00d2a1ea..2839fde320 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -132,6 +132,111 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foo", target.GetValue(property)); } + [Fact] + public void Completing_LocalValue_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + target.Bind(property, source); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Unset, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("foodefault", e.NewValue as string); + ++raised; + }; + + source.OnCompleted(); + + Assert.Equal("foodefault", target.GetValue(property)); + Assert.Equal(1, raised); + } + + [Fact] + public void Completing_Style_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + target.Bind(property, source, BindingPriority.Style); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Unset, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("foodefault", e.NewValue as string); + ++raised; + }; + + source.OnCompleted(); + + Assert.Equal("foodefault", target.GetValue(property)); + Assert.Equal(1, raised); + } + + [Fact] + public void Completing_LocalValue_Binding_With_Style_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + target.Bind(property, new BehaviorSubject("bar"), BindingPriority.Style); + target.Bind(property, source); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Style, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("bar", e.NewValue as string); + ++raised; + }; + + source.OnCompleted(); + + Assert.Equal("bar", target.GetValue(property)); + Assert.Equal(1, raised); + } + + [Fact] + public void Disposing_LocalValue_Binding_Raises_PropertyChanged() + { + var target = new Class1(); + var source = new BehaviorSubject>("foo"); + var property = Class1.FooProperty; + var raised = 0; + + var sub = target.Bind(property, source); + Assert.Equal("foo", target.GetValue(property)); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(BindingPriority.Unset, e.Priority); + Assert.Equal(property, e.Property); + Assert.Equal("foo", e.OldValue as string); + Assert.Equal("foodefault", e.NewValue as string); + ++raised; + }; + + sub.Dispose(); + + Assert.Equal("foodefault", target.GetValue(property)); + Assert.Equal(1, raised); + } + [Fact] public void Setting_Style_Value_Overrides_Binding_Permanently() { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index b1a5b5ae92..ca17afb94f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -188,6 +188,7 @@ namespace Avalonia.Base.UnitTests target.PropertyChanged += (s, e) => { Assert.Same(target, s); + Assert.Equal(BindingPriority.LocalValue, e.Priority); Assert.Equal(Class1.FooProperty, e.Property); Assert.Equal("newvalue", (string)e.OldValue); Assert.Equal("unset", (string)e.NewValue); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 40631d04cf..98305cc110 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -30,6 +30,7 @@ namespace Avalonia.Base.UnitTests target.PropertyChanged += (s, e) => { Assert.Same(target, s); + Assert.Equal(BindingPriority.Unset, e.Priority); Assert.Equal(Class1.FooProperty, e.Property); Assert.Equal("newvalue", (string)e.OldValue); Assert.Equal("foodefault", (string)e.NewValue); diff --git a/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs new file mode 100644 index 0000000000..7bccd65c81 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Styling +{ + [MemoryDiagnoser] + public class StyleAttachBenchmark : IDisposable + { + private readonly IDisposable _app; + private readonly TestRoot _root; + private readonly TextBox _control; + + public StyleAttachBenchmark() + { + _app = UnitTestApplication.Start( + TestServices.StyledWindow.With( + renderInterface: new NullRenderingPlatform(), + threadingInterface: new NullThreadingPlatform())); + + _root = new TestRoot(true, null) + { + Renderer = new NullRenderer(), + }; + + _control = new TextBox(); + } + + [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] + public void AttachTextBoxStyles() + { + var styles = UnitTestApplication.Current.Styles; + + styles.Attach(_control, UnitTestApplication.Current); + + styles.Detach(); + } + + public void Dispose() + { + _app.Dispose(); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 0508edd92f..81699b075c 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -341,11 +341,62 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.Manual, + Content = child + }; + + target.Show(); + + Assert.Equal(new Size(100, 50), child.MeasureSize); + } + } + + [Fact] + public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + target.Show(); + + Assert.Equal(Size.Infinity, child.MeasureSize); + } + } + private IWindowImpl CreateImpl(Mock renderer) { return Mock.Of(x => x.Scaling == 1 && x.CreateRenderer(It.IsAny()) == renderer.Object); } + + private class ChildControl : Control + { + public Size MeasureSize { get; private set; } + + protected override Size MeasureOverride(Size availableSize) + { + MeasureSize = availableSize; + return base.MeasureOverride(availableSize); + } + } } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 389b3c8df8..3b288dbf66 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -8,8 +8,10 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Diagnostics; using Avalonia.Layout; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using JetBrains.dotMemoryUnit; @@ -370,6 +372,56 @@ namespace Avalonia.LeakTests } } + [Fact] + public void Control_With_Style_RenderTransform_Is_Freed() + { + // # Issue #3545 + using (Start()) + { + Func run = () => + { + var window = new Window + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter + { + Property = Visual.RenderTransformProperty, + Value = new RotateTransform(45), + } + } + } + }, + Content = new Canvas() + }; + + window.Show(); + + // Do a layout and make sure that Canvas gets added to visual tree with + // its render transform. + window.LayoutManager.ExecuteInitialLayoutPass(window); + var canvas = Assert.IsType(window.Presenter.Child); + Assert.IsType(canvas.RenderTransform); + + // Clear the content and ensure the Canvas is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } + private IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow);