Browse Source

Fix virtualized item selection.

So that recycled items' selection state is set correctly.
pull/554/head
Steven Kirk 10 years ago
parent
commit
2c8d8179e5
  1. 6
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  2. 7
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  3. 17
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  4. 3
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  5. 4
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  6. 23
      src/Avalonia.Controls/ItemsControl.cs
  7. 13
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  8. 67
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

6
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -33,6 +32,11 @@ namespace Avalonia.Controls.Generators
/// </summary> /// </summary>
event EventHandler<ItemContainerEventArgs> Dematerialized; event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Event raised whenever containers are recycled.
/// </summary>
event EventHandler<ItemContainerEventArgs> Recycled;
/// <summary> /// <summary>
/// Creates a container control for an item. /// Creates a container control for an item.
/// </summary> /// </summary>

7
src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs

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

17
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -38,6 +38,9 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Dematerialized; public event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Recycled;
/// <summary> /// <summary>
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
@ -58,7 +61,7 @@ namespace Avalonia.Controls.Generators
var container = new ItemContainerInfo(CreateContainer(i), item, index); var container = new ItemContainerInfo(CreateContainer(i), item, index);
AddContainer(container); AddContainer(container);
Materialized?.Invoke(this, new ItemContainerEventArgs(index, container)); Materialized?.Invoke(this, new ItemContainerEventArgs(container));
return container; return container;
} }
@ -207,12 +210,13 @@ namespace Avalonia.Controls.Generators
/// <param name="newIndex">The new index.</param> /// <param name="newIndex">The new index.</param>
/// <param name="item">The new item.</param> /// <param name="item">The new item.</param>
/// <returns>The container info.</returns> /// <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 container = _containers[oldIndex];
var newContainer = new ItemContainerInfo(container.ContainerControl, item, newIndex); var newContainer = new ItemContainerInfo(container.ContainerControl, item, newIndex);
_containers[oldIndex] = null; _containers[oldIndex] = null;
AddContainer(newContainer); AddContainer(newContainer);
return newContainer;
} }
/// <summary> /// <summary>
@ -225,5 +229,14 @@ namespace Avalonia.Controls.Generators
{ {
return _containers.GetRange(index, count); 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);
}
} }
} }

3
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -93,7 +93,8 @@ namespace Avalonia.Controls.Generators
container.DataContext = i; container.DataContext = i;
} }
MoveContainer(oldIndex, newIndex, i); var info = MoveContainer(oldIndex, newIndex, i);
RaiseRecycled(new ItemContainerEventArgs(info));
return true; return true;
} }

4
src/Avalonia.Controls/Generators/TreeContainerIndex.cs

@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators
Materialized?.Invoke( Materialized?.Invoke(
this, this,
new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
} }
/// <summary> /// <summary>
@ -62,7 +62,7 @@ namespace Avalonia.Controls.Generators
Dematerialized?.Invoke( Dematerialized?.Invoke(
this, this,
new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
} }
/// <summary> /// <summary>

23
src/Avalonia.Controls/ItemsControl.cs

@ -89,6 +89,7 @@ namespace Avalonia.Controls
_itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
} }
} }
@ -264,6 +265,28 @@ namespace Avalonia.Controls
LogicalChildren.RemoveAll(toRemove); 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/> /// <inheritdoc/>
protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e) protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{ {

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

67
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -20,7 +20,7 @@ namespace Avalonia.Controls.UnitTests
{ {
var target = new ListBox var target = new ListBox
{ {
Template = CreateListBoxTemplate(), Template = ListBoxTemplate(),
Items = new[] { "Foo" }, Items = new[] { "Foo" },
ItemTemplate = new FuncDataTemplate<string>(_ => new Canvas()), ItemTemplate = new FuncDataTemplate<string>(_ => new Canvas()),
}; };
@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests
{ {
var target = new ListBox var target = new ListBox
{ {
Template = CreateListBoxTemplate(), Template = ListBoxTemplate(),
}; };
Prepare(target); Prepare(target);
@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests
var items = new[] { "Foo", "Bar", "Baz " }; var items = new[] { "Foo", "Bar", "Baz " };
var target = new ListBox var target = new ListBox
{ {
Template = CreateListBoxTemplate(), Template = ListBoxTemplate(),
Items = items, Items = items,
}; };
@ -76,7 +76,7 @@ namespace Avalonia.Controls.UnitTests
{ {
var target = new ListBox var target = new ListBox
{ {
Template = CreateListBoxTemplate(), Template = ListBoxTemplate(),
Items = new[] { "Foo", "Bar", "Baz " }, Items = new[] { "Foo", "Bar", "Baz " },
}; };
@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests
var target = new ListBox var target = new ListBox
{ {
Template = CreateListBoxTemplate(), Template = ListBoxTemplate(),
DataContext = "Base", DataContext = "Base",
DataTemplates = new DataTemplates 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 => return new FuncControlTemplate<ListBox>(parent =>
new ScrollViewer new ScrollViewer
{ {
Name = "PART_ScrollViewer", Name = "PART_ScrollViewer",
Template = new FuncControlTemplate(CreateScrollViewerTemplate), Template = ScrollViewerTemplate(),
Content = new ItemsPresenter Content = new ItemsPresenter
{ {
Name = "PART_ItemsPresenter", Name = "PART_ItemsPresenter",
@ -146,21 +170,26 @@ namespace Avalonia.Controls.UnitTests
private FuncControlTemplate ListBoxItemTemplate() private FuncControlTemplate ListBoxItemTemplate()
{ {
return new FuncControlTemplate<ListBoxItem>(parent => new ContentPresenter return new FuncControlTemplate<ListBoxItem>(parent =>
{ new ContentPresenter
Name = "PART_ContentPresenter", {
[!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], Name = "PART_ContentPresenter",
[!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty], [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
}); [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
});
} }
private Control CreateScrollViewerTemplate(ITemplatedControl parent) private FuncControlTemplate ScrollViewerTemplate()
{ {
return new ScrollContentPresenter return new FuncControlTemplate<ScrollViewer>(parent =>
{ new ScrollContentPresenter
Name = "PART_ContentPresenter", {
[~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty), 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) private void Prepare(ListBox target)

Loading…
Cancel
Save