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.
// 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.Controls;
using Avalonia.Diagnostics;
@ -26,16 +28,8 @@ namespace XamlTestApplication.Views
_exitMenu = this.FindControl<MenuItem>("exitMenu");
_exitMenu.Click += (s, e) => Application.Current.Exit();
var vadd = this.FindControl<Button>("vadd");
var vsp = this.FindControl<VirtualizingStackPanel>("vsp");
var ivp = (IVirtualizingPanel)vsp;
var index = 0;
vadd.Click += (s, e) =>
{
vsp.Children.Add(new TextBlock { Text = "Hello " + ++index });
vadd.IsEnabled = !ivp.IsFull;
};
var virtualList = this.FindControl<ListBox>("virtualList");
virtualList.Items = Enumerable.Range(0, 200).Select(x => $"Item {x}").ToList();
}
}
}

3
samples/XamlTestApplicationPcl/Views/MainWindow.xaml

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

2
src/Avalonia.Controls/IVirtualizingPanel.cs

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

1
src/Avalonia.Controls/ItemsControl.cs

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

15
src/Avalonia.Controls/ListBox.cs

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

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

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

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

@ -5,6 +5,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
@ -55,6 +56,7 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
Action IScrollable.InvalidateScroll { get; set; }
/// <inheritdoc/>
Size IScrollable.Extent
{
get
@ -69,39 +71,53 @@ namespace Avalonia.Controls.Presenters
}
}
/// <inheritdoc/>
Vector IScrollable.Offset { get; set; }
/// <inheritdoc/>
Size IScrollable.Viewport
{
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
{
get
{
throw new NotImplementedException();
}
}
/// <inheritdoc/>
Size IScrollable.ScrollSize => new Size(0, 1);
/// <inheritdoc/>
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/>
protected override void CreatePanel()
protected override void PanelCreated(IPanel panel)
{
base.CreatePanel();
var virtualizingPanel = Panel as IVirtualizingPanel;
_virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
if (((IScrollable)this).InvalidateScroll != null)
{
var virtualizingPanel = Panel as IVirtualizingPanel;
_virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
}
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{
@ -115,8 +131,19 @@ namespace Avalonia.Controls.Presenters
KeyboardNavigation.GetTabNavigation(this));
}
/// <inheritdoc/>
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
if (_virt == null)
{
ItemsChangedNonVirtualized(e);
}
else
{
ItemsChangedVirtualized(e);
}
}
private void ItemsChangedNonVirtualized(NotifyCollectionChangedEventArgs e)
{
var generator = ItemContainerGenerator;
@ -129,7 +156,7 @@ namespace Avalonia.Controls.Presenters
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
}
AddContainers(e.NewStartingIndex, e.NewItems);
AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
@ -138,7 +165,7 @@ namespace Avalonia.Controls.Presenters
case NotifyCollectionChangedAction.Replace:
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;
@ -156,7 +183,7 @@ namespace Avalonia.Controls.Presenters
if (Items != null)
{
AddContainers(0, Items);
AddContainersNonVirtualized(0, Items);
}
break;
@ -165,7 +192,11 @@ namespace Avalonia.Controls.Presenters
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 result = new List<ItemContainerInfo>();
@ -193,6 +224,42 @@ namespace Avalonia.Controls.Presenters
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)
{
foreach (var i in items)
@ -213,7 +280,7 @@ namespace Avalonia.Controls.Presenters
public IVirtualizingPanel Panel { get; }
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)
{
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;
@ -178,11 +184,26 @@ namespace Avalonia.Controls.Presenters
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>
/// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
/// time.
/// </summary>
protected virtual void CreatePanel()
private void CreatePanel()
{
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
@ -201,16 +222,11 @@ namespace Avalonia.Controls.Presenters
incc.CollectionChanged += ItemsCollectionChanged;
}
PanelCreated(Panel);
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>
/// Called when the <see cref="Items"/> collection changes.
/// </summary>

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

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

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

@ -15,7 +15,6 @@ namespace Avalonia.Controls.Presenters
if (_panel == null)
{
_panel = new VirtualizingStackPanel();
_panel.ArrangeCompleted = CheckPanel;
Child = _panel;
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)
{
_canBeRemoved = 0;
@ -51,7 +49,6 @@ namespace Avalonia.Controls
_averageItemSize = 0;
_averageCount = 0;
var result = base.ArrangeOverride(finalSize);
((IVirtualizingPanel)this).ArrangeCompleted?.Invoke();
return result;
}

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

@ -1,6 +1,8 @@
// 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.
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -14,9 +16,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
[Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel()
{
var target = new ItemsPresenter
{
};
var target = CreateTarget();
target.ClearValue(ItemsPresenter.ItemsPanelProperty);
target.ApplyTemplate();
@ -26,11 +27,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
[Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None()
{
var target = new ItemsPresenter
{
ItemsPanel = VirtualizingPanelTemplate(),
VirtualizationMode = ItemVirtualizationMode.None,
};
var target = CreateTarget(ItemVirtualizationMode.None);
target.ApplyTemplate();
@ -38,45 +35,108 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[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
{
ItemsPanel = VirtualizingPanelTemplate(),
ItemTemplate = ItemTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple,
};
target.ApplyTemplate();
Assert.False(((IScrollable)target).IsLogicalScrollEnabled);
}
[Fact]
public void Should_Return_IsLogicalScrollEnabled_True()
{
var target = CreateTarget();
target.ApplyTemplate();
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
{
[Fact]
public void Should_Return_Items_Count_For_Extent()
{
var target = new ItemsPresenter
{
Items = new string[10],
ItemsPanel = VirtualizingPanelTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple,
};
var target = CreateTarget();
target.ApplyTemplate();
Assert.Equal(new Size(0, 10), ((IScrollable)target).Extent);
Assert.Equal(new Size(0, 20), ((IScrollable)target).Extent);
}
[Fact]
public void Should_Have_Number_Of_Visible_Items_As_Viewport()
{
var target = new ItemsPresenter
{
Items = new string[20],
ItemsPanel = VirtualizingPanelTemplate(),
ItemTemplate = ItemTemplate(),
VirtualizationMode = ItemVirtualizationMode.Simple,
};
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(100, 100));
@ -84,13 +144,52 @@ namespace Avalonia.Controls.UnitTests.Presenters
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()
{
return new FuncDataTemplate<string>(x => new TextBlock
return new FuncDataTemplate<string>(x => new Canvas
{
Text = x,
Height = 10,
});
}

Loading…
Cancel
Save