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..e9118af9f1 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); } } } @@ -228,6 +232,7 @@ namespace Avalonia else { var priorityValue = new PriorityValue(_owner, property, this, l); + priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index b561a3423f..a438d7380b 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -14,6 +14,7 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Data; +using Avalonia.Layout; using Avalonia.Utilities; namespace Avalonia.Controls @@ -72,6 +73,18 @@ namespace Avalonia.Controls public static readonly StyledProperty TextAlignmentProperty = TextBlock.TextAlignmentProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); + public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); @@ -262,6 +275,24 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets the horizontal alignment of the content within the control. + /// + public HorizontalAlignment HorizontalContentAlignment + { + get { return GetValue(HorizontalContentAlignmentProperty); } + set { SetValue(HorizontalContentAlignmentProperty, value); } + } + + /// + /// Gets or sets the vertical alignment of the content within the control. + /// + public VerticalAlignment VerticalContentAlignment + { + get { return GetValue(VerticalContentAlignmentProperty); } + set { SetValue(VerticalContentAlignmentProperty, value); } + } + public TextAlignment TextAlignment { get { return GetValue(TextAlignmentProperty); } @@ -316,8 +347,7 @@ namespace Avalonia.Controls !AcceptsReturn && Text?.Length > 0) { - SelectionStart = 0; - SelectionEnd = Text.Length; + SelectAll(); } else { @@ -673,8 +703,7 @@ namespace Avalonia.Controls SelectionEnd = StringUtils.NextWord(text, index); break; case 3: - SelectionStart = 0; - SelectionEnd = text.Length; + SelectAll(); break; } } @@ -896,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.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index 9247fb48a9..aa5e0dd8ec 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -63,7 +63,7 @@ namespace Avalonia.Input event EventHandler PointerReleased; /// - /// Occurs when the mouse wheen is scrolled over the control. + /// Occurs when the mouse wheel is scrolled over the control. /// event EventHandler PointerWheelChanged; diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 64cea1c430..331f083e8b 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -162,9 +162,12 @@ namespace Avalonia.Native void IAvnWindowBaseEvents.Resized(AvnSize size) { - var s = new Size(size.Width, size.Height); - _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s); + if (_parent._native != null) + { + var s = new Size(size.Width, size.Height); + _parent._savedLogicalSize = s; + _parent.Resized?.Invoke(s); + } } void IAvnWindowBaseEvents.PositionChanged(AvnPoint position) @@ -317,7 +320,7 @@ namespace Avalonia.Native _native.SetTopMost(value); } - public double Scaling => _native.GetScaling(); + public double Scaling => _native?.GetScaling() ?? 1; public Action Deactivated { get; set; } public Action Activated { get; set; } 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.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 423d19da14..2c4cafde26 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -12,7 +12,9 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - + ()) + 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..4b477287e8 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); @@ -239,6 +240,17 @@ namespace Avalonia.Base.UnitTests Assert.Equal("two", target.GetValue(Class1.FooProperty)); } + [Fact] + public void SetValue_Animation_Overrides_LocalValue() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "one", BindingPriority.LocalValue); + Assert.Equal("one", target.GetValue(Class1.FooProperty)); + target.SetValue(Class1.FooProperty, "two", BindingPriority.Animation); + Assert.Equal("two", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Setting_UnsetValue_Reverts_To_Default_Value() { 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);