ListBoxTests.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System.Linq;
  4. using Avalonia.Collections;
  5. using Avalonia.Controls.Presenters;
  6. using Avalonia.Controls.Templates;
  7. using Avalonia.Input;
  8. using Avalonia.LogicalTree;
  9. using Avalonia.Styling;
  10. using Avalonia.UnitTests;
  11. using Avalonia.VisualTree;
  12. using Xunit;
  13. namespace Avalonia.Controls.UnitTests
  14. {
  15. public class ListBoxTests
  16. {
  17. private MouseTestHelper _mouse = new MouseTestHelper();
  18. [Fact]
  19. public void Should_Use_ItemTemplate_To_Create_Item_Content()
  20. {
  21. var target = new ListBox
  22. {
  23. Template = ListBoxTemplate(),
  24. Items = new[] { "Foo" },
  25. ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
  26. };
  27. Prepare(target);
  28. var container = (ListBoxItem)target.Presenter.Panel.Children[0];
  29. Assert.IsType<Canvas>(container.Presenter.Child);
  30. }
  31. [Fact]
  32. public void ListBox_Should_Find_ItemsPresenter_In_ScrollViewer()
  33. {
  34. var target = new ListBox
  35. {
  36. Template = ListBoxTemplate(),
  37. };
  38. Prepare(target);
  39. Assert.IsType<ItemsPresenter>(target.Presenter);
  40. }
  41. [Fact]
  42. public void ListBox_Should_Find_Scrollviewer_In_Template()
  43. {
  44. var target = new ListBox
  45. {
  46. Template = ListBoxTemplate(),
  47. };
  48. ScrollViewer viewer = null;
  49. target.TemplateApplied += (sender, e) =>
  50. {
  51. viewer = target.Scroll as ScrollViewer;
  52. };
  53. Prepare(target);
  54. Assert.NotNull(viewer);
  55. }
  56. [Fact]
  57. public void ListBoxItem_Containers_Should_Be_Generated()
  58. {
  59. using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
  60. {
  61. var items = new[] { "Foo", "Bar", "Baz " };
  62. var target = new ListBox
  63. {
  64. Template = ListBoxTemplate(),
  65. Items = items,
  66. };
  67. Prepare(target);
  68. var text = target.Presenter.Panel.Children
  69. .OfType<ListBoxItem>()
  70. .Select(x => x.Presenter.Child)
  71. .OfType<TextBlock>()
  72. .Select(x => x.Text)
  73. .ToList();
  74. Assert.Equal(items, text);
  75. }
  76. }
  77. [Fact]
  78. public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items()
  79. {
  80. using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
  81. {
  82. var target = new ListBox
  83. {
  84. Template = ListBoxTemplate(),
  85. Items = new[] { "Foo", "Bar", "Baz " },
  86. };
  87. Prepare(target);
  88. Assert.Equal(3, target.GetLogicalChildren().Count());
  89. foreach (var child in target.GetLogicalChildren())
  90. {
  91. Assert.IsType<ListBoxItem>(child);
  92. }
  93. }
  94. }
  95. [Fact]
  96. public void DataContexts_Should_Be_Correctly_Set()
  97. {
  98. using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
  99. {
  100. var items = new object[]
  101. {
  102. "Foo",
  103. new Item("Bar"),
  104. new TextBlock { Text = "Baz" },
  105. new ListBoxItem { Content = "Qux" },
  106. };
  107. var target = new ListBox
  108. {
  109. Template = ListBoxTemplate(),
  110. DataContext = "Base",
  111. DataTemplates =
  112. {
  113. new FuncDataTemplate<Item>((x, _) => new Button { Content = x })
  114. },
  115. Items = items,
  116. };
  117. Prepare(target);
  118. var dataContexts = target.Presenter.Panel.Children
  119. .Cast<Control>()
  120. .Select(x => x.DataContext)
  121. .ToList();
  122. Assert.Equal(
  123. new object[] { items[0], items[1], "Base", "Base" },
  124. dataContexts);
  125. }
  126. }
  127. [Fact]
  128. public void Selection_Should_Be_Cleared_On_Recycled_Items()
  129. {
  130. var target = new ListBox
  131. {
  132. Template = ListBoxTemplate(),
  133. Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
  134. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
  135. SelectedIndex = 0,
  136. };
  137. Prepare(target);
  138. // Make sure we're virtualized and first item is selected.
  139. Assert.Equal(10, target.Presenter.Panel.Children.Count);
  140. Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
  141. // Scroll down a page.
  142. target.Scroll.Offset = new Vector(0, 10);
  143. // Make sure recycled item isn't now selected.
  144. Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
  145. }
  146. [Fact]
  147. public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport()
  148. {
  149. var target = new ListBox
  150. {
  151. Template = ListBoxTemplate(),
  152. Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
  153. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
  154. SelectedIndex = 0,
  155. };
  156. Prepare(target);
  157. Assert.Equal(new Size(20, 20), target.Scroll.Extent);
  158. Assert.Equal(new Size(100, 10), target.Scroll.Viewport);
  159. }
  160. [Fact]
  161. public void Containers_Correct_After_Clear_Add_Remove()
  162. {
  163. // Issue #1936
  164. var items = new AvaloniaList<string>(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
  165. var target = new ListBox
  166. {
  167. Template = ListBoxTemplate(),
  168. Items = items,
  169. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
  170. SelectedIndex = 0,
  171. };
  172. Prepare(target);
  173. items.Clear();
  174. items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
  175. items.Remove("Item 2");
  176. Assert.Equal(
  177. items,
  178. target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content));
  179. }
  180. [Fact]
  181. public void Toggle_Selection_Should_Update_Containers()
  182. {
  183. var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
  184. var target = new ListBox
  185. {
  186. Template = ListBoxTemplate(),
  187. Items = items,
  188. SelectionMode = SelectionMode.Toggle,
  189. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
  190. };
  191. Prepare(target);
  192. var lbItems = target.GetLogicalChildren().OfType<ListBoxItem>().ToArray();
  193. var item = lbItems[0];
  194. Assert.Equal(false, item.IsSelected);
  195. RaisePressedEvent(target, item, MouseButton.Left);
  196. Assert.Equal(true, item.IsSelected);
  197. RaisePressedEvent(target, item, MouseButton.Left);
  198. Assert.Equal(false, item.IsSelected);
  199. }
  200. private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
  201. {
  202. _mouse.Click(listBox, item, mouseButton);
  203. }
  204. [Fact]
  205. public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown()
  206. {
  207. var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray();
  208. var target = new ListBox
  209. {
  210. Template = ListBoxTemplate(),
  211. Items = items,
  212. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 })
  213. };
  214. Prepare(target);
  215. var panel = target.Presenter.Panel as IVirtualizingPanel;
  216. var listBoxItems = panel.Children.OfType<ListBoxItem>();
  217. //virtualization should have created exactly 10 items
  218. Assert.Equal(10, listBoxItems.Count());
  219. Assert.Equal("0", listBoxItems.First().DataContext);
  220. Assert.Equal("9", listBoxItems.Last().DataContext);
  221. //instead pixeloffset > 0 there could be pretty complex sequence for repro
  222. //it involves add/remove/scroll to end multiple actions
  223. //which i can't find so far :(, but this is the simplest way to add it to unit test
  224. panel.PixelOffset = 1;
  225. //here scroll to end -> IndexOutOfRangeException is thrown
  226. target.Scroll.Offset = new Vector(0, 2);
  227. Assert.True(true);
  228. }
  229. [Fact]
  230. public void LayoutManager_Should_Measure_Arrange_All()
  231. {
  232. var virtualizationMode = ItemVirtualizationMode.Simple;
  233. using (UnitTestApplication.Start(TestServices.StyledWindow))
  234. {
  235. var items = new AvaloniaList<string>(Enumerable.Range(1, 7).Select(v => v.ToString()));
  236. var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight };
  237. wnd.IsVisible = true;
  238. var target = new ListBox();
  239. wnd.Content = target;
  240. var lm = wnd.LayoutManager;
  241. target.Height = 110;
  242. target.Width = 50;
  243. target.DataContext = items;
  244. target.VirtualizationMode = virtualizationMode;
  245. target.ItemTemplate = new FuncDataTemplate<object>((c, _) =>
  246. {
  247. var tb = new TextBlock() { Height = 10, Width = 30 };
  248. tb.Bind(TextBlock.TextProperty, new Data.Binding());
  249. return tb;
  250. }, true);
  251. lm.ExecuteInitialLayoutPass(wnd);
  252. target.Items = items;
  253. lm.ExecuteLayoutPass();
  254. items.Insert(3, "3+");
  255. lm.ExecuteLayoutPass();
  256. items.Insert(4, "4+");
  257. lm.ExecuteLayoutPass();
  258. //RESET
  259. items.Clear();
  260. foreach (var i in Enumerable.Range(1, 7))
  261. {
  262. items.Add(i.ToString());
  263. }
  264. //working bit better with this line no outof memory or remaining to arrange/measure ???
  265. //lm.ExecuteLayoutPass();
  266. items.Insert(2, "2+");
  267. lm.ExecuteLayoutPass();
  268. //after few more layout cycles layoutmanager shouldn't hold any more visual for measure/arrange
  269. lm.ExecuteLayoutPass();
  270. lm.ExecuteLayoutPass();
  271. var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic;
  272. var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
  273. var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
  274. Assert.Equal(0, toMeasure.Count());
  275. Assert.Equal(0, toArrange.Count());
  276. }
  277. }
  278. private FuncControlTemplate ListBoxTemplate()
  279. {
  280. return new FuncControlTemplate<ListBox>((parent, scope) =>
  281. new ScrollViewer
  282. {
  283. Name = "PART_ScrollViewer",
  284. Template = ScrollViewerTemplate(),
  285. Content = new ItemsPresenter
  286. {
  287. Name = "PART_ItemsPresenter",
  288. [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(),
  289. [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(),
  290. [~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(),
  291. }.RegisterInNameScope(scope)
  292. }.RegisterInNameScope(scope));
  293. }
  294. private FuncControlTemplate ListBoxItemTemplate()
  295. {
  296. return new FuncControlTemplate<ListBoxItem>((parent, scope) =>
  297. new ContentPresenter
  298. {
  299. Name = "PART_ContentPresenter",
  300. [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
  301. [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
  302. }.RegisterInNameScope(scope));
  303. }
  304. private FuncControlTemplate ScrollViewerTemplate()
  305. {
  306. return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
  307. new ScrollContentPresenter
  308. {
  309. Name = "PART_ContentPresenter",
  310. [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
  311. [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
  312. [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
  313. [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
  314. }.RegisterInNameScope(scope));
  315. }
  316. private void Prepare(ListBox target)
  317. {
  318. // The ListBox needs to be part of a rooted visual tree.
  319. var root = new TestRoot();
  320. root.Child = target;
  321. // Apply the template to the ListBox itself.
  322. target.ApplyTemplate();
  323. // Then to its inner ScrollViewer.
  324. var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single();
  325. scrollViewer.ApplyTemplate();
  326. // Then make the ScrollViewer create its child.
  327. ((ContentPresenter)scrollViewer.Presenter).UpdateChild();
  328. // Now the ItemsPresenter should be reigstered, so apply its template.
  329. target.Presenter.ApplyTemplate();
  330. // Because ListBox items are virtualized we need to do a layout to make them appear.
  331. target.Measure(new Size(100, 100));
  332. target.Arrange(new Rect(0, 0, 100, 100));
  333. // Now set and apply the item templates.
  334. foreach (ListBoxItem item in target.Presenter.Panel.Children)
  335. {
  336. item.Template = ListBoxItemTemplate();
  337. item.ApplyTemplate();
  338. item.Presenter.ApplyTemplate();
  339. ((ContentPresenter)item.Presenter).UpdateChild();
  340. }
  341. // The items were created before the template was applied, so now we need to go back
  342. // and re-arrange everything.
  343. foreach (IControl i in target.GetSelfAndVisualDescendants())
  344. {
  345. i.InvalidateMeasure();
  346. }
  347. target.Measure(new Size(100, 100));
  348. target.Arrange(new Rect(0, 0, 100, 100));
  349. }
  350. private class Item
  351. {
  352. public Item(string value)
  353. {
  354. Value = value;
  355. }
  356. public string Value { get; }
  357. }
  358. }
  359. }