From 13cd835bc0b2b33e8a2884f21b91080d72c8126d Mon Sep 17 00:00:00 2001 From: adospace Date: Sat, 7 Nov 2020 00:00:18 +0100 Subject: [PATCH] ItemsControl+ItemVirtualizerSimple did not recreated item containers when Items or ItemTemplate were replaced --- global.json | 2 +- src/Avalonia.Controls/ItemsControl.cs | 6 +- .../Presenters/ItemVirtualizerSimple.cs | 4 + .../Presenters/ItemsPresenterBase.cs | 4 +- .../ItemsControlTests.cs | 92 ++++++++++++++ .../ListBoxTests.cs | 112 ++++++++++++++++++ 6 files changed, 217 insertions(+), 3 deletions(-) diff --git a/global.json b/global.json index b2b2da7c4f..684d9bed71 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.401" + "version": "3.1.301" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 4dc8aec6f3..f955df5f21 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -449,7 +449,11 @@ namespace Avalonia.Controls if (_itemContainerGenerator != null) { _itemContainerGenerator.ItemTemplate = (IDataTemplate)e.NewValue; - // TODO: Rebuild the item containers. + + if (e.OldValue != null && Presenter != null) + { + Presenter.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 7d50ef7d33..51dbc969a8 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -200,6 +200,10 @@ namespace Avalonia.Controls.Presenters break; case NotifyCollectionChangedAction.Reset: + Owner.ItemContainerGenerator.Clear(); + VirtualizingPanel.Children.Clear(); + FirstIndex = NextIndex = 0; + RecycleContainersOnRemove(); CreateAndRemoveContainers(); panel.ForceInvalidateMeasure(); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 52f173fc71..6c408bbed9 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -57,6 +57,8 @@ namespace Avalonia.Controls.Presenters set { + var itemsReplaced = (_items != value); + _itemsSubscription?.Dispose(); _itemsSubscription = null; @@ -67,7 +69,7 @@ namespace Avalonia.Controls.Presenters SetAndRaise(ItemsProperty, ref _items, value); - if (_createdPanel) + if (_createdPanel && itemsReplaced) { ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 684486cbae..157eefb84a 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -596,6 +596,98 @@ namespace Avalonia.Controls.UnitTests root.Child = target; } + [Fact] + public void Presenter_Items_Should_Be_In_Sync_When_Replacing_Items() + { + var target = new ItemsControl + { + Template = GetTemplate(), + Items = new[] + { + new Item("Item1") + } + }; + + var root = new TestRoot { Child = target }; + var otherPanel = new StackPanel(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + int dematerializedEventCallCount = 0; + target.ItemContainerGenerator.Dematerialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item1", ((Item)e.Containers[0].Item).Value); + dematerializedEventCallCount++; + }; + + int materializedEventCallCount = 0; + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item2", ((Item)e.Containers[0].Item).Value); + materializedEventCallCount++; + }; + + target.Items = new[] + { + new Item("Item2") + }; + + //Ensure that events are called one time only + Assert.Equal(1, dematerializedEventCallCount); + Assert.Equal(1, materializedEventCallCount); + } + + [Fact] + public void Presenter_Items_Should_Be_In_Sync_When_Replacing_ItemTemplate() + { + var target = new ItemsControl + { + Template = GetTemplate(), + Items = new[] + { + new Item("Item1") + }, + ItemTemplate = new FuncDataTemplate((x, ns) => new TextBlock()) + }; + + var root = new TestRoot { Child = target }; + var otherPanel = new StackPanel(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + int dematerializedEventCallCount = 0; + target.ItemContainerGenerator.Dematerialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item1", ((Item)e.Containers[0].Item).Value); + var contentPresenter = ((ContentPresenter)e.Containers[0].ContainerControl); + contentPresenter.UpdateChild(); + Assert.IsType(contentPresenter.Child); + dematerializedEventCallCount++; + }; + + int materializedEventCallCount = 0; + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item1", ((Item)e.Containers[0].Item).Value); + var contentPresenter = ((ContentPresenter)e.Containers[0].ContainerControl); + contentPresenter.UpdateChild(); + Assert.IsType(contentPresenter.Child); + materializedEventCallCount++; + }; + + target.ItemTemplate = + new FuncDataTemplate((x, ns) => new Canvas()); + + Assert.Equal(1, dematerializedEventCallCount); + Assert.Equal(1, materializedEventCallCount); + } + private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 145fce4fed..364cb01c65 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -454,6 +454,118 @@ namespace Avalonia.Controls.UnitTests } } + + [Fact] + public void ListBox_Presenter_Items_Should_Be_In_Sync_When_Replacing_Items() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; + + var target = new ListBox() + { + VerticalAlignment = Layout.VerticalAlignment.Top, + AutoScrollToSelectedItem = true, + Width = 50, + VirtualizationMode = ItemVirtualizationMode.Simple, + Items = new[] + { + new Item("Item1") + }, + }; + wnd.Content = target; + + var lm = wnd.LayoutManager; + + lm.ExecuteInitialLayoutPass(); + + int dematerializedEventCallCount = 0; + target.ItemContainerGenerator.Dematerialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item1", ((Item)e.Containers[0].Item).Value); + dematerializedEventCallCount++; + }; + + int materializedEventCallCount = 0; + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item2", ((Item)e.Containers[0].Item).Value); + materializedEventCallCount++; + }; + + target.Items = new[] + { + new Item("Item2") + }; + + //assert that materialize/dematerialize events are called exactly one time + Assert.Equal(1, dematerializedEventCallCount); + Assert.Equal(1, materializedEventCallCount); + } + } + + [Fact] + public void ListBox_Items_Should_Be_In_Sync_When_Replacing_ItemTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; + + var target = new ListBox() + { + VerticalAlignment = Layout.VerticalAlignment.Top, + AutoScrollToSelectedItem = true, + Width = 50, + VirtualizationMode = ItemVirtualizationMode.Simple, + Items = new[] + { + new Item("Item1") + }, + ItemTemplate = + new FuncDataTemplate((x, ns) => new Canvas()) + }; + + wnd.Content = target; + + var lm = wnd.LayoutManager; + + lm.ExecuteInitialLayoutPass(); + + int dematerializedEventCallCount = 0; + target.ItemContainerGenerator.Dematerialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item1", ((Item)e.Containers[0].Item).Value); + Assert.IsType(((ListBoxItem)e.Containers[0].ContainerControl).Presenter.Child); + dematerializedEventCallCount++; + }; + + int materializedEventCallCount = 0; + ListBoxItem materializedListBoxItem = null; + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + Assert.Equal("Item1", ((Item)e.Containers[0].Item).Value); + materializedListBoxItem = ((ListBoxItem)e.Containers[0].ContainerControl); + materializedEventCallCount++; + }; + + target.ItemTemplate = + new FuncDataTemplate((x, ns) => new TextBlock()); + + //ensure events are called only one time + Assert.Equal(1, dematerializedEventCallCount); + Assert.Equal(1, materializedEventCallCount); + + wnd.LayoutManager.ExecuteLayoutPass(); + + //ensure that new template has been applied + Assert.IsType(materializedListBoxItem.Presenter.Child); + } + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) =>