ListBoxTests.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. using System.Linq;
  2. using System.Reactive.Subjects;
  3. using Avalonia.Collections;
  4. using Avalonia.Controls.Presenters;
  5. using Avalonia.Controls.Primitives;
  6. using Avalonia.Controls.Templates;
  7. using Avalonia.Data;
  8. using Avalonia.Input;
  9. using Avalonia.LogicalTree;
  10. using Avalonia.Styling;
  11. using Avalonia.Threading;
  12. using Avalonia.UnitTests;
  13. using Avalonia.VisualTree;
  14. using Xunit;
  15. namespace Avalonia.Controls.UnitTests
  16. {
  17. public class ListBoxTests
  18. {
  19. private MouseTestHelper _mouse = new MouseTestHelper();
  20. [Fact]
  21. public void Should_Use_ItemTemplate_To_Create_Item_Content()
  22. {
  23. var target = new ListBox
  24. {
  25. Template = ListBoxTemplate(),
  26. Items = new[] { "Foo" },
  27. ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
  28. };
  29. Prepare(target);
  30. var container = (ListBoxItem)target.Presenter.Panel.Children[0];
  31. Assert.IsType<Canvas>(container.Presenter.Child);
  32. }
  33. [Fact]
  34. public void ListBox_Should_Find_ItemsPresenter_In_ScrollViewer()
  35. {
  36. var target = new ListBox
  37. {
  38. Template = ListBoxTemplate(),
  39. };
  40. Prepare(target);
  41. Assert.IsType<ItemsPresenter>(target.Presenter);
  42. }
  43. [Fact]
  44. public void ListBox_Should_Find_Scrollviewer_In_Template()
  45. {
  46. var target = new ListBox
  47. {
  48. Template = ListBoxTemplate(),
  49. };
  50. ScrollViewer viewer = null;
  51. target.TemplateApplied += (sender, e) =>
  52. {
  53. viewer = target.Scroll as ScrollViewer;
  54. };
  55. Prepare(target);
  56. Assert.NotNull(viewer);
  57. }
  58. [Fact]
  59. public void ListBoxItem_Containers_Should_Be_Generated()
  60. {
  61. using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
  62. {
  63. var items = new[] { "Foo", "Bar", "Baz " };
  64. var target = new ListBox
  65. {
  66. Template = ListBoxTemplate(),
  67. Items = items,
  68. };
  69. Prepare(target);
  70. var text = target.Presenter.Panel.Children
  71. .OfType<ListBoxItem>()
  72. .Select(x => x.Presenter.Child)
  73. .OfType<TextBlock>()
  74. .Select(x => x.Text)
  75. .ToList();
  76. Assert.Equal(items, text);
  77. }
  78. }
  79. [Fact]
  80. public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items()
  81. {
  82. using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
  83. {
  84. var target = new ListBox
  85. {
  86. Template = ListBoxTemplate(),
  87. Items = new[] { "Foo", "Bar", "Baz " },
  88. };
  89. Prepare(target);
  90. Assert.Equal(3, target.GetLogicalChildren().Count());
  91. foreach (var child in target.GetLogicalChildren())
  92. {
  93. Assert.IsType<ListBoxItem>(child);
  94. }
  95. }
  96. }
  97. [Fact]
  98. public void DataContexts_Should_Be_Correctly_Set()
  99. {
  100. using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
  101. {
  102. var items = new object[]
  103. {
  104. "Foo",
  105. new Item("Bar"),
  106. new TextBlock { Text = "Baz" },
  107. new ListBoxItem { Content = "Qux" },
  108. };
  109. var target = new ListBox
  110. {
  111. Template = ListBoxTemplate(),
  112. DataContext = "Base",
  113. DataTemplates =
  114. {
  115. new FuncDataTemplate<Item>((x, _) => new Button { Content = x })
  116. },
  117. Items = items,
  118. };
  119. Prepare(target);
  120. var dataContexts = target.Presenter.Panel.Children
  121. .Cast<Control>()
  122. .Select(x => x.DataContext)
  123. .ToList();
  124. Assert.Equal(
  125. new object[] { items[0], items[1], "Base", "Base" },
  126. dataContexts);
  127. }
  128. }
  129. [Fact]
  130. public void Selection_Should_Be_Cleared_On_Recycled_Items()
  131. {
  132. var target = new ListBox
  133. {
  134. Template = ListBoxTemplate(),
  135. Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
  136. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
  137. SelectedIndex = 0,
  138. };
  139. Prepare(target);
  140. // Make sure we're virtualized and first item is selected.
  141. Assert.Equal(10, target.Presenter.Panel.Children.Count);
  142. Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
  143. // Scroll down a page.
  144. target.Scroll.Offset = new Vector(0, 10);
  145. // Make sure recycled item isn't now selected.
  146. Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
  147. }
  148. [Fact]
  149. public void ScrollViewer_Should_Have_Correct_Extent_And_Viewport()
  150. {
  151. var target = new ListBox
  152. {
  153. Template = ListBoxTemplate(),
  154. Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(),
  155. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
  156. SelectedIndex = 0,
  157. };
  158. Prepare(target);
  159. Assert.Equal(new Size(20, 20), target.Scroll.Extent);
  160. Assert.Equal(new Size(100, 10), target.Scroll.Viewport);
  161. }
  162. [Fact]
  163. public void Containers_Correct_After_Clear_Add_Remove()
  164. {
  165. // Issue #1936
  166. var items = new AvaloniaList<string>(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
  167. var target = new ListBox
  168. {
  169. Template = ListBoxTemplate(),
  170. Items = items,
  171. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
  172. SelectedIndex = 0,
  173. };
  174. Prepare(target);
  175. items.Clear();
  176. items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
  177. items.Remove("Item 2");
  178. Assert.Equal(
  179. items,
  180. target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content));
  181. }
  182. [Fact]
  183. public void Toggle_Selection_Should_Update_Containers()
  184. {
  185. var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
  186. var target = new ListBox
  187. {
  188. Template = ListBoxTemplate(),
  189. Items = items,
  190. SelectionMode = SelectionMode.Toggle,
  191. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
  192. };
  193. Prepare(target);
  194. var lbItems = target.GetLogicalChildren().OfType<ListBoxItem>().ToArray();
  195. var item = lbItems[0];
  196. Assert.Equal(false, item.IsSelected);
  197. RaisePressedEvent(target, item, MouseButton.Left);
  198. Assert.Equal(true, item.IsSelected);
  199. RaisePressedEvent(target, item, MouseButton.Left);
  200. Assert.Equal(false, item.IsSelected);
  201. }
  202. [Fact]
  203. public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection()
  204. {
  205. var items = new AvaloniaList<string>(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
  206. var target = new ListBox
  207. {
  208. Template = ListBoxTemplate(),
  209. Items = items,
  210. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
  211. };
  212. Prepare(target);
  213. target.Scroll.Offset = new Vector(0, 1);
  214. items.RemoveRange(0, 11);
  215. }
  216. private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
  217. {
  218. _mouse.Click(listBox, item, mouseButton);
  219. }
  220. [Fact]
  221. public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown()
  222. {
  223. var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray();
  224. var target = new ListBox
  225. {
  226. Template = ListBoxTemplate(),
  227. Items = items,
  228. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 })
  229. };
  230. Prepare(target);
  231. var panel = target.Presenter.Panel as IVirtualizingPanel;
  232. var listBoxItems = panel.Children.OfType<ListBoxItem>();
  233. //virtualization should have created exactly 10 items
  234. Assert.Equal(10, listBoxItems.Count());
  235. Assert.Equal("0", listBoxItems.First().DataContext);
  236. Assert.Equal("9", listBoxItems.Last().DataContext);
  237. //instead pixeloffset > 0 there could be pretty complex sequence for repro
  238. //it involves add/remove/scroll to end multiple actions
  239. //which i can't find so far :(, but this is the simplest way to add it to unit test
  240. panel.PixelOffset = 1;
  241. //here scroll to end -> IndexOutOfRangeException is thrown
  242. target.Scroll.Offset = new Vector(0, 2);
  243. Assert.True(true);
  244. }
  245. [Fact]
  246. public void LayoutManager_Should_Measure_Arrange_All()
  247. {
  248. var virtualizationMode = ItemVirtualizationMode.Simple;
  249. using (UnitTestApplication.Start(TestServices.StyledWindow))
  250. {
  251. var items = new AvaloniaList<string>(Enumerable.Range(1, 7).Select(v => v.ToString()));
  252. var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight };
  253. wnd.IsVisible = true;
  254. var target = new ListBox();
  255. wnd.Content = target;
  256. var lm = wnd.LayoutManager;
  257. target.Height = 110;
  258. target.Width = 50;
  259. target.DataContext = items;
  260. target.VirtualizationMode = virtualizationMode;
  261. target.ItemTemplate = new FuncDataTemplate<object>((c, _) =>
  262. {
  263. var tb = new TextBlock() { Height = 10, Width = 30 };
  264. tb.Bind(TextBlock.TextProperty, new Data.Binding());
  265. return tb;
  266. }, true);
  267. lm.ExecuteInitialLayoutPass();
  268. target.Items = items;
  269. lm.ExecuteLayoutPass();
  270. items.Insert(3, "3+");
  271. lm.ExecuteLayoutPass();
  272. items.Insert(4, "4+");
  273. lm.ExecuteLayoutPass();
  274. //RESET
  275. items.Clear();
  276. foreach (var i in Enumerable.Range(1, 7))
  277. {
  278. items.Add(i.ToString());
  279. }
  280. //working bit better with this line no outof memory or remaining to arrange/measure ???
  281. //lm.ExecuteLayoutPass();
  282. items.Insert(2, "2+");
  283. lm.ExecuteLayoutPass();
  284. //after few more layout cycles layoutmanager shouldn't hold any more visual for measure/arrange
  285. lm.ExecuteLayoutPass();
  286. lm.ExecuteLayoutPass();
  287. var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic;
  288. var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
  289. var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable<Layout.ILayoutable>;
  290. Assert.Equal(0, toMeasure.Count());
  291. Assert.Equal(0, toArrange.Count());
  292. }
  293. }
  294. [Fact]
  295. public void Clicking_Item_Should_Raise_BringIntoView_For_Correct_Control()
  296. {
  297. // Issue #3934
  298. var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
  299. var target = new ListBox
  300. {
  301. Template = ListBoxTemplate(),
  302. Items = items,
  303. ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
  304. SelectionMode = SelectionMode.AlwaysSelected,
  305. VirtualizationMode = ItemVirtualizationMode.None,
  306. };
  307. Prepare(target);
  308. // First an item that is not index 0 must be selected.
  309. _mouse.Click(target.Presenter.Panel.Children[1]);
  310. Assert.Equal(1, target.Selection.AnchorIndex);
  311. // We're going to be clicking on item 9.
  312. var item = (ListBoxItem)target.Presenter.Panel.Children[9];
  313. var raised = 0;
  314. // Make sure a RequestBringIntoView event is raised for item 9. It won't be handled
  315. // by the ScrollContentPresenter as the item is already visible, so we don't need
  316. // handledEventsToo: true. Issue #3934 failed here because item 0 was being scrolled
  317. // into view due to SelectionMode.AlwaysSelected.
  318. target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) =>
  319. {
  320. Assert.Same(item, e.TargetObject);
  321. ++raised;
  322. });
  323. // Click item 9.
  324. _mouse.Click(item);
  325. Assert.Equal(1, raised);
  326. }
  327. [Fact]
  328. public void Adding_And_Selecting_Item_With_AutoScrollToSelectedItem_Should_NotHide_FirstItem()
  329. {
  330. using (UnitTestApplication.Start(TestServices.StyledWindow))
  331. {
  332. var items = new AvaloniaList<string>();
  333. var wnd = new Window() { Width = 100, Height = 100, IsVisible = true };
  334. var target = new ListBox()
  335. {
  336. VerticalAlignment = Layout.VerticalAlignment.Top,
  337. AutoScrollToSelectedItem = true,
  338. Width = 50,
  339. VirtualizationMode = ItemVirtualizationMode.Simple,
  340. ItemTemplate = new FuncDataTemplate<object>((c, _) => new Border() { Height = 10 }),
  341. Items = items,
  342. };
  343. wnd.Content = target;
  344. var lm = wnd.LayoutManager;
  345. lm.ExecuteInitialLayoutPass();
  346. var panel = target.Presenter.Panel;
  347. items.Add("Item 1");
  348. target.Selection.Select(0);
  349. lm.ExecuteLayoutPass();
  350. Assert.Equal(1, panel.Children.Count);
  351. items.Add("Item 2");
  352. target.Selection.Select(1);
  353. lm.ExecuteLayoutPass();
  354. Assert.Equal(2, panel.Children.Count);
  355. //make sure we have enough space to show all items
  356. Assert.True(panel.Bounds.Height >= panel.Children.Sum(c => c.Bounds.Height));
  357. //make sure we show items and they completelly visible, not only partially
  358. Assert.True(panel.Children[0].Bounds.Top >= 0 && panel.Children[0].Bounds.Bottom <= panel.Bounds.Height, "first item is not completelly visible!");
  359. Assert.True(panel.Children[1].Bounds.Top >= 0 && panel.Children[1].Bounds.Bottom <= panel.Bounds.Height, "second item is not completelly visible!");
  360. }
  361. }
  362. private FuncControlTemplate ListBoxTemplate()
  363. {
  364. return new FuncControlTemplate<ListBox>((parent, scope) =>
  365. new ScrollViewer
  366. {
  367. Name = "PART_ScrollViewer",
  368. Template = ScrollViewerTemplate(),
  369. Content = new ItemsPresenter
  370. {
  371. Name = "PART_ItemsPresenter",
  372. [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(),
  373. [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(),
  374. [~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(),
  375. }.RegisterInNameScope(scope)
  376. }.RegisterInNameScope(scope));
  377. }
  378. private FuncControlTemplate ListBoxItemTemplate()
  379. {
  380. return new FuncControlTemplate<ListBoxItem>((parent, scope) =>
  381. new ContentPresenter
  382. {
  383. Name = "PART_ContentPresenter",
  384. [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
  385. [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
  386. }.RegisterInNameScope(scope));
  387. }
  388. private FuncControlTemplate ScrollViewerTemplate()
  389. {
  390. return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
  391. new Panel
  392. {
  393. Children =
  394. {
  395. new ScrollContentPresenter
  396. {
  397. Name = "PART_ContentPresenter",
  398. [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
  399. [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
  400. [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
  401. [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
  402. }.RegisterInNameScope(scope),
  403. new ScrollBar
  404. {
  405. Name = "verticalScrollBar",
  406. [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty],
  407. [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty],
  408. }
  409. }
  410. });
  411. }
  412. private void Prepare(ListBox target)
  413. {
  414. // The ListBox needs to be part of a rooted visual tree.
  415. var root = new TestRoot();
  416. root.Child = target;
  417. // Apply the template to the ListBox itself.
  418. target.ApplyTemplate();
  419. // Then to its inner ScrollViewer.
  420. var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single();
  421. scrollViewer.ApplyTemplate();
  422. // Then make the ScrollViewer create its child.
  423. ((ContentPresenter)scrollViewer.Presenter).UpdateChild();
  424. // Now the ItemsPresenter should be reigstered, so apply its template.
  425. target.Presenter.ApplyTemplate();
  426. // Because ListBox items are virtualized we need to do a layout to make them appear.
  427. target.Measure(new Size(100, 100));
  428. target.Arrange(new Rect(0, 0, 100, 100));
  429. // Now set and apply the item templates.
  430. foreach (ListBoxItem item in target.Presenter.Panel.Children)
  431. {
  432. item.Template = ListBoxItemTemplate();
  433. item.ApplyTemplate();
  434. item.Presenter.ApplyTemplate();
  435. ((ContentPresenter)item.Presenter).UpdateChild();
  436. }
  437. // The items were created before the template was applied, so now we need to go back
  438. // and re-arrange everything.
  439. foreach (IControl i in target.GetSelfAndVisualDescendants())
  440. {
  441. i.InvalidateMeasure();
  442. }
  443. target.Measure(new Size(100, 100));
  444. target.Arrange(new Rect(0, 0, 100, 100));
  445. }
  446. private class Item
  447. {
  448. public Item(string value)
  449. {
  450. Value = value;
  451. }
  452. public string Value { get; }
  453. }
  454. [Fact]
  455. public void SelectedItem_Validation()
  456. {
  457. var target = new ListBox
  458. {
  459. Template = ListBoxTemplate(),
  460. Items = new[] { "Foo" },
  461. ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
  462. SelectionMode = SelectionMode.AlwaysSelected,
  463. VirtualizationMode = ItemVirtualizationMode.None
  464. };
  465. Prepare(target);
  466. var exception = new System.InvalidCastException("failed validation");
  467. var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
  468. target.Bind(ComboBox.SelectedItemProperty, textObservable);
  469. Assert.True(DataValidationErrors.GetHasErrors(target));
  470. Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
  471. }
  472. }
  473. }