Browse Source

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
release/11.3.8
pavelovcharov 4 months ago
committed by Julien Lebosquain
parent
commit
1ddb0467e3
  1. 47
      src/Avalonia.Controls/LayoutTransformControl.cs
  2. 29
      tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs
  3. 49
      tests/Avalonia.LeakTests/ControlTests.cs

47
src/Avalonia.Controls/LayoutTransformControl.cs

@ -145,6 +145,19 @@ namespace Avalonia.Controls
// Return result to allocate enough space for the transformation // Return result to allocate enough space for the transformation
return transformedDesiredSize; 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; private IDisposable? _renderTransformChangedEvent;
@ -224,7 +237,6 @@ namespace Avalonia.Controls
/// Transformation matrix corresponding to _matrixTransform. /// Transformation matrix corresponding to _matrixTransform.
/// </summary> /// </summary>
private Matrix _transformation = Matrix.Identity; private Matrix _transformation = Matrix.Identity;
private IDisposable? _transformChangedEvent;
/// <summary> /// <summary>
/// Returns true if Size a is smaller than Size b in either dimension. /// 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) private void OnLayoutTransformChanged(AvaloniaPropertyChangedEventArgs e)
{ {
var newTransform = e.NewValue as Transform; if (this.IsAttachedToVisualTree)
_transformChangedEvent?.Dispose();
_transformChangedEvent = null;
if (newTransform != null)
{ {
_transformChangedEvent = Observable.FromEventPattern( UnsubscribeLayoutTransform(e.OldValue as Transform);
v => newTransform.Changed += v, v => newTransform.Changed -= v) SubscribeLayoutTransform(e.NewValue as Transform);
.Subscribe(_ => ApplyLayoutTransform());
} }
ApplyLayoutTransform();
}
private void OnTransformChanged(object? sender, EventArgs e)
{
ApplyLayoutTransform(); ApplyLayoutTransform();
} }
private void SubscribeLayoutTransform(Transform? transform)
{
if (transform != null)
{
transform.Changed += OnTransformChanged;
}
}
private void UnsubscribeLayoutTransform(Transform? transform)
{
if (transform != null)
{
transform.Changed -= OnTransformChanged;
}
}
} }
} }

29
tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs

@ -306,6 +306,35 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(m.M31, res.M31, 3); Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 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) private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize)
{ {

49
tests/Avalonia.LeakTests/ControlTests.cs

@ -13,6 +13,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; 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() private FuncControlTemplate CreateWindowTemplate()
{ {
return new FuncControlTemplate<Window>((parent, scope) => return new FuncControlTemplate<Window>((parent, scope) =>

Loading…
Cancel
Save