From 2c8d8179e5e16f4dc54bfc1fb1223bbd5090ea4b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 14:01:53 +0200 Subject: [PATCH] Fix virtualized item selection. So that recycled items' selection state is set correctly. --- .../Generators/IItemContainerGenerator.cs | 6 +- .../Generators/ItemContainerEventArgs.cs | 7 +- .../Generators/ItemContainerGenerator.cs | 17 ++++- .../Generators/ItemContainerGenerator`1.cs | 3 +- .../Generators/TreeContainerIndex.cs | 4 +- src/Avalonia.Controls/ItemsControl.cs | 23 +++++++ .../Primitives/SelectingItemsControl.cs | 13 ++++ .../ListBoxTests.cs | 67 +++++++++++++------ 8 files changed, 110 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index b5ac8aef6e..ba584e33b9 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; using System.Collections.Generic; using Avalonia.Controls.Templates; @@ -33,6 +32,11 @@ namespace Avalonia.Controls.Generators /// event EventHandler Dematerialized; + /// + /// Event raised whenever containers are recycled. + /// + event EventHandler Recycled; + /// /// Creates a container control for an item. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs index cd26b0ba83..6821a842e3 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs @@ -15,13 +15,10 @@ namespace Avalonia.Controls.Generators /// /// Initializes a new instance of the class. /// - /// The index of the first container in the source items. /// The container. - public ItemContainerEventArgs( - int startingIndex, - ItemContainerInfo container) + public ItemContainerEventArgs(ItemContainerInfo container) { - StartingIndex = startingIndex; + StartingIndex = container.Index; Containers = new[] { container }; } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 801f237804..b5c176cf33 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -38,6 +38,9 @@ namespace Avalonia.Controls.Generators /// public event EventHandler Dematerialized; + /// + public event EventHandler Recycled; + /// /// Gets or sets the data template used to display the items in the control. /// @@ -58,7 +61,7 @@ namespace Avalonia.Controls.Generators var container = new ItemContainerInfo(CreateContainer(i), item, index); AddContainer(container); - Materialized?.Invoke(this, new ItemContainerEventArgs(index, container)); + Materialized?.Invoke(this, new ItemContainerEventArgs(container)); return container; } @@ -207,12 +210,13 @@ namespace Avalonia.Controls.Generators /// The new index. /// The new item. /// The container info. - protected void MoveContainer(int oldIndex, int newIndex, object item) + protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item) { var container = _containers[oldIndex]; var newContainer = new ItemContainerInfo(container.ContainerControl, item, newIndex); _containers[oldIndex] = null; AddContainer(newContainer); + return newContainer; } /// @@ -225,5 +229,14 @@ namespace Avalonia.Controls.Generators { return _containers.GetRange(index, count); } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseRecycled(ItemContainerEventArgs e) + { + Recycled?.Invoke(this, e); + } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 76922bfc55..c514e0e85f 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -93,7 +93,8 @@ namespace Avalonia.Controls.Generators container.DataContext = i; } - MoveContainer(oldIndex, newIndex, i); + var info = MoveContainer(oldIndex, newIndex, i); + RaiseRecycled(new ItemContainerEventArgs(info)); return true; } diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index f58f7e019d..e0c52beb11 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators Materialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); + new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } /// @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Generators Dematerialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); + new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index ef090bc697..8e8603ca24 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -89,6 +89,7 @@ namespace Avalonia.Controls _itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); + _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); } } @@ -264,6 +265,28 @@ namespace Avalonia.Controls LogicalChildren.RemoveAll(toRemove); } + /// + /// Called when containers are recycled for the by its + /// . + /// + /// The details of the containers. + protected virtual void OnContainersRecycled(ItemContainerEventArgs e) + { + var toRemove = new List(); + + foreach (var container in e.Containers) + { + // If the item is its own container, then it will be removed from the logical tree + // when it is removed from the Items collection. + if (container?.ContainerControl != container?.Item) + { + toRemove.Add(container.ContainerControl); + } + } + + LogicalChildren.RemoveAll(toRemove); + } + /// protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 018507bbc8..a46aa8d853 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -394,6 +394,19 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnContainersRecycled(ItemContainerEventArgs e) + { + foreach (var i in e.Containers) + { + if (i.ContainerControl != null && i.Item != null) + { + MarkContainerSelected( + i.ContainerControl, + SelectedItems.Contains(i.Item)); + } + } + } + /// protected override void OnDataContextChanging() { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index ab0d4030d5..d1443fb2ae 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate(_ => new Canvas()), }; @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), }; Prepare(target); @@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests var items = new[] { "Foo", "Bar", "Baz " }; var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), Items = items, }; @@ -76,7 +76,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), Items = new[] { "Foo", "Bar", "Baz " }, }; @@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), DataContext = "Base", DataTemplates = new DataTemplates { @@ -128,13 +128,37 @@ namespace Avalonia.Controls.UnitTests } } - private FuncControlTemplate CreateListBoxTemplate() + [Fact] + public void Selection_Should_Be_Cleared_On_Recycled_Items() + { + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + SelectedIndex = 0, + }; + + Prepare(target); + + // Make sure we're virtualized and first item is selected. + Assert.Equal(10, target.Presenter.Panel.Children.Count); + Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + + // Scroll down a page. + target.Scroll.Offset = new Vector(0, 10); + + // Make sure recycled item isn't now selected. + Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + } + + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate(parent => new ScrollViewer { Name = "PART_ScrollViewer", - Template = new FuncControlTemplate(CreateScrollViewerTemplate), + Template = ScrollViewerTemplate(), Content = new ItemsPresenter { Name = "PART_ItemsPresenter", @@ -146,21 +170,26 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ListBoxItemTemplate() { - return new FuncControlTemplate(parent => new ContentPresenter - { - Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], - [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty], - }); + return new FuncControlTemplate(parent => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], + [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty], + }); } - private Control CreateScrollViewerTemplate(ITemplatedControl parent) + private FuncControlTemplate ScrollViewerTemplate() { - return new ScrollContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty), - }; + return new FuncControlTemplate(parent => + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + }); } private void Prepare(ListBox target)