TreeViewTests.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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 LogicalChildren_Should_Be_Set()
  137. {
  138. var target = new TreeView
  139. {
  140. Template = CreateTreeViewTemplate(),
  141. Items = new[] { "Foo", "Bar", "Baz " },
  142. };
  143. ApplyTemplates(target);
  144. var result = target.GetLogicalChildren()
  145. .OfType<TreeViewItem>()
  146. .Select(x => x.Header)
  147. .OfType<TextBlock>()
  148. .Select(x => x.Text)
  149. .ToList();
  150. Assert.Equal(new[] { "Foo", "Bar", "Baz " }, result);
  151. }
  152. [Fact]
  153. public void Removing_Item_Should_Remove_Itself_And_Children_From_Index()
  154. {
  155. var tree = CreateTestTreeData();
  156. var target = new TreeView
  157. {
  158. Template = CreateTreeViewTemplate(),
  159. Items = tree,
  160. };
  161. var root = new TestRoot();
  162. root.Child = target;
  163. CreateNodeDataTemplate(target);
  164. ApplyTemplates(target);
  165. Assert.Equal(5, target.ItemContainerGenerator.Index.Items.Count());
  166. tree[0].Children.RemoveAt(1);
  167. Assert.Equal(3, target.ItemContainerGenerator.Index.Items.Count());
  168. }
  169. [Fact]
  170. public void DataContexts_Should_Be_Correctly_Set()
  171. {
  172. var items = new object[]
  173. {
  174. "Foo",
  175. new Node { Value = "Bar" },
  176. new TextBlock { Text = "Baz" },
  177. new TreeViewItem { Header = "Qux" },
  178. };
  179. var target = new TreeView
  180. {
  181. Template = CreateTreeViewTemplate(),
  182. DataContext = "Base",
  183. DataTemplates =
  184. {
  185. new FuncDataTemplate<Node>(x => new Button { Content = x })
  186. },
  187. Items = items,
  188. };
  189. ApplyTemplates(target);
  190. var dataContexts = target.Presenter.Panel.Children
  191. .Cast<Control>()
  192. .Select(x => x.DataContext)
  193. .ToList();
  194. Assert.Equal(
  195. new object[] { items[0], items[1], "Base", "Base" },
  196. dataContexts);
  197. }
  198. [Fact]
  199. public void Control_Item_Should_Not_Be_NameScope()
  200. {
  201. var items = new object[]
  202. {
  203. new TreeViewItem(),
  204. };
  205. var target = new TreeView
  206. {
  207. Template = CreateTreeViewTemplate(),
  208. Items = items,
  209. };
  210. target.ApplyTemplate();
  211. target.Presenter.ApplyTemplate();
  212. var item = target.Presenter.Panel.LogicalChildren[0];
  213. Assert.Null(NameScope.GetNameScope((TreeViewItem)item));
  214. }
  215. [Fact]
  216. public void DataTemplate_Created_Item_Should_Be_NameScope()
  217. {
  218. var items = new object[]
  219. {
  220. "foo",
  221. };
  222. var target = new TreeView
  223. {
  224. Template = CreateTreeViewTemplate(),
  225. Items = items,
  226. };
  227. target.ApplyTemplate();
  228. target.Presenter.ApplyTemplate();
  229. var item = target.Presenter.Panel.LogicalChildren[0];
  230. Assert.NotNull(NameScope.GetNameScope((TreeViewItem)item));
  231. }
  232. [Fact]
  233. public void Should_React_To_Children_Changing()
  234. {
  235. var data = CreateTestTreeData();
  236. var target = new TreeView
  237. {
  238. Template = CreateTreeViewTemplate(),
  239. Items = data,
  240. };
  241. CreateNodeDataTemplate(target);
  242. ApplyTemplates(target);
  243. Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0));
  244. Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1));
  245. Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2));
  246. // Make sure that the binding to Node.Children does not get collected.
  247. GC.Collect();
  248. data[0].Children = new AvaloniaList<Node>
  249. {
  250. new Node
  251. {
  252. Value = "NewChild1",
  253. }
  254. };
  255. Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0));
  256. Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1));
  257. }
  258. [Fact]
  259. public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node()
  260. {
  261. using (UnitTestApplication.Start(TestServices.RealFocus))
  262. {
  263. var focus = FocusManager.Instance;
  264. var navigation = AvaloniaLocator.Current.GetService<IKeyboardNavigationHandler>();
  265. var data = CreateTestTreeData();
  266. var target = new TreeView
  267. {
  268. Template = CreateTreeViewTemplate(),
  269. Items = data,
  270. };
  271. var button = new Button();
  272. var root = new TestRoot
  273. {
  274. Child = new StackPanel
  275. {
  276. Children = { target, button },
  277. }
  278. };
  279. CreateNodeDataTemplate(target);
  280. ApplyTemplates(target);
  281. var item = data[0].Children[0];
  282. var node = target.ItemContainerGenerator.Index.ContainerFromItem(item);
  283. Assert.NotNull(node);
  284. target.SelectedItem = item;
  285. node.Focus();
  286. Assert.Same(node, focus.Current);
  287. navigation.Move(focus.Current, NavigationDirection.Next);
  288. Assert.Same(button, focus.Current);
  289. navigation.Move(focus.Current, NavigationDirection.Next);
  290. Assert.Same(node, focus.Current);
  291. }
  292. }
  293. private void ApplyTemplates(TreeView tree)
  294. {
  295. tree.ApplyTemplate();
  296. tree.Presenter.ApplyTemplate();
  297. ApplyTemplates(tree.Presenter.Panel.Children);
  298. }
  299. private void ApplyTemplates(IEnumerable<IControl> controls)
  300. {
  301. foreach (TreeViewItem control in controls)
  302. {
  303. control.Template = CreateTreeViewItemTemplate();
  304. control.ApplyTemplate();
  305. control.Presenter.ApplyTemplate();
  306. control.HeaderPresenter.ApplyTemplate();
  307. ApplyTemplates(control.Presenter.Panel.Children);
  308. }
  309. }
  310. private IList<Node> CreateTestTreeData()
  311. {
  312. return new AvaloniaList<Node>
  313. {
  314. new Node
  315. {
  316. Value = "Root",
  317. Children = new AvaloniaList<Node>
  318. {
  319. new Node
  320. {
  321. Value = "Child1",
  322. },
  323. new Node
  324. {
  325. Value = "Child2",
  326. Children = new AvaloniaList<Node>
  327. {
  328. new Node
  329. {
  330. Value = "Grandchild2a",
  331. },
  332. },
  333. },
  334. new Node
  335. {
  336. Value = "Child3",
  337. },
  338. }
  339. }
  340. };
  341. }
  342. private void CreateNodeDataTemplate(IControl control)
  343. {
  344. control.DataTemplates.Add(new TestTreeDataTemplate());
  345. }
  346. private IControlTemplate CreateTreeViewTemplate()
  347. {
  348. return new FuncControlTemplate<TreeView>(parent => new ItemsPresenter
  349. {
  350. Name = "PART_ItemsPresenter",
  351. [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
  352. });
  353. }
  354. private IControlTemplate CreateTreeViewItemTemplate()
  355. {
  356. return new FuncControlTemplate<TreeViewItem>(parent => new Panel
  357. {
  358. Children =
  359. {
  360. new ContentPresenter
  361. {
  362. Name = "PART_HeaderPresenter",
  363. [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty],
  364. },
  365. new ItemsPresenter
  366. {
  367. Name = "PART_ItemsPresenter",
  368. [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
  369. }
  370. }
  371. });
  372. }
  373. private List<string> ExtractItemHeader(TreeView tree, int level)
  374. {
  375. return ExtractItemContent(tree.Presenter.Panel, 0, level)
  376. .Select(x => x.Header)
  377. .OfType<TextBlock>()
  378. .Select(x => x.Text)
  379. .ToList();
  380. }
  381. private IEnumerable<TreeViewItem> ExtractItemContent(IPanel panel, int currentLevel, int level)
  382. {
  383. foreach (TreeViewItem container in panel.Children)
  384. {
  385. if (container.Template == null)
  386. {
  387. container.Template = CreateTreeViewItemTemplate();
  388. container.ApplyTemplate();
  389. }
  390. if (currentLevel == level)
  391. {
  392. yield return container;
  393. }
  394. else
  395. {
  396. foreach (var child in ExtractItemContent(container.Presenter.Panel, currentLevel + 1, level))
  397. {
  398. yield return child;
  399. }
  400. }
  401. }
  402. }
  403. private class Node : NotifyingBase
  404. {
  405. private IAvaloniaList<Node> _children;
  406. public string Value { get; set; }
  407. public IAvaloniaList<Node> Children
  408. {
  409. get
  410. {
  411. return _children;
  412. }
  413. set
  414. {
  415. _children = value;
  416. RaisePropertyChanged(nameof(Children));
  417. }
  418. }
  419. }
  420. private class TestTreeDataTemplate : ITreeDataTemplate
  421. {
  422. public IControl Build(object param)
  423. {
  424. var node = (Node)param;
  425. return new TextBlock { Text = node.Value };
  426. }
  427. public bool SupportsRecycling => false;
  428. public InstancedBinding ItemsSelector(object item)
  429. {
  430. var obs = ExpressionObserver.Create(item, o => (o as Node).Children);
  431. return InstancedBinding.OneWay(obs);
  432. }
  433. public bool Match(object data)
  434. {
  435. return data is Node;
  436. }
  437. }
  438. }
  439. }