diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 6cd2ee14bb..c6eef00a76 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -145,6 +145,19 @@ namespace Avalonia.Controls // Return result to allocate enough space for the transformation return transformedDesiredSize; } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + SubscribeLayoutTransform(LayoutTransform as Transform); + ApplyLayoutTransform(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + UnsubscribeLayoutTransform(LayoutTransform as Transform); + } private IDisposable? _renderTransformChangedEvent; @@ -224,7 +237,6 @@ namespace Avalonia.Controls /// Transformation matrix corresponding to _matrixTransform. /// private Matrix _transformation = Matrix.Identity; - private IDisposable? _transformChangedEvent; /// /// Returns true if Size a is smaller than Size b in either dimension. @@ -424,19 +436,34 @@ namespace Avalonia.Controls private void OnLayoutTransformChanged(AvaloniaPropertyChangedEventArgs e) { - var newTransform = e.NewValue as Transform; - - _transformChangedEvent?.Dispose(); - _transformChangedEvent = null; - - if (newTransform != null) + if (this.IsAttachedToVisualTree) { - _transformChangedEvent = Observable.FromEventPattern( - v => newTransform.Changed += v, v => newTransform.Changed -= v) - .Subscribe(_ => ApplyLayoutTransform()); + UnsubscribeLayoutTransform(e.OldValue as Transform); + SubscribeLayoutTransform(e.NewValue as Transform); } + + ApplyLayoutTransform(); + } + private void OnTransformChanged(object? sender, EventArgs e) + { ApplyLayoutTransform(); } + + private void SubscribeLayoutTransform(Transform? transform) + { + if (transform != null) + { + transform.Changed += OnTransformChanged; + } + } + + private void UnsubscribeLayoutTransform(Transform? transform) + { + if (transform != null) + { + transform.Changed -= OnTransformChanged; + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs index c8a5af05a4..965cd251c9 100644 --- a/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs @@ -306,6 +306,35 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(m.M31, res.M31, 3); Assert.Equal(m.M32, res.M32, 3); } + + [Fact] + public void Should_Apply_Transform_On_Attach_To_VisualTree() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var transform = new SkewTransform() { AngleX = -45, AngleY = -45 }; + + LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform( + 100, + 100, + transform); + + transform.AngleX = 45; + transform.AngleY = 45; + + var window = new Window { Content = lt }; + window.Show(); + + Matrix actual = lt.TransformRoot.RenderTransform.Value; + Matrix expected = Matrix.CreateSkew(Matrix.ToRadians(45), Matrix.ToRadians(45)); + Assert.Equal(expected.M11, actual.M11, 3); + Assert.Equal(expected.M12, actual.M12, 3); + Assert.Equal(expected.M21, actual.M21, 3); + Assert.Equal(expected.M22, actual.M22, 3); + Assert.Equal(expected.M31, actual.M31, 3); + Assert.Equal(expected.M32, actual.M32, 3); + } + } private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize) { diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index c83c92b1e2..a4998f06b7 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -13,6 +13,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -1000,6 +1001,54 @@ namespace Avalonia.LeakTests } } + [Fact] + public void LayoutTransformControl_Is_Freed() + { + using (Start()) + { + var transform = new RotateTransform { Angle = 90 }; + + Func run = () => + { + var window = new Window + { + Content = new LayoutTransformControl + { + LayoutTransform = transform, + Child = new Canvas() + } + }; + + window.Show(); + + // Do a layout and make sure that LayoutTransformControl gets added to visual tree + window.LayoutManager.ExecuteInitialLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.NotEmpty(window.Presenter.Child.GetVisualChildren()); + + // Clear the content and ensure the LayoutTransformControl is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + + // We are keeping transform alive to simulate a resource that outlives the control. + GC.KeepAlive(transform); + } + } + private FuncControlTemplate CreateWindowTemplate() { return new FuncControlTemplate((parent, scope) =>