Browse Source

More WIP on adding virtualization to ItemsPresenter.

pull/554/head
Steven Kirk 10 years ago
parent
commit
38afaf1aca
  1. 14
      samples/XamlTestApplicationPcl/Views/MainWindow.cs
  2. 3
      samples/XamlTestApplicationPcl/Views/MainWindow.xaml
  3. 2
      src/Avalonia.Controls/IVirtualizingPanel.cs
  4. 1
      src/Avalonia.Controls/ItemsControl.cs
  5. 15
      src/Avalonia.Controls/ListBox.cs
  6. 3
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  7. 111
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  8. 34
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  9. 1
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  10. 1
      src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs
  11. 3
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  12. 149
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

14
samples/XamlTestApplicationPcl/Views/MainWindow.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
@ -26,16 +28,8 @@ namespace XamlTestApplication.Views
_exitMenu = this.FindControl<MenuItem>("exitMenu"); _exitMenu = this.FindControl<MenuItem>("exitMenu");
_exitMenu.Click += (s, e) => Application.Current.Exit(); _exitMenu.Click += (s, e) => Application.Current.Exit();
var vadd = this.FindControl<Button>("vadd"); var virtualList = this.FindControl<ListBox>("virtualList");
var vsp = this.FindControl<VirtualizingStackPanel>("vsp"); virtualList.Items = Enumerable.Range(0, 200).Select(x => $"Item {x}").ToList();
var ivp = (IVirtualizingPanel)vsp;
var index = 0;
vadd.Click += (s, e) =>
{
vsp.Children.Add(new TextBlock { Text = "Hello " + ++index });
vadd.IsEnabled = !ivp.IsFull;
};
} }
} }
} }

3
samples/XamlTestApplicationPcl/Views/MainWindow.xaml

@ -29,9 +29,8 @@
<TabItem Header="Virtualization"> <TabItem Header="Virtualization">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Right" Orientation="Vertical"> <StackPanel DockPanel.Dock="Right" Orientation="Vertical">
<Button Name="vadd">Add Item</Button>
</StackPanel> </StackPanel>
<Thingamybob></Thingamybob> <ListBox Name="virtualList"/>
</DockPanel> </DockPanel>
</TabItem> </TabItem>

2
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -11,7 +11,5 @@ namespace Avalonia.Controls
double AverageItemSize { get; } double AverageItemSize { get; }
double PixelOffset { get; set; } double PixelOffset { get; set; }
Action ArrangeCompleted { get; set; }
} }
} }

1
src/Avalonia.Controls/ItemsControl.cs

@ -25,7 +25,6 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// The default value for the <see cref="ItemsPanel"/> property. /// The default value for the <see cref="ItemsPanel"/> property.
/// </summary> /// </summary>
[SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")]
private static readonly FuncTemplate<IPanel> DefaultPanel = private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel()); new FuncTemplate<IPanel>(() => new StackPanel());

