ItemsControlTests.cs 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.ObjectModel;
  5. using System.Collections.Specialized;
  6. using System.Linq;
  7. using Avalonia.Collections;
  8. using Avalonia.Controls.Presenters;
  9. using Avalonia.Controls.Primitives;
  10. using Avalonia.Controls.Templates;
  11. using Avalonia.Data;
  12. using Avalonia.Input;
  13. using Avalonia.Layout;
  14. using Avalonia.LogicalTree;
  15. using Avalonia.Markup.Xaml.Templates;
  16. using Avalonia.Styling;
  17. using Avalonia.UnitTests;
  18. using Avalonia.VisualTree;
  19. using Xunit;
  20. #nullable enable
  21. namespace Avalonia.Controls.UnitTests
  22. {
  23. public class ItemsControlTests
  24. {
  25. [Fact]
  26. public void Setting_ItemsSource_Should_Populate_Items()
  27. {
  28. using var app = Start();
  29. var target = CreateTarget(itemsSource: new[] { "foo", "bar" });
  30. Assert.NotSame(target.ItemsSource, target.Items);
  31. Assert.Equal(target.ItemsSource, target.Items);
  32. }
  33. [Fact]
  34. public void Cannot_Set_ItemsSource_With_Items_Present()
  35. {
  36. using var app = Start();
  37. var target = CreateTarget();
  38. target.Items.Add("foo");
  39. Assert.Throws<InvalidOperationException>(() => target.ItemsSource = new[] { "baz" });
  40. }
  41. [Fact]
  42. public void Cannot_Modify_Items_When_ItemsSource_Set()
  43. {
  44. using var app = Start();
  45. var target = CreateTarget(itemsSource: Array.Empty<string>());
  46. Assert.Throws<InvalidOperationException>(() => target.Items.Add("foo"));
  47. }
  48. [Fact]
  49. public void Should_Use_ItemTemplate_To_Create_Control()
  50. {
  51. using var app = Start();
  52. var target = CreateTarget(
  53. itemsSource: new[] { "Foo" },
  54. itemTemplate: new FuncDataTemplate<string>((_, __) => new Canvas()));
  55. var container = GetContainer(target);
  56. Assert.IsType<Canvas>(container.Child);
  57. }
  58. [Fact]
  59. public void ItemTemplate_Can_Be_Changed()
  60. {
  61. using var app = Start();
  62. var target = CreateTarget(
  63. itemsSource: new[] { "Foo" },
  64. itemTemplate: new FuncDataTemplate<string>((_, __) => new Canvas()));
  65. var container = GetContainer(target);
  66. Assert.IsType<Canvas>(container.Child);
  67. target.ItemTemplate = new FuncDataTemplate<string>((_, __) => new Border());
  68. Layout(target);
  69. container = GetContainer(target);
  70. Assert.IsType<Border>(container.Child);
  71. }
  72. [Fact]
  73. public void Panel_Should_Have_TemplatedParent_Set_To_ItemsControl()
  74. {
  75. using var app = Start();
  76. var target = CreateTarget(itemsSource: new[] { "Foo" });
  77. Assert.Equal(target, target.ItemsPanelRoot?.TemplatedParent);
  78. }
  79. [Fact]
  80. public void Panel_Should_Have_ItemsHost_Set_To_True()
  81. {
  82. using var app = Start();
  83. var target = CreateTarget(itemsSource: new[] { "Foo" });
  84. Assert.True(target.ItemsPanelRoot?.IsItemsHost);
  85. }
  86. [Fact]
  87. public void Container_Should_Have_TemplatedParent_Set_To_Null()
  88. {
  89. using var app = Start();
  90. var target = CreateTarget(itemsSource: new[] { "Foo" });
  91. var container = GetContainer(target);
  92. Assert.Null(container.TemplatedParent);
  93. }
  94. [Fact]
  95. public void Container_Should_Have_Theme_Set_To_ItemContainerTheme()
  96. {
  97. using var app = Start();
  98. var theme = new ControlTheme { TargetType = typeof(ContentPresenter) };
  99. var target = CreateTarget(
  100. itemsSource: new[] { "Foo" },
  101. itemContainerTheme: theme);
  102. var container = GetContainer(target);
  103. Assert.Same(container.Theme, theme);
  104. }
  105. [Fact]
  106. public void Container_Should_Have_LogicalParent_Set_To_ItemsControl()
  107. {
  108. using var app = UnitTestApplication.Start(TestServices.StyledWindow);
  109. var target = new ItemsControl();
  110. var root = CreateRoot(target);
  111. var templatedParent = new Button();
  112. target.TemplatedParent = templatedParent;
  113. target.Template = CreateItemsControlTemplate();
  114. target.ItemsSource = new[] { "Foo" };
  115. root.LayoutManager.ExecuteInitialLayoutPass();
  116. var container = GetContainer(target);
  117. Assert.Equal(target, container.Parent);
  118. }
  119. [Fact]
  120. public void Control_Item_Should_Be_Logical_Child_Before_ApplyTemplate()
  121. {
  122. using var app = Start();
  123. var child = new Control();
  124. var target = CreateTarget(items: new[] { child }, performLayout: false);
  125. Assert.False(target.IsMeasureValid);
  126. Assert.Empty(target.GetVisualChildren());
  127. Assert.Equal(child.Parent, target);
  128. Assert.Equal(child.GetLogicalParent(), target);
  129. Assert.Equal(new[] { child }, target.GetLogicalChildren());
  130. }
  131. [Fact]
  132. public void Control_Item_Should_Be_Logical_Child_After_Layout()
  133. {
  134. using var app = Start();
  135. var child = new Control();
  136. var target = CreateTarget(items: new[] { child });
  137. Assert.True(target.IsMeasureValid);
  138. Assert.Single(target.GetVisualChildren());
  139. Assert.Equal(target, child.Parent);
  140. Assert.Equal(target, child.GetLogicalParent());
  141. Assert.Equal(new[] { child }, target.GetLogicalChildren());
  142. }
  143. [Fact]
  144. public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl()
  145. {
  146. using var app = Start();
  147. var items = new ObservableCollection<Border>();
  148. var target = CreateTarget(itemsSource: items);
  149. var item = new Border();
  150. items.Add(item);
  151. Assert.Equal(target, item.Parent);
  152. }
  153. [Fact]
  154. public void Control_Item_Can_Be_Removed_From_Logical_Children_Before_ApplyTemplate()
  155. {
  156. using var app = Start();
  157. var child = new Control();
  158. var target = CreateTarget(items: new[] { child }, performLayout: false);
  159. Assert.False(target.IsMeasureValid);
  160. Assert.Empty(target.GetVisualChildren());
  161. Assert.Single(target.GetLogicalChildren());
  162. target.Items.RemoveAt(0);
  163. Assert.Null(child.Parent);
  164. Assert.Null(child.GetLogicalParent());
  165. Assert.Empty(target.GetLogicalChildren());
  166. }
  167. [Fact]
  168. public void Clearing_Items_Should_Clear_Child_Controls_Parent_Before_ApplyTemplate()
  169. {
  170. using var app = Start();
  171. var child = new Control();
  172. var target = CreateTarget(items: new[] { child }, performLayout: false);
  173. Assert.False(target.IsMeasureValid);
  174. Assert.Empty(target.GetVisualChildren());
  175. Assert.Single(target.GetLogicalChildren());
  176. target.Items.Clear();
  177. Assert.Null(child.Parent);
  178. Assert.Null(child.GetLogicalParent());
  179. }
  180. [Fact]
  181. public void Assigning_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate()
  182. {
  183. using var app = Start();
  184. var child = new Control();
  185. var target = CreateTarget(itemsSource: new[] { child }, performLayout: false);
  186. var called = false;
  187. ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
  188. var list = new AvaloniaList<Control>(new[] { child });
  189. target.ItemsSource = list;
  190. Assert.False(called);
  191. }
  192. [Fact]
  193. public void Removing_ItemsSource_Items_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate()
  194. {
  195. using var app = Start();
  196. var items = new AvaloniaList<string> { "Foo", "Bar" };
  197. var target = CreateTarget(itemsSource: items, performLayout: false);
  198. var called = false;
  199. ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
  200. items.Remove("Bar");
  201. Assert.False(called);
  202. }
  203. [Fact]
  204. public void Changing_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate()
  205. {
  206. using var app = Start();
  207. var child = new Control();
  208. var target = CreateTarget(itemsSource: new[] { child }, performLayout: false);
  209. var called = false;
  210. ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true;
  211. var list = new AvaloniaList<Control>();
  212. target.ItemsSource = list;
  213. list.Add(child);
  214. Assert.False(called);
  215. }
  216. [Fact]
  217. public void Clearing_Items_Should_Clear_Child_Controls_Parent()
  218. {
  219. using var app = Start();
  220. var child = new Control();
  221. var target = CreateTarget(items: new[] { child });
  222. target.Items.Clear();
  223. Assert.Null(child.Parent);
  224. Assert.Null(((ILogical)child).LogicalParent);
  225. }
  226. [Fact]
  227. public void Adding_Control_Item_Should_Make_Control_Appear_In_LogicalChildren()
  228. {
  229. using var app = Start();
  230. var child = new Control();
  231. var target = CreateTarget(items: new[] { child }, performLayout: false);
  232. // Should appear both before and after applying template.
  233. Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren());
  234. Layout(target);
  235. Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren());
  236. }
  237. [Fact]
  238. public void Adding_String_Item_Should_Make_ContentPresenter_Appear_In_LogicalChildren()
  239. {
  240. using var app = Start();
  241. var target = CreateTarget(itemsSource: new[] { "Foo " });
  242. var logical = (ILogical)target;
  243. Assert.Equal(1, logical.LogicalChildren.Count);
  244. Assert.IsType<ContentPresenter>(logical.LogicalChildren[0]);
  245. }
  246. [Fact]
  247. public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged()
  248. {
  249. using var app = Start();
  250. var target = CreateTarget();
  251. var called = false;
  252. target.Template = CreateItemsControlTemplate();
  253. target.ApplyTemplate();
  254. ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
  255. called = e.Action == NotifyCollectionChangedAction.Add;
  256. var child = new Control();
  257. target.Items.Add(child);
  258. Assert.True(called);
  259. }
  260. [Fact]
  261. public void Clearing_Items_Should_Fire_LogicalChildren_CollectionChanged()
  262. {
  263. using var app = Start();
  264. var child = new Control();
  265. var target = CreateTarget(items: new[] { child });
  266. var called = false;
  267. ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) =>
  268. called = e.Action == NotifyCollectionChangedAction.Remove;
  269. target.Items.Clear();
  270. Assert.True(called);
  271. }
  272. [Fact]
  273. public void LogicalChildren_Should_Not_Change_Instance_When_Template_Changed()
  274. {
  275. using var app = Start();
  276. var target = CreateTarget();
  277. var before = ((ILogical)target).LogicalChildren;
  278. target.Template = null;
  279. target.Template = CreateItemsControlTemplate();
  280. Layout(target);
  281. var after = ((ILogical)target).LogicalChildren;
  282. Assert.NotNull(before);
  283. Assert.NotNull(after);
  284. Assert.Same(before, after);
  285. }
  286. [Fact]
  287. public void Should_Clear_Containers_When_ItemsPresenter_Changes()
  288. {
  289. using var app = Start();
  290. var target = CreateTarget(itemsSource: new[] { "foo", "bar" });
  291. var panel = Assert.IsAssignableFrom<Panel>(target.Presenter?.Panel);
  292. Assert.Equal(2, panel.Children.Count());
  293. target.Template = CreateItemsControlTemplate();
  294. target.ApplyTemplate();
  295. Assert.Empty(panel.Children);
  296. }
  297. [Fact]
  298. public void Empty_Class_Should_Initially_Be_Applied()
  299. {
  300. using var app = Start();
  301. var target = CreateTarget(performLayout: false);
  302. Assert.Contains(":empty", target.Classes);
  303. }
  304. [Fact]
  305. public void Empty_Class_Should_Be_Cleared_When_Items_Added()
  306. {
  307. using var app = Start();
  308. var target = CreateTarget(items: new[] { 1, 2, 3 }, performLayout: false);
  309. Assert.DoesNotContain(":empty", target.Classes);
  310. }
  311. [Fact]
  312. public void Empty_Class_Should_Be_Cleared_When_ItemsSource_Items_Added()
  313. {
  314. using var app = Start();
  315. var target = CreateTarget(itemsSource: new[] { 1, 2, 3 }, performLayout: false);
  316. Assert.DoesNotContain(":empty", target.Classes);
  317. }
  318. [Fact]
  319. public void Empty_Class_Should_Be_Set_When_ItemsSource_Collection_Cleared()
  320. {
  321. using var app = Start();
  322. var target = CreateTarget(itemsSource: new[] { 1, 2, 3 });
  323. target.ItemsSource = new int[0];
  324. Assert.Contains(":empty", target.Classes);
  325. }
  326. [Fact]
  327. public void Item_Count_Should_Be_Set_When_ItemsSource_Set()
  328. {
  329. using var app = Start();
  330. var target = CreateTarget(itemsSource: new[] { 1, 2, 3 });
  331. Assert.Equal(3, target.ItemCount);
  332. }
  333. [Fact]
  334. public void Item_Count_Should_Be_Set_When_Items_Changed()
  335. {
  336. using var app = Start();
  337. var items = new ObservableCollection<int>() { 1, 2, 3 };
  338. var target = CreateTarget(items: new[] { 1, 2, 3 });
  339. target.Items.Add(4);
  340. Assert.Equal(4, target.ItemCount);
  341. target.Items.Clear();
  342. Assert.Equal(0, target.ItemCount);
  343. }
  344. [Fact]
  345. public void Item_Count_Should_Be_Set_When_ItemsSource_Items_Changed()
  346. {
  347. using var app = Start();
  348. var items = new ObservableCollection<int>() { 1, 2, 3 };
  349. var target = CreateTarget(itemsSource: items);
  350. items.Add(4);
  351. Assert.Equal(4, target.ItemCount);
  352. items.Clear();
  353. Assert.Equal(0, target.ItemCount);
  354. }
  355. [Fact]
  356. public void Empty_Class_Should_Be_Set_When_Items_Collection_Cleared()
  357. {
  358. using var app = Start();
  359. var items = new ObservableCollection<int>() { 1, 2, 3 };
  360. var target = CreateTarget(itemsSource: items);
  361. items.Clear();
  362. Assert.Contains(":empty", target.Classes);
  363. }
  364. [Fact]
  365. public void Empty_Class_Should_Not_Be_Set_When_ItemsSource_Collection_Count_Increases()
  366. {
  367. using var app = Start();
  368. var items = new ObservableCollection<int>() { };
  369. var target = CreateTarget(itemsSource: items);
  370. items.Add(1);
  371. Assert.DoesNotContain(":empty", target.Classes);
  372. }
  373. [Fact]
  374. public void Single_Item_Class_Should_Be_Set_When_ItemsSource_Collection_Count_Increases_To_One()
  375. {
  376. using var app = Start();
  377. var items = new ObservableCollection<int>() { };
  378. var target = CreateTarget(itemsSource: items);
  379. items.Add(1);
  380. Assert.Contains(":singleitem", target.Classes);
  381. }
  382. [Fact]
  383. public void Empty_Class_Should_Not_Be_Set_When_ItemsSource_Collection_Cleared()
  384. {
  385. using var app = Start();
  386. var items = new ObservableCollection<int>() { 1, 2, 3 };
  387. var target = CreateTarget(itemsSource: items);
  388. items.Clear();
  389. Assert.DoesNotContain(":singleitem", target.Classes);
  390. }
  391. [Fact]
  392. public void Single_Item_Class_Should_Not_Be_Set_When_Items_Collection_Count_Increases_Beyond_One()
  393. {
  394. using var app = Start();
  395. var items = new ObservableCollection<int>() { 1 };
  396. var target = CreateTarget(itemsSource: items);
  397. items.Add(2);
  398. Assert.DoesNotContain(":singleitem", target.Classes);
  399. }
  400. [Fact]
  401. public void DataContexts_Should_Be_Correctly_Set()
  402. {
  403. using var app = Start();
  404. var items = new object[]
  405. {
  406. "Foo",
  407. new Item("Bar"),
  408. new TextBlock { Text = "Baz" },
  409. new ListBoxItem { Content = "Qux" },
  410. };
  411. var dataTemplate = new FuncDataTemplate<Item>((x, __) => new Button { Content = x });
  412. var target = CreateTarget(
  413. dataContext: "Base",
  414. itemsSource: items,
  415. dataTemplates: new[] { dataTemplate });
  416. var panel = Assert.IsAssignableFrom<Panel>(target.ItemsPanelRoot);
  417. var dataContexts = panel.Children
  418. .Do(x => (x as ContentPresenter)?.UpdateChild())
  419. .Cast<Control>()
  420. .Select(x => x.DataContext)
  421. .ToList();
  422. Assert.Equal(
  423. new object[] { items[0], items[1], "Base", "Base" },
  424. dataContexts);
  425. }
  426. [Fact]
  427. public void Control_Item_Should_Not_Be_NameScope()
  428. {
  429. using var app = Start();
  430. var items = new object[] { new TextBlock() };
  431. var target = CreateTarget(itemsSource: items);
  432. var item = target.LogicalChildren[0];
  433. Assert.Null(NameScope.GetNameScope((TextBlock)item));
  434. }
  435. [Fact]
  436. public void Focuses_Next_Item_On_Key_Down()
  437. {
  438. using var app = Start();
  439. var items = new object[]
  440. {
  441. new Button(),
  442. new Button(),
  443. };
  444. var target = CreateTarget(itemsSource: items);
  445. GetContainer<Button>(target).Focus();
  446. target.RaiseEvent(new KeyEventArgs
  447. {
  448. RoutedEvent = InputElement.KeyDownEvent,
  449. Key = Key.Down,
  450. });
  451. var panel = Assert.IsAssignableFrom<Panel>(target.ItemsPanelRoot);
  452. Assert.Equal(panel.Children[1], FocusManager.Instance!.Current);
  453. }
  454. [Fact]
  455. public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down()
  456. {
  457. using var app = Start();
  458. var items = new object[]
  459. {
  460. new Button(),
  461. new Button { Focusable = false },
  462. new Button(),
  463. };
  464. var target = CreateTarget(itemsSource: items);
  465. GetContainer<Button>(target).Focus();
  466. target.RaiseEvent(new KeyEventArgs
  467. {
  468. RoutedEvent = InputElement.KeyDownEvent,
  469. Key = Key.Down,
  470. });
  471. var panel = Assert.IsAssignableFrom<Panel>(target.ItemsPanelRoot);
  472. Assert.Equal(panel.Children[2], FocusManager.Instance!.Current);
  473. }
  474. [Fact]
  475. public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw()
  476. {
  477. // # Issue 3487
  478. using var app = Start();
  479. var target = CreateTarget(
  480. itemsSource: new[] { "foo", "bar" },
  481. itemTemplate: new FuncDataTemplate<string>((_, __) => new Canvas()));
  482. var root = Assert.IsType<TestRoot>(target.GetVisualRoot());
  483. root.Child = null;
  484. root.Child = target;
  485. root.LayoutManager.ExecuteLayoutPass();
  486. root.Child = null;
  487. root.Child = target;
  488. }
  489. [Fact]
  490. public void Should_Use_DisplayMemberBinding()
  491. {
  492. using var app = Start();
  493. var target = CreateTarget(
  494. itemsSource: new[] { "Foo" },
  495. displayMemberBinding: new Binding("Length"));
  496. var container = GetContainer(target);
  497. var textBlock = Assert.IsType<TextBlock>(container.Child);
  498. Assert.Equal(textBlock.Text, "3");
  499. }
  500. [Fact]
  501. public void DisplayMemberBinding_Can_Be_Changed()
  502. {
  503. using var app = Start();
  504. var target = CreateTarget(
  505. itemsSource: new[] { new Item("Foo", "Bar") },
  506. displayMemberBinding: new Binding("Value"));
  507. var container = GetContainer(target);
  508. var textBlock = Assert.IsType<TextBlock>(container.Child);
  509. Assert.Equal(textBlock.Text, "Bar");
  510. target.DisplayMemberBinding = new Binding("Caption");
  511. Layout(target);
  512. container = GetContainer(target);
  513. textBlock = Assert.IsType<TextBlock>(container.Child);
  514. Assert.Equal(textBlock.Text, "Foo");
  515. }
  516. [Fact]
  517. public void Cannot_Set_Both_DisplayMemberBinding_And_ItemTemplate_1()
  518. {
  519. using var app = Start();
  520. var target = CreateTarget(
  521. displayMemberBinding: new Binding("Length"));
  522. Assert.Throws<InvalidOperationException>(() =>
  523. target.ItemTemplate = new FuncDataTemplate<string>((_, _) => new TextBlock()));
  524. }
  525. [Fact]
  526. public void Cannot_Set_Both_DisplayMemberBinding_And_ItemTemplate_2()
  527. {
  528. using var app = Start();
  529. var target = CreateTarget(
  530. itemTemplate: new FuncDataTemplate<string>((_, _) => new TextBlock()));
  531. Assert.Throws<InvalidOperationException>(() => target.DisplayMemberBinding = new Binding("Length"));
  532. }
  533. [Fact]
  534. public void ContainerPrepared_Is_Raised_For_Each_Control_Item_Container()
  535. {
  536. using var app = Start();
  537. var items = new AvaloniaList<string>();
  538. var target = CreateTarget();
  539. var result = new List<Control>();
  540. var index = 0;
  541. target.ContainerPrepared += (s, e) =>
  542. {
  543. Assert.Equal(index++, e.Index);
  544. result.Add(e.Container);
  545. };
  546. target.Items.Add(new Button());
  547. target.Items.Add(new Button());
  548. target.Items.Add(new Button());
  549. Assert.Equal(3, result.Count);
  550. Assert.Equal(target.GetRealizedContainers(), result);
  551. }
  552. [Fact]
  553. public void ContainerPrepared_Is_Raised_For_Each_Item_Container()
  554. {
  555. using var app = Start();
  556. var items = new AvaloniaList<string>();
  557. var target = CreateTarget();
  558. var result = new List<Control>();
  559. var index = 0;
  560. target.ContainerPrepared += (s, e) =>
  561. {
  562. Assert.Equal(index++, e.Index);
  563. result.Add(e.Container);
  564. };
  565. target.Items.Add("Foo");
  566. target.Items.Add("Bar");
  567. target.Items.Add("Baz");
  568. Assert.Equal(3, result.Count);
  569. Assert.Equal(target.GetRealizedContainers(), result);
  570. }
  571. [Fact]
  572. public void ContainerPrepared_Is_Raised_For_Each_ItemsSource_Item_Container_On_Layout()
  573. {
  574. using var app = Start();
  575. var items = new AvaloniaList<string>();
  576. var target = CreateTarget(itemsSource: items);
  577. var result = new List<Control>();
  578. var index = 0;
  579. target.ContainerPrepared += (s, e) =>
  580. {
  581. Assert.Equal(index++, e.Index);
  582. result.Add(e.Container);
  583. };
  584. items.AddRange(new[] { "Foo", "Bar", "Baz" });
  585. Assert.Equal(3, result.Count);
  586. Assert.Equal(target.GetRealizedContainers(), result);
  587. }
  588. [Fact]
  589. public void ContainerIndexChanged_Is_Raised_When_Item_Added()
  590. {
  591. using var app = Start();
  592. var target = CreateTarget(items: new[] { "Foo", "Bar", "Baz" });
  593. var result = new List<Control>();
  594. var index = 1;
  595. target.ContainerIndexChanged += (s, e) =>
  596. {
  597. Assert.Equal(index++, e.OldIndex);
  598. Assert.Equal(index, e.NewIndex);
  599. result.Add(e.Container);
  600. };
  601. target.Items.Insert(1, "Qux");
  602. Assert.Equal(2, result.Count);
  603. Assert.Equal(target.GetRealizedContainers().Skip(2), result);
  604. }
  605. [Fact]
  606. public void ContainerClearing_Is_Raised_When_Item_Removed()
  607. {
  608. using var app = Start();
  609. var target = CreateTarget(items: new[] { "Foo", "Bar", "Baz" });
  610. var expected = target.ContainerFromIndex(1);
  611. var raised = 0;
  612. target.ContainerClearing += (s, e) =>
  613. {
  614. Assert.Same(expected, e.Container);
  615. ++raised;
  616. };
  617. target.Items.RemoveAt(1);
  618. Assert.Equal(1, raised);
  619. }
  620. [Fact]
  621. public void Handles_Recycling_Control_Items_Inside_Containers()
  622. {
  623. // Issue #10825
  624. using var app = Start();
  625. // The items must be controls but not of the container type.
  626. var items = Enumerable.Range(0, 100).Select(x => new TextBlock
  627. {
  628. Text = $"Item {x}",
  629. Width = 100,
  630. Height = 100,
  631. }).ToList();
  632. // Virtualization is required
  633. var itemsPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel());
  634. // Create an ItemsControl which uses containers, and provide a scroll viewer.
  635. var target = CreateTarget<ItemsControlWithContainer>(
  636. items: items,
  637. itemsPanel: itemsPanel,
  638. scrollViewer: true);
  639. var scroll = target.FindAncestorOfType<ScrollViewer>();
  640. Assert.NotNull(scroll);
  641. Assert.Equal(10, target.GetRealizedContainers().Count());
  642. // Scroll so that half a container is visible: an extra container is generated.
  643. scroll.Offset = new(0, 2050);
  644. Layout(target);
  645. // Scroll so that the extra container is no longer needed and recycled.
  646. scroll.Offset = new(0, 2100);
  647. Layout(target);
  648. // Scroll back: issue #10825 triggered.
  649. scroll.Offset = new(0, 2000);
  650. Layout(target);
  651. }
  652. [Fact]
  653. public void ItemIsOwnContainer_Content_Should_Not_Be_Cleared_When_Removed()
  654. {
  655. // Issue #11128.
  656. using var app = Start();
  657. var item = new ContentPresenter { Content = "foo" };
  658. var target = CreateTarget(items: new[] { item });
  659. target.Items.RemoveAt(0);
  660. Assert.Equal("foo", item.Content);
  661. }
  662. private static ItemsControl CreateTarget(
  663. object? dataContext = null,
  664. IBinding? displayMemberBinding = null,
  665. IList? items = null,
  666. IList? itemsSource = null,
  667. ControlTheme? itemContainerTheme = null,
  668. IDataTemplate? itemTemplate = null,
  669. IEnumerable<IDataTemplate>? dataTemplates = null,
  670. bool performLayout = true)
  671. {
  672. return CreateTarget<ItemsControl>(
  673. dataContext: dataContext,
  674. displayMemberBinding: displayMemberBinding,
  675. items: items,
  676. itemsSource: itemsSource,
  677. itemContainerTheme: itemContainerTheme,
  678. itemTemplate: itemTemplate,
  679. dataTemplates: dataTemplates,
  680. performLayout: performLayout);
  681. }
  682. private static T CreateTarget<T>(
  683. object? dataContext = null,
  684. IBinding? displayMemberBinding = null,
  685. IList? items = null,
  686. IList? itemsSource = null,
  687. ControlTheme? itemContainerTheme = null,
  688. IDataTemplate? itemTemplate = null,
  689. ITemplate<Panel?>? itemsPanel = null,
  690. IEnumerable<IDataTemplate>? dataTemplates = null,
  691. bool performLayout = true,
  692. bool scrollViewer = false)
  693. where T : ItemsControl, new()
  694. {
  695. var target = new T
  696. {
  697. DataContext = dataContext,
  698. DisplayMemberBinding = displayMemberBinding,
  699. ItemContainerTheme = itemContainerTheme,
  700. ItemTemplate = itemTemplate,
  701. ItemsSource = itemsSource,
  702. };
  703. if (items is not null)
  704. {
  705. foreach (var item in items)
  706. target.Items.Add(item);
  707. }
  708. if (itemsPanel is not null)
  709. target.ItemsPanel = itemsPanel;
  710. var scroll = scrollViewer ? new ScrollViewer { Content = target } : null;
  711. var root = CreateRoot(scroll ?? (Control)target);
  712. if (dataTemplates is not null)
  713. {
  714. foreach (var dataTemplate in dataTemplates)
  715. root.DataTemplates.Add(dataTemplate);
  716. }
  717. if (performLayout)
  718. root.LayoutManager.ExecuteInitialLayoutPass();
  719. return target;
  720. }
  721. private static TestRoot CreateRoot(Control child)
  722. {
  723. return new TestRoot
  724. {
  725. Resources =
  726. {
  727. { typeof(ContentControl), CreateContentControlTheme() },
  728. { typeof(ItemsControl), CreateItemsControlTheme() },
  729. { typeof(ScrollViewer), CreateScrollViewerTheme() },
  730. },
  731. Child = child,
  732. };
  733. }
  734. private static ControlTheme CreateContentControlTheme()
  735. {
  736. return new ControlTheme(typeof(ContentControl))
  737. {
  738. Setters =
  739. {
  740. new Setter(TreeView.TemplateProperty, CreateContentControlTemplate()),
  741. },
  742. };
  743. }
  744. private static FuncControlTemplate CreateContentControlTemplate()
  745. {
  746. return new FuncControlTemplate<ContentControl>((parent, scope) =>
  747. new ContentPresenter
  748. {
  749. Name = "PART_ContentPresenter",
  750. [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
  751. [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
  752. }.RegisterInNameScope(scope));
  753. }
  754. private static ControlTheme CreateItemsControlTheme()
  755. {
  756. return new ControlTheme(typeof(ItemsControl))
  757. {
  758. Setters =
  759. {
  760. new Setter(TreeView.TemplateProperty, CreateItemsControlTemplate()),
  761. },
  762. };
  763. }
  764. private static FuncControlTemplate CreateItemsControlTemplate()
  765. {
  766. return new FuncControlTemplate<ItemsControl>((parent, scope) =>
  767. {
  768. return new Border
  769. {
  770. Background = new Media.SolidColorBrush(0xffffffff),
  771. Child = new ItemsPresenter
  772. {
  773. Name = "PART_ItemsPresenter",
  774. [~ItemsPresenter.ItemsPanelProperty] = parent[~ItemsControl.ItemsPanelProperty],
  775. }.RegisterInNameScope(scope)
  776. };
  777. });
  778. }
  779. private static ControlTheme CreateScrollViewerTheme()
  780. {
  781. return new ControlTheme(typeof(ScrollViewer))
  782. {
  783. Setters =
  784. {
  785. new Setter(TreeView.TemplateProperty, CreateScrollViewerTemplate()),
  786. },
  787. };
  788. }
  789. private static FuncControlTemplate CreateScrollViewerTemplate()
  790. {
  791. return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
  792. new Panel
  793. {
  794. Children =
  795. {
  796. new ScrollContentPresenter
  797. {
  798. Name = "PART_ContentPresenter",
  799. [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
  800. }.RegisterInNameScope(scope),
  801. new ScrollBar
  802. {
  803. Name = "verticalScrollBar",
  804. }
  805. }
  806. });
  807. }
  808. private static void Layout(Control c)
  809. {
  810. (c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass();
  811. }
  812. private static ContentPresenter GetContainer(ItemsControl target, int index = 0)
  813. {
  814. return Assert.IsType<ContentPresenter>(target.GetRealizedContainers().ElementAt(index));
  815. }
  816. private static T GetContainer<T>(ItemsControl target, int index = 0)
  817. {
  818. return Assert.IsType<T>(target.GetRealizedContainers().ElementAt(index));
  819. }
  820. public static IDisposable Start()
  821. {
  822. return UnitTestApplication.Start(
  823. TestServices.MockThreadingInterface.With(
  824. focusManager: new FocusManager(),
  825. fontManagerImpl: new MockFontManagerImpl(),
  826. keyboardDevice: () => new KeyboardDevice(),
  827. keyboardNavigation: new KeyboardNavigationHandler(),
  828. inputManager: new InputManager(),
  829. renderInterface: new MockPlatformRenderInterface(),
  830. textShaperImpl: new MockTextShaperImpl()));
  831. }
  832. private class ItemsControlWithContainer : ItemsControl, IStyleable
  833. {
  834. Type IStyleable.StyleKey => typeof(ItemsControl);
  835. protected internal override Control CreateContainerForItemOverride()
  836. {
  837. return new ContainerControl();
  838. }
  839. protected internal override bool IsItemItsOwnContainerOverride(Control item)
  840. {
  841. return item is ContainerControl;
  842. }
  843. }
  844. private class ContainerControl : ContentControl, IStyleable
  845. {
  846. Type IStyleable.StyleKey => typeof(ContentControl);
  847. }
  848. private record Item(string Caption, string? Value = null);
  849. }
  850. }