ControlTests.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.Linq;
  5. using System.Runtime.Remoting.Contexts;
  6. using Avalonia.Controls;
  7. using Avalonia.Controls.Shapes;
  8. using Avalonia.Controls.Templates;
  9. using Avalonia.Diagnostics;
  10. using Avalonia.Input;
  11. using Avalonia.Layout;
  12. using Avalonia.Media;
  13. using Avalonia.Platform;
  14. using Avalonia.Rendering;
  15. using Avalonia.Styling;
  16. using Avalonia.UnitTests;
  17. using Avalonia.VisualTree;
  18. using JetBrains.dotMemoryUnit;
  19. using Moq;
  20. using Xunit;
  21. using Xunit.Abstractions;
  22. namespace Avalonia.LeakTests
  23. {
  24. [DotMemoryUnit(FailIfRunWithoutSupport = false)]
  25. public class ControlTests
  26. {
  27. // Need to have the collection as field, so GC will not free it
  28. private readonly ObservableCollection<string> _observableCollection = new();
  29. public ControlTests(ITestOutputHelper atr)
  30. {
  31. DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
  32. }
  33. [Fact]
  34. public void DataGrid_Is_Freed()
  35. {
  36. using (Start())
  37. {
  38. // When attached to INotifyCollectionChanged, DataGrid will subscribe to it's events, potentially causing leak
  39. Func<Window> run = () =>
  40. {
  41. var window = new Window
  42. {
  43. Content = new DataGrid
  44. {
  45. Items = _observableCollection
  46. }
  47. };
  48. window.Show();
  49. // Do a layout and make sure that DataGrid gets added to visual tree.
  50. window.LayoutManager.ExecuteInitialLayoutPass();
  51. Assert.IsType<DataGrid>(window.Presenter.Child);
  52. // Clear the content and ensure the DataGrid is removed.
  53. window.Content = null;
  54. window.LayoutManager.ExecuteLayoutPass();
  55. Assert.Null(window.Presenter.Child);
  56. return window;
  57. };
  58. var result = run();
  59. dotMemory.Check(memory =>
  60. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<DataGrid>()).ObjectsCount));
  61. }
  62. }
  63. [Fact]
  64. public void Canvas_Is_Freed()
  65. {
  66. using (Start())
  67. {
  68. Func<Window> run = () =>
  69. {
  70. var window = new Window
  71. {
  72. Content = new Canvas()
  73. };
  74. window.Show();
  75. // Do a layout and make sure that Canvas gets added to visual tree.
  76. window.LayoutManager.ExecuteInitialLayoutPass();
  77. Assert.IsType<Canvas>(window.Presenter.Child);
  78. // Clear the content and ensure the Canvas is removed.
  79. window.Content = null;
  80. window.LayoutManager.ExecuteLayoutPass();
  81. Assert.Null(window.Presenter.Child);
  82. return window;
  83. };
  84. var result = run();
  85. dotMemory.Check(memory =>
  86. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
  87. }
  88. }
  89. [Fact]
  90. public void Named_Canvas_Is_Freed()
  91. {
  92. using (Start())
  93. {
  94. Func<Window> run = () =>
  95. {
  96. var scope = new NameScope();
  97. var window = new Window
  98. {
  99. Content = new Canvas
  100. {
  101. Name = "foo"
  102. }.RegisterInNameScope(scope)
  103. };
  104. NameScope.SetNameScope(window, scope);
  105. window.Show();
  106. // Do a layout and make sure that Canvas gets added to visual tree.
  107. window.LayoutManager.ExecuteInitialLayoutPass();
  108. Assert.IsType<Canvas>(window.Find<Canvas>("foo"));
  109. Assert.IsType<Canvas>(window.Presenter.Child);
  110. // Clear the content and ensure the Canvas is removed.
  111. window.Content = null;
  112. NameScope.SetNameScope(window, null);
  113. window.LayoutManager.ExecuteLayoutPass();
  114. Assert.Null(window.Presenter.Child);
  115. return window;
  116. };
  117. var result = run();
  118. dotMemory.Check(memory =>
  119. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
  120. }
  121. }
  122. [Fact]
  123. public void ScrollViewer_With_Content_Is_Freed()
  124. {
  125. using (Start())
  126. {
  127. Func<Window> run = () =>
  128. {
  129. var window = new Window
  130. {
  131. Content = new ScrollViewer
  132. {
  133. Content = new Canvas()
  134. }
  135. };
  136. window.Show();
  137. // Do a layout and make sure that ScrollViewer gets added to visual tree and its
  138. // template applied.
  139. window.LayoutManager.ExecuteInitialLayoutPass();
  140. Assert.IsType<ScrollViewer>(window.Presenter.Child);
  141. Assert.IsType<Canvas>(((ScrollViewer)window.Presenter.Child).Presenter.Child);
  142. // Clear the content and ensure the ScrollViewer is removed.
  143. window.Content = null;
  144. window.LayoutManager.ExecuteLayoutPass();
  145. Assert.Null(window.Presenter.Child);
  146. return window;
  147. };
  148. var result = run();
  149. dotMemory.Check(memory =>
  150. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBox>()).ObjectsCount));
  151. dotMemory.Check(memory =>
  152. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
  153. }
  154. }
  155. [Fact]
  156. public void TextBox_Is_Freed()
  157. {
  158. using (Start())
  159. {
  160. Func<Window> run = () =>
  161. {
  162. var window = new Window
  163. {
  164. Content = new TextBox()
  165. };
  166. window.Show();
  167. // Do a layout and make sure that TextBox gets added to visual tree and its
  168. // template applied.
  169. window.LayoutManager.ExecuteInitialLayoutPass();
  170. Assert.IsType<TextBox>(window.Presenter.Child);
  171. Assert.NotEmpty(window.Presenter.Child.GetVisualChildren());
  172. // Clear the content and ensure the TextBox is removed.
  173. window.Content = null;
  174. window.LayoutManager.ExecuteLayoutPass();
  175. Assert.Null(window.Presenter.Child);
  176. return window;
  177. };
  178. var result = run();
  179. dotMemory.Check(memory =>
  180. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBox>()).ObjectsCount));
  181. }
  182. }
  183. [Fact]
  184. public void TextBox_With_Xaml_Binding_Is_Freed()
  185. {
  186. using (Start())
  187. {
  188. Func<Window> run = () =>
  189. {
  190. var window = new Window
  191. {
  192. DataContext = new Node { Name = "foo" },
  193. Content = new TextBox()
  194. };
  195. var binding = new Avalonia.Data.Binding
  196. {
  197. Path = "Name"
  198. };
  199. var textBox = (TextBox)window.Content;
  200. textBox.Bind(TextBox.TextProperty, binding);
  201. window.Show();
  202. // Do a layout and make sure that TextBox gets added to visual tree and its
  203. // Text property set.
  204. window.LayoutManager.ExecuteInitialLayoutPass();
  205. Assert.IsType<TextBox>(window.Presenter.Child);
  206. Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text);
  207. // Clear the content and DataContext and ensure the TextBox is removed.
  208. window.Content = null;
  209. window.DataContext = null;
  210. window.LayoutManager.ExecuteLayoutPass();
  211. Assert.Null(window.Presenter.Child);
  212. return window;
  213. };
  214. var result = run();
  215. dotMemory.Check(memory =>
  216. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBox>()).ObjectsCount));
  217. dotMemory.Check(memory =>
  218. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Node>()).ObjectsCount));
  219. }
  220. }
  221. [Fact]
  222. public void TextBox_Class_Listeners_Are_Freed()
  223. {
  224. using (Start())
  225. {
  226. TextBox textBox;
  227. var window = new Window
  228. {
  229. Content = textBox = new TextBox()
  230. };
  231. window.Show();
  232. // Do a layout and make sure that TextBox gets added to visual tree and its
  233. // template applied.
  234. window.LayoutManager.ExecuteInitialLayoutPass();
  235. Assert.Same(textBox, window.Presenter.Child);
  236. // Get the border from the TextBox template.
  237. var border = textBox.GetTemplateChildren().FirstOrDefault(x => x.Name == "border");
  238. // The TextBox should have subscriptions to its Classes collection from the
  239. // default theme.
  240. Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers());
  241. // Clear the content and ensure the TextBox is removed.
  242. window.Content = null;
  243. window.LayoutManager.ExecuteLayoutPass();
  244. Assert.Null(window.Presenter.Child);
  245. // Check that the TextBox has no subscriptions to its Classes collection.
  246. Assert.Null(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers());
  247. }
  248. }
  249. [Fact]
  250. public void TreeView_Is_Freed()
  251. {
  252. using (Start())
  253. {
  254. Func<Window> run = () =>
  255. {
  256. var nodes = new[]
  257. {
  258. new Node
  259. {
  260. Children = new[] { new Node() },
  261. }
  262. };
  263. TreeView target;
  264. var window = new Window
  265. {
  266. Content = target = new TreeView
  267. {
  268. DataTemplates =
  269. {
  270. new FuncTreeDataTemplate<Node>(
  271. (x, _) => new TextBlock { Text = x.Name },
  272. x => x.Children)
  273. },
  274. Items = nodes
  275. }
  276. };
  277. window.Show();
  278. // Do a layout and make sure that TreeViewItems get realized.
  279. window.LayoutManager.ExecuteInitialLayoutPass();
  280. Assert.Single(target.ItemContainerGenerator.Containers);
  281. // Clear the content and ensure the TreeView is removed.
  282. window.Content = null;
  283. window.LayoutManager.ExecuteLayoutPass();
  284. Assert.Null(window.Presenter.Child);
  285. return window;
  286. };
  287. var result = run();
  288. dotMemory.Check(memory =>
  289. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TreeView>()).ObjectsCount));
  290. }
  291. }
  292. [Fact]
  293. public void Slider_Is_Freed()
  294. {
  295. using (Start())
  296. {
  297. Func<Window> run = () =>
  298. {
  299. var window = new Window
  300. {
  301. Content = new Slider()
  302. };
  303. window.Show();
  304. // Do a layout and make sure that Slider gets added to visual tree.
  305. window.LayoutManager.ExecuteInitialLayoutPass();
  306. Assert.IsType<Slider>(window.Presenter.Child);
  307. // Clear the content and ensure the Slider is removed.
  308. window.Content = null;
  309. window.LayoutManager.ExecuteLayoutPass();
  310. Assert.Null(window.Presenter.Child);
  311. return window;
  312. };
  313. var result = run();
  314. dotMemory.Check(memory =>
  315. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Slider>()).ObjectsCount));
  316. }
  317. }
  318. [Fact]
  319. public void TabItem_Is_Freed()
  320. {
  321. using (Start())
  322. {
  323. Func<Window> run = () =>
  324. {
  325. var window = new Window
  326. {
  327. Content = new TabControl
  328. {
  329. Items = new[] { new TabItem() }
  330. }
  331. };
  332. window.Show();
  333. // Do a layout and make sure that TabControl and TabItem gets added to visual tree.
  334. window.LayoutManager.ExecuteInitialLayoutPass();
  335. var tabControl = Assert.IsType<TabControl>(window.Presenter.Child);
  336. Assert.IsType<TabItem>(tabControl.Presenter.Panel.Children[0]);
  337. // Clear the items and ensure the TabItem is removed.
  338. tabControl.Items = null;
  339. window.LayoutManager.ExecuteLayoutPass();
  340. Assert.Empty(tabControl.Presenter.Panel.Children);
  341. return window;
  342. };
  343. var result = run();
  344. dotMemory.Check(memory =>
  345. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TabItem>()).ObjectsCount));
  346. }
  347. }
  348. [Fact]
  349. public void RendererIsDisposed()
  350. {
  351. using (Start())
  352. {
  353. var renderer = new Mock<IRenderer>();
  354. renderer.Setup(x => x.Dispose());
  355. var impl = new Mock<IWindowImpl>();
  356. impl.SetupGet(x => x.RenderScaling).Returns(1);
  357. impl.SetupProperty(x => x.Closed);
  358. impl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
  359. impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed());
  360. AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatform>()
  361. .ToConstant(new MockWindowingPlatform(() => impl.Object));
  362. var window = new Window()
  363. {
  364. Content = new Button()
  365. };
  366. window.Show();
  367. window.Close();
  368. renderer.Verify(r => r.Dispose());
  369. }
  370. }
  371. [Fact]
  372. public void Control_With_Style_RenderTransform_Is_Freed()
  373. {
  374. // # Issue #3545
  375. using (Start())
  376. {
  377. Func<Window> run = () =>
  378. {
  379. var window = new Window
  380. {
  381. Styles =
  382. {
  383. new Style(x => x.OfType<Canvas>())
  384. {
  385. Setters =
  386. {
  387. new Setter
  388. {
  389. Property = Visual.RenderTransformProperty,
  390. Value = new RotateTransform(45),
  391. }
  392. }
  393. }
  394. },
  395. Content = new Canvas()
  396. };
  397. window.Show();
  398. // Do a layout and make sure that Canvas gets added to visual tree with
  399. // its render transform.
  400. window.LayoutManager.ExecuteInitialLayoutPass();
  401. var canvas = Assert.IsType<Canvas>(window.Presenter.Child);
  402. Assert.IsType<RotateTransform>(canvas.RenderTransform);
  403. // Clear the content and ensure the Canvas is removed.
  404. window.Content = null;
  405. window.LayoutManager.ExecuteLayoutPass();
  406. Assert.Null(window.Presenter.Child);
  407. return window;
  408. };
  409. var result = run();
  410. dotMemory.Check(memory =>
  411. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
  412. }
  413. }
  414. [Fact]
  415. public void Attached_ContextMenu_Is_Freed()
  416. {
  417. using (Start())
  418. {
  419. void AttachShowAndDetachContextMenu(Control control)
  420. {
  421. var contextMenu = new ContextMenu
  422. {
  423. Items = new[]
  424. {
  425. new MenuItem { Header = "Foo" },
  426. new MenuItem { Header = "Foo" },
  427. }
  428. };
  429. control.ContextMenu = contextMenu;
  430. contextMenu.Open(control);
  431. contextMenu.Close();
  432. control.ContextMenu = null;
  433. }
  434. var window = new Window();
  435. window.Show();
  436. Assert.Same(window, FocusManager.Instance.Current);
  437. // Context menu in resources means the baseline may not be 0.
  438. var initialMenuCount = 0;
  439. var initialMenuItemCount = 0;
  440. dotMemory.Check(memory =>
  441. {
  442. initialMenuCount = memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount;
  443. initialMenuItemCount = memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount;
  444. });
  445. AttachShowAndDetachContextMenu(window);
  446. Mock.Get(window.PlatformImpl).Invocations.Clear();
  447. dotMemory.Check(memory =>
  448. Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
  449. dotMemory.Check(memory =>
  450. Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
  451. }
  452. }
  453. [Fact]
  454. public void Standalone_ContextMenu_Is_Freed()
  455. {
  456. using (Start())
  457. {
  458. void BuildAndShowContextMenu(Control control)
  459. {
  460. var contextMenu = new ContextMenu
  461. {
  462. Items = new[]
  463. {
  464. new MenuItem { Header = "Foo" },
  465. new MenuItem { Header = "Foo" },
  466. }
  467. };
  468. contextMenu.Open(control);
  469. contextMenu.Close();
  470. }
  471. var window = new Window();
  472. window.Show();
  473. Assert.Same(window, FocusManager.Instance.Current);
  474. // Context menu in resources means the baseline may not be 0.
  475. var initialMenuCount = 0;
  476. var initialMenuItemCount = 0;
  477. dotMemory.Check(memory =>
  478. {
  479. initialMenuCount = memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount;
  480. initialMenuItemCount = memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount;
  481. });
  482. BuildAndShowContextMenu(window);
  483. BuildAndShowContextMenu(window);
  484. Mock.Get(window.PlatformImpl).Invocations.Clear();
  485. dotMemory.Check(memory =>
  486. Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
  487. dotMemory.Check(memory =>
  488. Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
  489. }
  490. }
  491. [Fact]
  492. public void Path_Is_Freed()
  493. {
  494. using (Start())
  495. {
  496. var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
  497. Func<Window> run = () =>
  498. {
  499. var window = new Window
  500. {
  501. Content = new Path
  502. {
  503. Data = geometry
  504. }
  505. };
  506. window.Show();
  507. window.LayoutManager.ExecuteInitialLayoutPass();
  508. Assert.IsType<Path>(window.Presenter.Child);
  509. window.Content = null;
  510. window.LayoutManager.ExecuteLayoutPass();
  511. Assert.Null(window.Presenter.Child);
  512. return window;
  513. };
  514. var result = run();
  515. dotMemory.Check(memory =>
  516. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Path>()).ObjectsCount));
  517. // We are keeping geometry alive to simulate a resource that outlives the control.
  518. GC.KeepAlive(geometry);
  519. }
  520. }
  521. [Fact]
  522. public void ItemsRepeater_Is_Freed()
  523. {
  524. using (Start())
  525. {
  526. Func<Window> run = () =>
  527. {
  528. var window = new Window
  529. {
  530. Content = new ItemsRepeater(),
  531. };
  532. window.Show();
  533. window.LayoutManager.ExecuteInitialLayoutPass();
  534. Assert.IsType<ItemsRepeater>(window.Presenter.Child);
  535. window.Content = null;
  536. window.LayoutManager.ExecuteLayoutPass();
  537. Assert.Null(window.Presenter.Child);
  538. return window;
  539. };
  540. var result = run();
  541. dotMemory.Check(memory =>
  542. Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ItemsRepeater>()).ObjectsCount));
  543. }
  544. }
  545. private IDisposable Start()
  546. {
  547. return UnitTestApplication.Start(TestServices.StyledWindow.With(
  548. focusManager: new FocusManager(),
  549. keyboardDevice: () => new KeyboardDevice(),
  550. inputManager: new InputManager()));
  551. }
  552. private class Node
  553. {
  554. public string Name { get; set; }
  555. public IEnumerable<Node> Children { get; set; }
  556. }
  557. private class NullRenderer : IRenderer
  558. {
  559. public bool DrawFps { get; set; }
  560. public bool DrawDirtyRects { get; set; }
  561. #pragma warning disable CS0067
  562. public event EventHandler<SceneInvalidatedEventArgs> SceneInvalidated;
  563. #pragma warning restore CS0067
  564. public void AddDirty(IVisual visual)
  565. {
  566. }
  567. public void Dispose()
  568. {
  569. }
  570. public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter) => null;
  571. public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter) => null;
  572. public void Paint(Rect rect)
  573. {
  574. }
  575. public void RecalculateChildren(IVisual visual)
  576. {
  577. }
  578. public void Resized(Size size)
  579. {
  580. }
  581. public void Start()
  582. {
  583. }
  584. public void Stop()
  585. {
  586. }
  587. }
  588. }
  589. }