15
src/Avalonia.Controls/ListBox.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -16,6 +17,12 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class ListBox : SelectingItemsControl public class ListBox : SelectingItemsControl
{ {
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
/// <summary> /// <summary>
/// Defines the <see cref="SelectedItems"/> property. /// Defines the <see cref="SelectedItems"/> property.
/// </summary> /// </summary>
@ -28,6 +35,14 @@ namespace Avalonia.Controls
public static readonly new AvaloniaProperty<SelectionMode> SelectionModeProperty = public static readonly new AvaloniaProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty; SelectingItemsControl.SelectionModeProperty;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
/// </summary>
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
}
/// <inheritdoc/> /// <inheritdoc/>
public new IList SelectedItems => base.SelectedItems; public new IList SelectedItems => base.SelectedItems;

3
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void CreatePanel() protected override void PanelCreated(IPanel panel)
{ {
base.CreatePanel();
var task = MoveToPage(-1, SelectedIndex); var task = MoveToPage(-1, SelectedIndex);
} }

111
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -5,6 +5,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
@ -55,6 +56,7 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
Action IScrollable.InvalidateScroll { get; set; } Action IScrollable.InvalidateScroll { get; set; }
/// <inheritdoc/>
Size IScrollable.Extent Size IScrollable.Extent
{ {
get get
@ -69,39 +71,53 @@ namespace Avalonia.Controls.Presenters
} }
} }
/// <inheritdoc/>
Vector IScrollable.Offset { get; set; } Vector IScrollable.Offset { get; set; }
/// <inheritdoc/>
Size IScrollable.Viewport Size IScrollable.Viewport
{ {
get get
{ {
throw new NotImplementedException(); switch (VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
return new Size(0, (_virt.LastIndex - _virt.FirstIndex) + 1);
default:
return default(Size);
}
} }
} }
Size IScrollable.ScrollSize /// <inheritdoc/>
{ Size IScrollable.ScrollSize => new Size(0, 1);
get
{ /// <inheritdoc/>
throw new NotImplementedException(); Size IScrollable.PageScrollSize => new Size(0, 1);
}
}
Size IScrollable.PageScrollSize /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{ {
get var result = base.ArrangeOverride(finalSize);
if (_virt != null)
{ {
throw new NotImplementedException(); CreateRemoveVirtualizedContainers();
((IScrollable)this).InvalidateScroll();
} }
return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void CreatePanel() protected override void PanelCreated(IPanel panel)
{ {
base.CreatePanel(); if (((IScrollable)this).InvalidateScroll != null)
{
var virtualizingPanel = Panel as IVirtualizingPanel; var virtualizingPanel = Panel as IVirtualizingPanel;
_virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null; _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
}
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{ {
@ -115,8 +131,19 @@ namespace Avalonia.Controls.Presenters
KeyboardNavigation.GetTabNavigation(this)); KeyboardNavigation.GetTabNavigation(this));
} }
/// <inheritdoc/>
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
if (_virt == null)
{
ItemsChangedNonVirtualized(e);
}
else
{
ItemsChangedVirtualized(e);
}
}
private void ItemsChangedNonVirtualized(NotifyCollectionChangedEventArgs e)
{ {
var generator = ItemContainerGenerator; var generator = ItemContainerGenerator;
@ -129,7 +156,7 @@ namespace Avalonia.Controls.Presenters
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
} }
AddContainers(e.NewStartingIndex, e.NewItems); AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems);
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
@ -138,7 +165,7 @@ namespace Avalonia.Controls.Presenters
case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Replace:
RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
var containers = AddContainers(e.NewStartingIndex, e.NewItems); var containers = AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems);
var i = e.NewStartingIndex; var i = e.NewStartingIndex;
@ -156,7 +183,7 @@ namespace Avalonia.Controls.Presenters
if (Items != null) if (Items != null)
{ {
AddContainers(0, Items); AddContainersNonVirtualized(0, Items);
} }
break; break;
@ -165,7 +192,11 @@ namespace Avalonia.Controls.Presenters
InvalidateMeasure(); InvalidateMeasure();
} }
private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items) private void ItemsChangedVirtualized(NotifyCollectionChangedEventArgs e)
{
}
private IList<ItemContainerInfo> AddContainersNonVirtualized(int index, IEnumerable items)
{ {
var generator = ItemContainerGenerator; var generator = ItemContainerGenerator;
var result = new List<ItemContainerInfo>(); var result = new List<ItemContainerInfo>();
@ -193,6 +224,42 @@ namespace Avalonia.Controls.Presenters
return result; return result;
} }
private void CreateRemoveVirtualizedContainers()
{
var generator = ItemContainerGenerator;
var panel = _virt.Panel;
if (!panel.IsFull)
{
var index = _virt.LastIndex + 1;
var items = Items.Cast<object>().Skip(index);
var memberSelector = MemberSelector;
foreach (var item in items)
{
var materialized = generator.Materialize(index++, item, memberSelector);
panel.Children.Add(materialized.ContainerControl);
if (panel.IsFull)
{
break;
}
}
_virt.LastIndex = index - 1;
}
if (panel.OverflowCount > 0)
{
var remove = panel.OverflowCount;
panel.Children.RemoveRange(
panel.Children.Count - remove,
panel.OverflowCount);
_virt.LastIndex -= remove;
}
}
private void RemoveContainers(IEnumerable<ItemContainerInfo> items) private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
{ {
foreach (var i in items) foreach (var i in items)
@ -213,7 +280,7 @@ namespace Avalonia.Controls.Presenters
public IVirtualizingPanel Panel { get; } public IVirtualizingPanel Panel { get; }
public int FirstIndex { get; set; } public int FirstIndex { get; set; }
public int LastIndex { get; set; } public int LastIndex { get; set; } = -1;
} }
} }
} }

