ItemsControlTests.cs 35 KB

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