From b53b3d590f6db78df737c878b1b3391e0334cada Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 24 Sep 2018 13:08:48 +0300 Subject: [PATCH 1/4] add failing unit test for listbox issue #1936 --- .../ListBoxTests.cs | 108 +++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index a5f5f8d328..eb3a6cf0c0 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -1,16 +1,17 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; -using Avalonia.Input; +using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; -using Avalonia.Collections; namespace Avalonia.Controls.UnitTests { @@ -170,6 +171,109 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(100, 10), target.Scroll.Viewport); } + [Theory] + [InlineData(ItemVirtualizationMode.Simple)] + [InlineData(ItemVirtualizationMode.None)] + public void When_Added_Removed_AfterItems_Reset_Should_Work(ItemVirtualizationMode virtMode) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new ObservableCollection(); + + Action create = () => + { + foreach (var i in Enumerable.Range(1, 7)) + { + items.Add(i.ToString()); + } + }; + + create(); + + var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight }; + + wnd.IsVisible = true; + + var target = new ListBox() { VirtualizationMode = virtMode }; + + wnd.Content = target; + + var lm = wnd.LayoutManager; + + target.Height = 110;//working fine when <=106 or >=119 + target.Width = 50; + + target.ItemTemplate = new FuncDataTemplate(c => + { + var tb = new TextBlock() { Height = 10, Width = 30 }; + tb.Bind(TextBlock.TextProperty, new Binding()); + return tb; + }, true); + + target.DataContext = items; + + lm.ExecuteInitialLayoutPass(wnd); + + target.Bind(ItemsControl.ItemsProperty, new Binding()); + + lm.ExecuteLayoutPass(); + + var panel = target.Presenter.Panel; + + Func itemsToString = () => + string.Join(",", panel.Children.OfType().Select(l => l.Content.ToString()).ToArray()); + + Action addafter = (item, newitem) => + { + items.Insert(items.IndexOf(item) + 1, newitem); + + lm.ExecuteLayoutPass(); + }; + + Action remove = item => + { + items.Remove(item); + lm.ExecuteLayoutPass(); + }; + + addafter("1", "1+");//expected 1,1+,2,3,4,5,6,7 + + addafter("2", "2+");//expected 1,1+,2,2+,3,4,5,6 + + remove("2+");//expected 1,1+,2,3,4,5,6,7 + + //Reset items + items.Clear(); + create(); + + addafter("1", "1+");//expected 1,1+,2,3,4,5,6,7 + + addafter("2", "2+");//expected 1,1+,2,2+,3,4,5,6 + + remove("2+");//expected 1,1+,2,3,4,5,6,7 + + var sti = itemsToString(); + + var lbItems = panel.Children.OfType().ToArray(); + + Assert.Equal("1", lbItems[0].Content); + Assert.Equal("1+", lbItems[1].Content); + Assert.Equal("2", lbItems[2].Content); + Assert.Equal("3", lbItems[3].Content); //bug it's 2+ instead + Assert.Equal("4", lbItems[4].Content); + + int lbi = 0; + + //ensure all items are fine + foreach (var lb in lbItems) + { + Assert.Equal(items[lbi++], lb.Content); + } + + //Assert.Equal("1,1+,2,3,4,5,6,7", sti); + } + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate(parent => From a865f6dddfcc7bfee925af7bdfc407601ead504b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 6 Oct 2018 01:41:08 +0300 Subject: [PATCH 2/4] introduce local functions for listbox issue #1936 --- .../ListBoxTests.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index eb3a6cf0c0..c6968f19f8 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -180,13 +180,13 @@ namespace Avalonia.Controls.UnitTests { var items = new ObservableCollection(); - Action create = () => + void create() { foreach (var i in Enumerable.Range(1, 7)) { items.Add(i.ToString()); } - }; + } create(); @@ -220,21 +220,20 @@ namespace Avalonia.Controls.UnitTests var panel = target.Presenter.Panel; - Func itemsToString = () => - string.Join(",", panel.Children.OfType().Select(l => l.Content.ToString()).ToArray()); + string itemsToString() => + string.Join(",", panel.Children.OfType().Select(l => l.Content.ToString()).ToArray()); - Action addafter = (item, newitem) => - { - items.Insert(items.IndexOf(item) + 1, newitem); - - lm.ExecuteLayoutPass(); - }; + void addafter(string item, string newitem) + { + items.Insert(items.IndexOf(item) + 1, newitem); + lm.ExecuteLayoutPass(); + } - Action remove = item => + void remove(string item) { items.Remove(item); lm.ExecuteLayoutPass(); - }; + } addafter("1", "1+");//expected 1,1+,2,3,4,5,6,7 @@ -276,7 +275,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ListBoxTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate(parent => new ScrollViewer { Name = "PART_ScrollViewer", @@ -293,7 +292,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ListBoxItemTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate(parent => new ContentPresenter { Name = "PART_ContentPresenter", From 6efba9837db65f3a0eef0674a9277f1e345c1731 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 7 Oct 2018 00:59:50 +0200 Subject: [PATCH 3/4] Simplified test. Simplified @donandren's test for #1936. --- .../ListBoxTests.cs | 113 +++--------------- 1 file changed, 18 insertions(+), 95 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index c6968f19f8..1debccd3c5 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -171,106 +172,28 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(100, 10), target.Scroll.Viewport); } - [Theory] - [InlineData(ItemVirtualizationMode.Simple)] - [InlineData(ItemVirtualizationMode.None)] - public void When_Added_Removed_AfterItems_Reset_Should_Work(ItemVirtualizationMode virtMode) + [Fact] + public void Containers_Correct_After_Clear_Add_Remove() { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + // Issue #1936 + var items = new AvaloniaList(Enumerable.Range(0, 11).Select(x => $"Item {x}")); + var target = new ListBox { - var items = new ObservableCollection(); - - void create() - { - foreach (var i in Enumerable.Range(1, 7)) - { - items.Add(i.ToString()); - } - } - - create(); - - var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight }; - - wnd.IsVisible = true; - - var target = new ListBox() { VirtualizationMode = virtMode }; - - wnd.Content = target; - - var lm = wnd.LayoutManager; - - target.Height = 110;//working fine when <=106 or >=119 - target.Width = 50; - - target.ItemTemplate = new FuncDataTemplate(c => - { - var tb = new TextBlock() { Height = 10, Width = 30 }; - tb.Bind(TextBlock.TextProperty, new Binding()); - return tb; - }, true); - - target.DataContext = items; - - lm.ExecuteInitialLayoutPass(wnd); - - target.Bind(ItemsControl.ItemsProperty, new Binding()); - - lm.ExecuteLayoutPass(); - - var panel = target.Presenter.Panel; - - string itemsToString() => - string.Join(",", panel.Children.OfType().Select(l => l.Content.ToString()).ToArray()); - - void addafter(string item, string newitem) - { - items.Insert(items.IndexOf(item) + 1, newitem); - lm.ExecuteLayoutPass(); - } - - void remove(string item) - { - items.Remove(item); - lm.ExecuteLayoutPass(); - } - - addafter("1", "1+");//expected 1,1+,2,3,4,5,6,7 - - addafter("2", "2+");//expected 1,1+,2,2+,3,4,5,6 - - remove("2+");//expected 1,1+,2,3,4,5,6,7 - - //Reset items - items.Clear(); - create(); - - addafter("1", "1+");//expected 1,1+,2,3,4,5,6,7 - - addafter("2", "2+");//expected 1,1+,2,2+,3,4,5,6 - - remove("2+");//expected 1,1+,2,3,4,5,6,7 - - var sti = itemsToString(); - - var lbItems = panel.Children.OfType().ToArray(); - - Assert.Equal("1", lbItems[0].Content); - Assert.Equal("1+", lbItems[1].Content); - Assert.Equal("2", lbItems[2].Content); - Assert.Equal("3", lbItems[3].Content); //bug it's 2+ instead - Assert.Equal("4", lbItems[4].Content); + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectedIndex = 0, + }; - int lbi = 0; + Prepare(target); - //ensure all items are fine - foreach (var lb in lbItems) - { - Assert.Equal(items[lbi++], lb.Content); - } + items.Clear(); + items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}")); + items.Remove("Item 2"); - //Assert.Equal("1,1+,2,3,4,5,6,7", sti); - } + Assert.Equal( + items, + target.Presenter.Panel.Children.Cast().Select(x => (string)x.Content)); } private FuncControlTemplate ListBoxTemplate() From c608163fe99412bf7ce1da9d898e08eeebef3341 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 7 Oct 2018 01:00:18 +0200 Subject: [PATCH 4/4] Ensure containers are ordered correctly. Fixes #1936. --- src/Avalonia.Controls/Generators/ItemContainerGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 882d2f4ddd..f1a1f94a01 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private Dictionary _containers = new Dictionary(); + private SortedDictionary _containers = new SortedDictionary(); /// /// Initializes a new instance of the class. @@ -246,4 +246,4 @@ namespace Avalonia.Controls.Generators Recycled?.Invoke(this, e); } } -} \ No newline at end of file +}