34
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -102,7 +102,13 @@ namespace Avalonia.Controls.Presenters
if (_generator == null) if (_generator == null)
{ {
var i = TemplatedParent as ItemsControl; var i = TemplatedParent as ItemsControl;
_generator = (i?.ItemContainerGenerator) ?? new ItemContainerGenerator(this); _generator = i?.ItemContainerGenerator;
if (_generator == null)
{
_generator = new ItemContainerGenerator(this);
_generator.ItemTemplate = ItemTemplate;
}
} }
return _generator; return _generator;
@ -178,11 +184,26 @@ namespace Avalonia.Controls.Presenters
return finalSize; return finalSize;
} }
/// <summary>
/// Called when the <see cref="Panel"/> is created.
/// </summary>
/// <param name="panel">The panel.</param>
protected virtual void PanelCreated(IPanel panel)
{
}
/// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, the items collection has been modified, or the panel has been created.
/// </summary>
/// <param name="e">A description of the change.</param>
protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
/// <summary> /// <summary>
/// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first /// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
/// time. /// time.
/// </summary> /// </summary>
protected virtual void CreatePanel() private void CreatePanel()
{ {
Panel = ItemsPanel.Build(); Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent); Panel.SetValue(TemplatedParentProperty, TemplatedParent);
@ -201,16 +222,11 @@ namespace Avalonia.Controls.Presenters
incc.CollectionChanged += ItemsCollectionChanged; incc.CollectionChanged += ItemsCollectionChanged;
} }
PanelCreated(Panel);
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
} }
/// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, the items collection has been modified, or the panel has been created.
/// </summary>
/// <param name="e">A description of the change.</param>
protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
/// <summary> /// <summary>
/// Called when the <see cref="Items"/> collection changes. /// Called when the <see cref="Items"/> collection changes.
/// </summary> /// </summary>

1
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -213,6 +213,7 @@ namespace Avalonia.Controls.Presenters
else if (child != null) else if (child != null)
{ {
child.Arrange(new Rect(finalSize)); child.Arrange(new Rect(finalSize));
return finalSize;
} }
return new Size(); return new Size();

1
src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs

@ -15,7 +15,6 @@ namespace Avalonia.Controls.Presenters
if (_panel == null) if (_panel == null)
{ {
_panel = new VirtualizingStackPanel(); _panel = new VirtualizingStackPanel();
_panel.ArrangeCompleted = CheckPanel;
Child = _panel; Child = _panel;
CheckPanel(); CheckPanel();
} }

3
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -42,8 +42,6 @@ namespace Avalonia.Controls
} }
} }
Action IVirtualizingPanel.ArrangeCompleted { get; set; }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
_canBeRemoved = 0; _canBeRemoved = 0;
@ -51,7 +49,6 @@ namespace Avalonia.Controls
_averageItemSize = 0; _averageItemSize = 0;
_averageCount = 0; _averageCount = 0;
var result = base.ArrangeOverride(finalSize); var result = base.ArrangeOverride(finalSize);
((IVirtualizingPanel)this).ArrangeCompleted?.Invoke();
return result; return result;
} }

