Browse Source

Fix virtualized item selection.

So that recycled items' selection state is set correctly.
Steven Kirk 9 years ago
parent
commit
2c8d8179e5

+ 5 - 1
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
         /// </summary>
         event EventHandler<ItemContainerEventArgs> Dematerialized;
 
+        /// <summary>
+        /// Event raised whenever containers are recycled.
+        /// </summary>
+        event EventHandler<ItemContainerEventArgs> Recycled;
+
         /// <summary>
         /// Creates a container control for an item.
         /// </summary>

+ 2 - 5
src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs

@@ -15,13 +15,10 @@ namespace Avalonia.Controls.Generators
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
         /// </summary>
-        /// <param name="startingIndex">The index of the first container in the source items.</param>
         /// <param name="container">The container.</param>
-        public ItemContainerEventArgs(
-            int startingIndex,
-            ItemContainerInfo container)
+        public ItemContainerEventArgs(ItemContainerInfo container)
         {
-            StartingIndex = startingIndex;
+            StartingIndex = container.Index;
             Containers = new[] { container };
         }
 

+ 15 - 2
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@@ -38,6 +38,9 @@ namespace Avalonia.Controls.Generators
         /// <inheritdoc/>
         public event EventHandler<ItemContainerEventArgs> Dematerialized;
 
+        /// <inheritdoc/>
+        public event EventHandler<ItemContainerEventArgs> Recycled;
+
         /// <summary>
         /// Gets or sets the data template used to display the items in the control.
         /// </summary>
@@ -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
         /// <param name="newIndex">The new index.</param>
         /// <param name="item">The new item.</param>
         /// <returns>The container info.</returns>
-        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;
         }
 
         /// <summary>
@@ -225,5 +229,14 @@ namespace Avalonia.Controls.Generators
         {
             return _containers.GetRange(index, count);
         }
+
+        /// <summary>
+        /// Raises the <see cref="Recycled"/> event.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        protected void RaiseRecycled(ItemContainerEventArgs e)
+        {
+            Recycled?.Invoke(this, e);
+        }
     }
 }

+ 2 - 1
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;
         }

+ 2 - 2
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)));
         }
 
         /// <summary>
@@ -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)));
         }
 
         /// <summary>

+ 23 - 0
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);
         }
 
+        /// <summary>
+        /// Called when containers are recycled for the <see cref="ItemsControl"/> by its
+        /// <see cref="ItemContainerGenerator"/>.
+        /// </summary>
+        /// <param name="e">The details of the containers.</param>
+        protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
+        {
+            var toRemove = new List<ILogical>();
+
+            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);
+        }
+
         /// <inheritdoc/>
         protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e)
         {

+ 13 - 0
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));
+                }
+            }
+        }
+
         /// <inheritdoc/>
         protected override void OnDataContextChanging()
         {

+ 48 - 19
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<string>(_ => 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<string>(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<ListBox>(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<ListBoxItem>(parent => new ContentPresenter
-            {
-                Name = "PART_ContentPresenter",
-                [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
-                [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
-            });
+            return new FuncControlTemplate<ListBoxItem>(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<ScrollViewer>(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)