From b3c0035b0b8b092ea399b25caa7db000149135cc Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 18 May 2020 14:01:38 +0200 Subject: [PATCH 1/2] Add failing leak test for Path control. --- tests/Avalonia.LeakTests/ControlTests.cs | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 0afb2465ee..9bb9fd7145 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.Remoting.Contexts; using Avalonia.Controls; +using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Diagnostics; using Avalonia.Input; @@ -492,6 +493,45 @@ namespace Avalonia.LeakTests } } + [Fact] + public void Path_Is_Freed() + { + using (Start()) + { + var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; + + Func run = () => + { + var window = new Window + { + Content = new Path + { + Data = geometry + } + }; + + window.Show(); + + window.LayoutManager.ExecuteInitialLayoutPass(window); + Assert.IsType(window.Presenter.Child); + + 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)); + + // We are keeping geometry alive to simulate a resource that outlives the control. + GC.KeepAlive(geometry); + } + } + private IDisposable Start() { return UnitTestApplication.Start(TestServices.StyledWindow.With( From 93320e4759f96edc1510f40181c71a65c197d18f Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 18 May 2020 14:03:31 +0200 Subject: [PATCH 2/2] Only subscribe to Path.Data when Path is attached to the visual tree. --- src/Avalonia.Controls/Shapes/Path.cs | 34 +++++++++++++++++-- .../Shapes/PathTests.cs | 4 +++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs index 3fd84c0c7b..d0ffc27d20 100644 --- a/src/Avalonia.Controls/Shapes/Path.cs +++ b/src/Avalonia.Controls/Shapes/Path.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Data; using Avalonia.Media; namespace Avalonia.Controls.Shapes @@ -9,6 +8,8 @@ namespace Avalonia.Controls.Shapes public static readonly StyledProperty DataProperty = AvaloniaProperty.Register(nameof(Data)); + private EventHandler _geometryChangedHandler; + static Path() { AffectsGeometry(DataProperty); @@ -21,21 +22,48 @@ namespace Avalonia.Controls.Shapes set { SetValue(DataProperty, value); } } + private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= GeometryChanged; + protected override Geometry CreateDefiningGeometry() => Data; + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (Data is object) + { + Data.Changed += GeometryChangedHandler; + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + if (Data is object) + { + Data.Changed -= GeometryChangedHandler; + } + } + private void DataChanged(AvaloniaPropertyChangedEventArgs e) { + if (VisualRoot is null) + { + return; + } + var oldGeometry = (Geometry)e.OldValue; var newGeometry = (Geometry)e.NewValue; if (oldGeometry is object) { - oldGeometry.Changed -= GeometryChanged; + oldGeometry.Changed -= GeometryChangedHandler; } if (newGeometry is object) { - newGeometry.Changed += GeometryChanged; + newGeometry.Changed += GeometryChangedHandler; } } diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs index 5a9ca410e4..88c64e76cc 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -23,12 +23,16 @@ namespace Avalonia.Controls.UnitTests.Shapes var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; var target = new Path { Data = geometry }; + var root = new TestRoot(target); + target.Measure(Size.Infinity); Assert.True(target.IsMeasureValid); geometry.Rect = new Rect(0, 0, 20, 20); Assert.False(target.IsMeasureValid); + + root.Child = null; } } }