using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Input; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; using JetBrains.dotMemoryUnit; using Moq; using Xunit; using Xunit.Abstractions; namespace Avalonia.LeakTests { [DotMemoryUnit(FailIfRunWithoutSupport = false)] public class ControlTests { // Need to have the collection as field, so GC will not free it private readonly ObservableCollection _observableCollection = new(); public ControlTests(ITestOutputHelper atr) { DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); } [Fact] public void DataGrid_Is_Freed() { using (Start()) { // When attached to INotifyCollectionChanged, DataGrid will subscribe to it's events, potentially causing leak Func run = () => { var window = new Window { Content = new DataGrid { Items = _observableCollection } }; window.Show(); // Do a layout and make sure that DataGrid gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the DataGrid 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)); } } [Fact] public void Canvas_Is_Freed() { using (Start()) { Func run = () => { var window = new Window { Content = new Canvas() }; window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); // 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)); } } [Fact] public void Named_Canvas_Is_Freed() { using (Start()) { Func run = () => { var scope = new NameScope(); var window = new Window { Content = new Canvas { Name = "foo" }.RegisterInNameScope(scope) }; NameScope.SetNameScope(window, scope); window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Find("foo")); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Canvas is removed. window.Content = null; NameScope.SetNameScope(window, 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)); } } [Fact] public void ScrollViewer_With_Content_Is_Freed() { using (Start()) { Func run = () => { var window = new Window { Content = new ScrollViewer { Content = new Canvas() } }; window.Show(); // Do a layout and make sure that ScrollViewer gets added to visual tree and its // template applied. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); // Clear the content and ensure the ScrollViewer 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)); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } [Fact] public void TextBox_Is_Freed() { using (Start()) { Func run = () => { var window = new Window { Content = new TextBox() }; window.Show(); // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.NotEmpty(window.Presenter.Child.GetVisualChildren()); // Clear the content and ensure the TextBox 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)); } } [Fact] public void TextBox_With_Xaml_Binding_Is_Freed() { using (Start()) { Func run = () => { var window = new Window { DataContext = new Node { Name = "foo" }, Content = new TextBox() }; var binding = new Avalonia.Data.Binding { Path = "Name" }; var textBox = (TextBox)window.Content; textBox.Bind(TextBox.TextProperty, binding); window.Show(); // Do a layout and make sure that TextBox gets added to visual tree and its // Text property set. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); // Clear the content and DataContext and ensure the TextBox is removed. window.Content = null; window.DataContext = 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)); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } [Fact] public void TextBox_Class_Listeners_Are_Freed() { using (Start()) { TextBox textBox; var window = new Window { Content = textBox = new TextBox() }; window.Show(); // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.Same(textBox, window.Presenter.Child); // Get the border from the TextBox template. var border = textBox.GetTemplateChildren().FirstOrDefault(x => x.Name == "border"); // The TextBox should have subscriptions to its Classes collection from the // default theme. Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers()); // Clear the content and ensure the TextBox is removed. window.Content = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); // Check that the TextBox has no subscriptions to its Classes collection. Assert.Null(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers()); } } [Fact] public void TreeView_Is_Freed() { using (Start()) { Func run = () => { var nodes = new[] { new Node { Children = new[] { new Node() }, } }; TreeView target; var window = new Window { Content = target = new TreeView { DataTemplates = { new FuncTreeDataTemplate( (x, _) => new TextBlock { Text = x.Name }, x => x.Children) }, Items = nodes } }; window.Show(); // Do a layout and make sure that TreeViewItems get realized. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.Single(target.ItemContainerGenerator.Containers); // Clear the content and ensure the TreeView 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)); } } [Fact] public void Slider_Is_Freed() { using (Start()) { Func run = () => { var window = new Window { Content = new Slider() }; window.Show(); // Do a layout and make sure that Slider gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Slider 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)); } } [Fact] public void TabItem_Is_Freed() { using (Start()) { Func run = () => { var window = new Window { Content = new TabControl { Items = new[] { new TabItem() } } }; window.Show(); // Do a layout and make sure that TabControl and TabItem gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); var tabControl = Assert.IsType(window.Presenter.Child); Assert.IsType(tabControl.Presenter.Panel.Children[0]); // Clear the items and ensure the TabItem is removed. tabControl.Items = null; window.LayoutManager.ExecuteLayoutPass(); Assert.Empty(tabControl.Presenter.Panel.Children); return window; }; var result = run(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } [Fact] public void RendererIsDisposed() { using (Start()) { var renderer = new Mock(); renderer.Setup(x => x.Dispose()); var impl = new Mock(); impl.SetupGet(x => x.RenderScaling).Returns(1); impl.SetupProperty(x => x.Closed); impl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed()); AvaloniaLocator.CurrentMutable.Bind() .ToConstant(new MockWindowingPlatform(() => impl.Object)); var window = new Window() { Content = new Button() }; window.Show(); window.Close(); renderer.Verify(r => r.Dispose()); } } [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(); 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)); } } [Fact] public void Attached_ContextMenu_Is_Freed() { using (Start()) { void AttachShowAndDetachContextMenu(Control control) { var contextMenu = new ContextMenu { Items = new[] { new MenuItem { Header = "Foo" }, new MenuItem { Header = "Foo" }, } }; control.ContextMenu = contextMenu; contextMenu.Open(control); contextMenu.Close(); control.ContextMenu = null; } var window = new Window(); window.Show(); Assert.Same(window, FocusManager.Instance.Current); // Context menu in resources means the baseline may not be 0. var initialMenuCount = 0; var initialMenuItemCount = 0; dotMemory.Check(memory => { initialMenuCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; initialMenuItemCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; }); AttachShowAndDetachContextMenu(window); Mock.Get(window.PlatformImpl).Invocations.Clear(); dotMemory.Check(memory => Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } [Fact] public void Standalone_ContextMenu_Is_Freed() { using (Start()) { void BuildAndShowContextMenu(Control control) { var contextMenu = new ContextMenu { Items = new[] { new MenuItem { Header = "Foo" }, new MenuItem { Header = "Foo" }, } }; contextMenu.Open(control); contextMenu.Close(); } var window = new Window(); window.Show(); Assert.Same(window, FocusManager.Instance.Current); // Context menu in resources means the baseline may not be 0. var initialMenuCount = 0; var initialMenuItemCount = 0; dotMemory.Check(memory => { initialMenuCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; initialMenuItemCount = memory.GetObjects(where => where.Type.Is()).ObjectsCount; }); BuildAndShowContextMenu(window); BuildAndShowContextMenu(window); Mock.Get(window.PlatformImpl).Invocations.Clear(); dotMemory.Check(memory => Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } [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(); 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); } } [Fact] public void ItemsRepeater_Is_Freed() { using (Start()) { Func run = () => { var window = new Window { Content = new ItemsRepeater(), }; window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); 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)); } } [Fact] public void ElementName_Binding_In_DataTemplate_Is_Freed() { using (Start()) { var items = new ObservableCollection(Enumerable.Range(0, 10)); NameScope ns; TextBox tb; ListBox lb; var window = new Window { [NameScope.NameScopeProperty] = ns = new NameScope(), Width = 100, Height = 100, Content = new StackPanel { Children = { (tb = new TextBox { Name = "tb", Text = "foo", }), (lb = new ListBox { Items = items, ItemTemplate = new FuncDataTemplate((_, _) => new Canvas { Width = 10, Height = 10, [!Control.TagProperty] = new Binding { ElementName = "tb", Path = "Text", NameScope = new WeakReference(ns), } }) }), } } }; tb.RegisterInNameScope(ns); window.Show(); window.LayoutManager.ExecuteInitialLayoutPass(); void AssertInitialItemState() { var item0 = (ListBoxItem)lb.ItemContainerGenerator.Containers.First().ContainerControl; var canvas0 = (Canvas)item0.Presenter.Child; Assert.Equal("foo", canvas0.Tag); } Assert.Equal(10, lb.ItemContainerGenerator.Containers.Count()); AssertInitialItemState(); items.Clear(); window.LayoutManager.ExecuteLayoutPass(); Assert.Empty(lb.ItemContainerGenerator.Containers); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } private IDisposable Start() { void Cleanup() { // KeyboardDevice holds a reference to the focused item. KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); // Empty the dispatcher queue. Dispatcher.UIThread.RunJobs(); } return new CompositeDisposable { Disposable.Create(Cleanup), UnitTestApplication.Start(TestServices.StyledWindow.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), inputManager: new InputManager())) }; } private class Node { public string Name { get; set; } public IEnumerable Children { get; set; } } private class NullRenderer : IRenderer { public bool DrawFps { get; set; } public bool DrawDirtyRects { get; set; } #pragma warning disable CS0067 public event EventHandler SceneInvalidated; #pragma warning restore CS0067 public void AddDirty(IVisual visual) { } public void Dispose() { } public IEnumerable HitTest(Point p, IVisual root, Func filter) => null; public IVisual HitTestFirst(Point p, IVisual root, Func filter) => null; public void Paint(Rect rect) { } public void RecalculateChildren(IVisual visual) { } public void Resized(Size size) { } public void Start() { } public void Stop() { } } } }