149
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -14,9 +16,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
[Fact] [Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel() public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel()
{ {
var target = new ItemsPresenter var target = CreateTarget();
{ target.ClearValue(ItemsPresenter.ItemsPanelProperty);
};
target.ApplyTemplate(); target.ApplyTemplate();
@ -26,11 +27,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
[Fact] [Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None() public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None()
{ {
var target = new ItemsPresenter var target = CreateTarget(ItemVirtualizationMode.None);
{
ItemsPanel = VirtualizingPanelTemplate(),
VirtualizationMode = ItemVirtualizationMode.None,
};
target.ApplyTemplate(); target.ApplyTemplate();
@ -38,45 +35,108 @@ namespace Avalonia.Controls.UnitTests.Presenters
} }
[Fact] [Fact]
public void Should_Return_IsLogicalScrollEnabled_True_When_Has_Virtualizing_Panel() public void Should_Return_IsLogicalScrollEnabled_False_When_Doesnt_Have_ScrollPresenter_Parent()
{ {
var target = new ItemsPresenter var target = new ItemsPresenter
{ {
ItemsPanel = VirtualizingPanelTemplate(), ItemsPanel = VirtualizingPanelTemplate(),
ItemTemplate = ItemTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple,
}; };
target.ApplyTemplate(); target.ApplyTemplate();
Assert.False(((IScrollable)target).IsLogicalScrollEnabled);
}
[Fact]
public void Should_Return_IsLogicalScrollEnabled_True()
{
var target = CreateTarget();
target.ApplyTemplate();
Assert.True(((IScrollable)target).IsLogicalScrollEnabled); Assert.True(((IScrollable)target).IsLogicalScrollEnabled);
} }
[Fact]
public void Should_Fill_Panel_With_Containers()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count);
}
[Fact]
public void Should_Only_Create_Enough_Containers_To_Display_All_Items()
{
var target = CreateTarget(itemCount: 2);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(2, target.Panel.Children.Count);
}
[Fact]
public void Initial_Item_DataContexts_Should_Be_Correct()
{
var target = CreateTarget();
var items = (IList<string>)target.Items;
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
for (var i = 0; i < target.Panel.Children.Count; ++i)
{
Assert.Equal(items[i], target.Panel.Children[i].DataContext);
}
}
[Fact]
public void Should_Add_New_Items_When_Control_Is_Enlarged()
{
var target = CreateTarget();
var items = (IList<string>)target.Items;
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count);
target.Arrange(new Rect(0, 0, 100, 120));
Assert.Equal(12, target.Panel.Children.Count);
for (var i = 0; i < target.Panel.Children.Count; ++i)
{
Assert.Equal(items[i], target.Panel.Children[i].DataContext);
}
}
public class Simple public class Simple
{ {
[Fact] [Fact]
public void Should_Return_Items_Count_For_Extent() public void Should_Return_Items_Count_For_Extent()
{ {
var target = new ItemsPresenter var target = CreateTarget();
{
Items = new string[10],
ItemsPanel = VirtualizingPanelTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple,
};
target.ApplyTemplate(); target.ApplyTemplate();
Assert.Equal(new Size(0, 10), ((IScrollable)target).Extent); Assert.Equal(new Size(0, 20), ((IScrollable)target).Extent);
} }
[Fact] [Fact]
public void Should_Have_Number_Of_Visible_Items_As_Viewport() public void Should_Have_Number_Of_Visible_Items_As_Viewport()
{ {
var target = new ItemsPresenter var target = CreateTarget();
{
Items = new string[20],
ItemsPanel = VirtualizingPanelTemplate(),
ItemTemplate = ItemTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple,
};
target.ApplyTemplate(); target.ApplyTemplate();
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
@ -84,13 +144,52 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(10, ((IScrollable)target).Viewport.Height); Assert.Equal(10, ((IScrollable)target).Viewport.Height);
} }
[Fact]
public void Should_Remove_Items_When_Control_Is_Shrank()
{
var target = CreateTarget();
var items = (IList<string>)target.Items;
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count);
target.Arrange(new Rect(0, 0, 100, 80));
Assert.Equal(8, target.Panel.Children.Count);
}
}
private static ItemsPresenter CreateTarget(
ItemVirtualizationMode mode = ItemVirtualizationMode.Simple,
int itemCount = 20)
{
ItemsPresenter result;
var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList();
var scroller = new ScrollContentPresenter
{
Content = result = new ItemsPresenter
{
Items = items,
ItemsPanel = VirtualizingPanelTemplate(),
ItemTemplate = ItemTemplate(),
VirtualizationMode = mode,
}
};
scroller.UpdateChild();
return result;
} }
private static IDataTemplate ItemTemplate() private static IDataTemplate ItemTemplate()
{ {
return new FuncDataTemplate<string>(x => new TextBlock return new FuncDataTemplate<string>(x => new Canvas
{ {
Text = x,
Height = 10, Height = 10,
}); });
} }

Loading…
Cancel
Save