TreeViewTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using Avalonia.Collections;
  7. using Avalonia.Controls.Presenters;
  8. using Avalonia.Controls.Templates;
  9. using Avalonia.Data;
  10. using Avalonia.Data.Core;
  11. using Avalonia.Input;
  12. using Avalonia.LogicalTree;
  13. using Avalonia.Markup.Data;
  14. using Avalonia.UnitTests;
  15. using Xunit;
  16. namespace Avalonia.Controls.UnitTests
  17. {
  18. public class TreeViewTests
  19. {
  20. [Fact]
  21. public void Items_Should_Be_Created()
  22. {
  23. var target = new TreeView
  24. {
  25. Template = CreateTreeViewTemplate(),
  26. Items = CreateTestTreeData(),
  27. };
  28. CreateNodeDataTemplate(target);
  29. ApplyTemplates(target);
  30. Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0));
  31. Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1));
  32. Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2));
  33. }
  34. [Fact]
  35. public void Items_Should_Be_Created_Using_ItemTemplate_If_Present()
  36. {
  37. TreeView target;
  38. var root = new TestRoot
  39. {
  40. Child = target = new TreeView
  41. {
  42. Template = CreateTreeViewTemplate(),
  43. Items = CreateTestTreeData(),
  44. ItemTemplate = new FuncTreeDataTemplate<Node>(
  45. _ => new Canvas(),
  46. x => x.Children),
  47. }
  48. };
  49. ApplyTemplates(target);
  50. var items = target.ItemContainerGenerator.Index.Items
  51. .OfType<TreeViewItem>()
  52. .ToList();
  53. Assert.Equal(5, items.Count);
  54. Assert.All(items, x => Assert.IsType<Canvas>(x.HeaderPresenter.Child));
  55. }
  56. [Fact]
  57. public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers()
  58. {
  59. var target = new TreeView
  60. {
  61. Template = CreateTreeViewTemplate(),
  62. Items = CreateTestTreeData(),
  63. };
  64. CreateNodeDataTemplate(target);
  65. ApplyTemplates(target);
  66. var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single().ContainerControl;
  67. var header = (TextBlock)container.Header;
  68. Assert.Equal("Root", header.Text);
  69. }
  70. [Fact]
  71. public void Root_TreeContainerFromItem_Should_Return_Descendant_Item()
  72. {
  73. var tree = CreateTestTreeData();
  74. var target = new TreeView
  75. {
  76. Template = CreateTreeViewTemplate(),
  77. Items = tree,
  78. };
  79. // For TreeViewItem to find its parent TreeView, OnAttachedToLogicalTree needs
  80. // to be called, which requires an IStyleRoot.
  81. var root = new TestRoot();
  82. root.Child = target;
  83. CreateNodeDataTemplate(target);
  84. ApplyTemplates(target);
  85. var container = target.ItemContainerGenerator.Index.ContainerFromItem(
  86. tree[0].Children[1].Children[0]);
  87. Assert.NotNull(container);
  88. var header = ((TreeViewItem)container).Header;
  89. var headerContent = ((TextBlock)header).Text;
  90. Assert.Equal("Grandchild2a", headerContent);
  91. }
  92. [Fact]
  93. public void Clicking_Item_Should_Select_It()
  94. {
  95. var tree = CreateTestTreeData();
  96. var target = new TreeView
  97. {
  98. Template = CreateTreeViewTemplate(),
  99. Items = tree,
  100. };
  101. var visualRoot = new TestRoot();
  102. visualRoot.Child = target;
  103. CreateNodeDataTemplate(target);
  104. ApplyTemplates(target);
  105. var item = tree[0].Children[1].Children[0];
  106. var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
  107. Assert.NotNull(container);
  108. container.RaiseEvent(new PointerPressedEventArgs
  109. {
  110. RoutedEvent = InputElement.PointerPressedEvent,
  111. MouseButton = MouseButton.Left,
  112. });
  113. Assert.Equal(item, target.SelectedItem);
  114. Assert.True(container.IsSelected);
  115. }
  116. [Fact]
  117. public void Setting_SelectedItem_Should_Set_Container_Selected()
  118. {
  119. var tree = CreateTestTreeData();
  120. var target = new TreeView
  121. {
  122. Template = CreateTreeViewTemplate(),
  123. Items = tree,
  124. };
  125. var visualRoot = new TestRoot();
  126. visualRoot.Child = target;
  127. CreateNodeDataTemplate(target);
  128. ApplyTemplates(target);
  129. var item = tree[0].Children[1].Children[0];
  130. var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
  131. Assert.NotNull(container);
  132. target.SelectedItem = item;
  133. Assert.True(container.IsSelected);
  134. }
  135. [Fact]
  136. public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event()
  137. {
  138. var tree = CreateTestTreeData();
  139. var target = new TreeView
  140. {
  141. Template = CreateTreeViewTemplate(),
  142. Items = tree,
  143. };
  144. var visualRoot = new TestRoot();
  145. visualRoot.Child = target;
  146. CreateNodeDataTemplate(target);
  147. ApplyTemplates(target);
  148. var item = tree[0].Children[1].Children[0];
  149. var called = false;
  150. target.SelectedItemChanged += (s, e) =>
  151. {
  152. Assert.Null(e.OldItem);
  153. Assert.Same(item, e.NewItem);
  154. called = true;
  155. };
  156. target.SelectedItem = item;
  157. Assert.True(called);
  158. }
  159. [Fact]
  160. public void LogicalChildren_Should_Be_Set()
  161. {
  162. var target = new TreeView
  163. {
  164. Template = CreateTreeViewTemplate(),
  165. Items = new[] { "Foo", "Bar", "Baz " },
  166. };
  167. ApplyTemplates(target);
  168. var result = target.GetLogicalChildren()
  169. .OfType<TreeViewItem>()
  170. .Select(x => x.Header)
  171. .OfType<TextBlock>()
  172. .Select(x => x.Text)
  173. .ToList();
  174. Assert.Equal(new[] { "Foo", "Bar", "Baz " }, result);
  175. }
  176. [Fact]
  177. public void Removing_Item_Should_Remove_Itself_And_Children_From_Index()
  178. {
  179. var tree = CreateTestTreeData();
  180. var target = new TreeView
  181. {
  182. Template = CreateTreeViewTemplate(),
  183. Items = tree,
  184. };
  185. var root = new TestRoot();
  186. root.Child = target;
  187. CreateNodeDataTemplate(target);
  188. ApplyTemplates(target);
  189. Assert.Equal(5, target.ItemContainerGenerator.Index.Items.Count());
  190. tree[0].Children.RemoveAt(1);
  191. Assert.Equal(3, target.ItemContainerGenerator.Index.Items.Count());
  192. }
  193. [Fact]
  194. public void DataContexts_Should_Be_Correctly_Set()
  195. {
  196. var items = new object[]
  197. {
  198. "Foo",
  199. new Node { Value = "Bar" },
  200. new TextBlock { Text = "Baz" },
  201. new TreeViewItem { Header = "Qux" },
  202. };
  203. var target = new TreeView
  204. {
  205. Template = CreateTreeViewTemplate(),
  206. DataContext = "Base",
  207. DataTemplates =
  208. {
  209. new FuncDataTemplate<Node>(x => new Button { Content = x })
  210. },
  211. Items = items,
  212. };
  213. ApplyTemplates(target);
  214. var dataContexts = target.Presenter.Panel.Children
  215. .Cast<Control>()
  216. .Select(x => x.DataContext)
  217. .ToList();
  218. Assert.Equal(
  219. new object[] { items[0], items[1], "Base", "Base" },
  220. dataContexts);
  221. }
  222. [Fact]
  223. public void Control_Item_Should_Not_Be_NameScope()
  224. {
  225. var items = new object[]
  226. {
  227. new TreeViewItem(),
  228. };
  229. var target = new TreeView
  230. {
  231. Template = CreateTreeViewTemplate(),
  232. Items = items,
  233. };
  234. target.ApplyTemplate();
  235. target.Presenter.ApplyTemplate();
  236. var item = target.Presenter.Panel.LogicalChildren[0];
  237. Assert.Null(NameScope.GetNameScope((TreeViewItem)item));
  238. }
  239. [Fact]
  240. public void DataTemplate_Created_Item_Should_Be_NameScope()
  241. {
  242. var items = new object[]
  243. {
  244. "foo",
  245. };
  246. var target = new TreeView
  247. {
  248. Template = CreateTreeViewTemplate(),
  249. Items = items,
  250. };
  251. target.ApplyTemplate();
  252. target.Presenter.ApplyTemplate();
  253. var item = target.Presenter.Panel.LogicalChildren[0];
  254. Assert.NotNull(NameScope.GetNameScope((TreeViewItem)item));
  255. }
  256. [Fact]
  257. public void Should_React_To_Children_Changing()
  258. {
  259. var data = CreateTestTreeData();
  260. var target = new TreeView
  261. {
  262. Template = CreateTreeViewTemplate(),
  263. Items = data,
  264. };
  265. CreateNodeDataTemplate(target);
  266. ApplyTemplates(target);
  267. Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0));
  268. Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1));
  269. Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2));
  270. // Make sure that the binding to Node.Children does not get collected.
  271. GC.Collect();
  272. data[0].Children = new AvaloniaList<Node>
  273. {
  274. new Node
  275. {
  276. Value = "NewChild1",
  277. }
  278. };
  279. Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0));
  280. Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1));
  281. }
  282. [Fact]
  283. public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node()
  284. {
  285. using (UnitTestApplication.Start(TestServices.RealFocus))
  286. {
  287. var focus = FocusManager.Instance;
  288. var navigation = AvaloniaLocator.Current.GetService<IKeyboardNavigationHandler>();
  289. var data = CreateTestTreeData();
  290. var target = new TreeView
  291. {
  292. Template = CreateTreeViewTemplate(),
  293. Items = data,
  294. };
  295. var button = new Button();
  296. var root = new TestRoot
  297. {
  298. Child = new StackPanel
  299. {
  300. Children = { target, button },
  301. }
  302. };
  303. CreateNodeDataTemplate(target);
  304. ApplyTemplates(target);
  305. var item = data[0].Children[0];
  306. var node = target.ItemContainerGenerator.Index.ContainerFromItem(item);
  307. Assert.NotNull(node);
  308. target.SelectedItem = item;
  309. node.Focus();
  310. Assert.Same(node, focus.Current);
  311. navigation.Move(focus.Current, NavigationDirection.Next);
  312. Assert.Same(button, focus.Current);
  313. navigation.Move(focus.Current, NavigationDirection.Next);
  314. Assert.Same(node, focus.Current);
  315. }
  316. }
  317. private void ApplyTemplates(TreeView tree)
  318. {
  319. tree.ApplyTemplate();
  320. tree.Presenter.ApplyTemplate();
  321. ApplyTemplates(tree.Presenter.Panel.Children);
  322. }
  323. private void ApplyTemplates(IEnumerable<IControl> controls)
  324. {
  325. foreach (TreeViewItem control in controls)
  326. {
  327. control.Template = CreateTreeViewItemTemplate();
  328. control.ApplyTemplate();
  329. control.Presenter.ApplyTemplate();
  330. control.HeaderPresenter.ApplyTemplate();
  331. ApplyTemplates(control.Presenter.Panel.Children);
  332. }
  333. }
  334. private IList<Node> CreateTestTreeData()
  335. {
  336. return new AvaloniaList<Node>
  337. {
  338. new Node
  339. {
  340. Value = "Root",
  341. Children = new AvaloniaList<Node>
  342. {
  343. new Node
  344. {
  345. Value = "Child1",
  346. },
  347. new Node
  348. {
  349. Value = "Child2",
  350. Children = new AvaloniaList<Node>
  351. {
  352. new Node
  353. {
  354. Value = "Grandchild2a",
  355. },
  356. },
  357. },
  358. new Node
  359. {
  360. Value = "Child3",
  361. },
  362. }
  363. }
  364. };
  365. }
  366. private void CreateNodeDataTemplate(IControl control)
  367. {
  368. control.DataTemplates.Add(new TestTreeDataTemplate());
  369. }
  370. private IControlTemplate CreateTreeViewTemplate()
  371. {
  372. return new FuncControlTemplate<TreeView>(parent => new ItemsPresenter
  373. {
  374. Name = "PART_ItemsPresenter",
  375. [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
  376. });
  377. }
  378. private IControlTemplate CreateTreeViewItemTemplate()
  379. {
  380. return new FuncControlTemplate<TreeViewItem>(parent => new Panel
  381. {
  382. Children =
  383. {
  384. new ContentPresenter
  385. {
  386. Name = "PART_HeaderPresenter",
  387. [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty],
  388. },
  389. new ItemsPresenter
  390. {
  391. Name = "PART_ItemsPresenter",
  392. [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
  393. }
  394. }
  395. });
  396. }
  397. private List<string> ExtractItemHeader(TreeView tree, int level)
  398. {
  399. return ExtractItemContent(tree.Presenter.Panel, 0, level)
  400. .Select(x => x.Header)
  401. .OfType<TextBlock>()
  402. .Select(x => x.Text)
  403. .ToList();
  404. }
  405. private IEnumerable<TreeViewItem> ExtractItemContent(IPanel panel, int currentLevel, int level)
  406. {
  407. foreach (TreeViewItem container in panel.Children)
  408. {
  409. if (container.Template == null)
  410. {
  411. container.Template = CreateTreeViewItemTemplate();
  412. container.ApplyTemplate();
  413. }
  414. if (currentLevel == level)
  415. {
  416. yield return container;
  417. }
  418. else
  419. {
  420. foreach (var child in ExtractItemContent(container.Presenter.Panel, currentLevel + 1, level))
  421. {
  422. yield return child;
  423. }
  424. }
  425. }
  426. }
  427. private class Node : NotifyingBase
  428. {
  429. private IAvaloniaList<Node> _children;
  430. public string Value { get; set; }
  431. public IAvaloniaList<Node> Children
  432. {
  433. get
  434. {
  435. return _children;
  436. }
  437. set
  438. {
  439. _children = value;
  440. RaisePropertyChanged(nameof(Children));
  441. }
  442. }
  443. }
  444. private class TestTreeDataTemplate : ITreeDataTemplate
  445. {
  446. public IControl Build(object param)
  447. {
  448. var node = (Node)param;
  449. return new TextBlock { Text = node.Value };
  450. }
  451. public bool SupportsRecycling => false;
  452. public InstancedBinding ItemsSelector(object item)
  453. {
  454. var obs = ExpressionObserver.Create(item, o => (o as Node).Children);
  455. return InstancedBinding.OneWay(obs);
  456. }
  457. public bool Match(object data)
  458. {
  459. return data is Node;
  460. }
  461. }
  462. }
  463. }