Просмотр исходного кода

LayoutTransformControl - fix memory leak due to Transform.Changed event subscription (#19718)

* fix Transform.Changed memory leak in LayoutTransformControl

* refix

* apply transform on attach to visual tree
pavelovcharov 1 неделя назад
Родитель
Сommit
1d8e38417b

+ 37 - 10
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.
         /// </summary>
         private Matrix _transformation = Matrix.Identity;
-        private IDisposable? _transformChangedEvent;
 
         /// <summary>
         /// 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;
+            }
+        }
     }
 }

+ 29 - 0
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)
         {

+ 49 - 0
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<Window> 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<LayoutTransformControl>(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<LayoutTransformControl>()).ObjectsCount));
+                dotMemory.Check(memory =>
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
+
+                // We are keeping transform alive to simulate a resource that outlives the control.
+                GC.KeepAlive(transform);
+            }
+        }
+
         private FuncControlTemplate CreateWindowTemplate()
         {
             return new FuncControlTemplate<Window>((parent, scope) =>