Browse Source

Refactored virtualization handling into classes

Also take into account the scroll direction of the panel.
pull/554/head
Steven Kirk 10 years ago
parent
commit
91e2f2a0ca
  1. 3
      src/Avalonia.Controls/Avalonia.Controls.csproj
  2. 25
      src/Avalonia.Controls/IVirtualizingPanel.cs
  3. 52
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  4. 137
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  5. 93
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  6. 197
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  7. 19
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  8. 2
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  9. 7
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject
  10. 41
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

3
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -70,7 +70,10 @@
<Compile Include="Presenters\IContentPresenterHost.cs" /> <Compile Include="Presenters\IContentPresenterHost.cs" />
<Compile Include="Presenters\IItemsPresenterHost.cs" /> <Compile Include="Presenters\IItemsPresenterHost.cs" />
<Compile Include="Presenters\ItemsPresenterBase.cs" /> <Compile Include="Presenters\ItemsPresenterBase.cs" />
<Compile Include="Presenters\ItemVirtualizerNone.cs" />
<Compile Include="Presenters\ItemVirtualizerSimple.cs" />
<Compile Include="Presenters\ThingamybobPresenter.cs" /> <Compile Include="Presenters\ThingamybobPresenter.cs" />
<Compile Include="Presenters\ItemVirtualizer.cs" />
<Compile Include="Primitives\HeaderedSelectingControl.cs" /> <Compile Include="Primitives\HeaderedSelectingControl.cs" />
<Compile Include="Primitives\IScrollable.cs" /> <Compile Include="Primitives\IScrollable.cs" />
<Compile Include="Primitives\TabStripItem.cs" /> <Compile Include="Primitives\TabStripItem.cs" />

25
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -1,15 +1,38 @@
using System; // 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;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary>
/// A panel that can be used to virtualize items.
/// </summary>
public interface IVirtualizingPanel : IPanel public interface IVirtualizingPanel : IPanel
{ {
/// <summary>
/// Gets a value indicating whether the panel is full.
/// </summary>
bool IsFull { get; } bool IsFull { get; }
/// <summary>
/// Gets the number of items that can be removed while keeping the panel full.
/// </summary>
int OverflowCount { get; } int OverflowCount { get; }
/// <summary>
/// Gets the direction of scroll.
/// </summary>
Orientation ScrollDirection { get; }
/// <summary>
/// Gets the average size of the materialized items in the direction of scroll.
/// </summary>
double AverageItemSize { get; } double AverageItemSize { get; }
/// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary>
double PixelOffset { get; set; } double PixelOffset { get; set; }
} }
} }

52
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -0,0 +1,52 @@
// 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;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls.Presenters
{
internal abstract class ItemVirtualizer
{
public ItemVirtualizer(ItemsPresenter owner)
{
Owner = owner;
}
public ItemsPresenter Owner { get; }
public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
public IEnumerable Items { get; private set; }
public int FirstIndex { get; set; }
public int LastIndex { get; set; } = -1;
public abstract bool IsLogicalScrollEnabled { get; }
public abstract Size Extent { get; }
public abstract Size Viewport { get; }
public static ItemVirtualizer Create(ItemsPresenter owner)
{
var virtualizingPanel = owner.Panel as IVirtualizingPanel;
var scrollable = (IScrollable)owner;
if (virtualizingPanel != null && scrollable.InvalidateScroll != null)
{
switch (owner.VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
return new ItemVirtualizerSimple(owner);
}
}
return new ItemVirtualizerNone(owner);
}
public abstract void Arranging(Size finalSize);
public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
{
Items = items;
}
}
}

137
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@ -0,0 +1,137 @@
// 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;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Presenters
{
internal class ItemVirtualizerNone : ItemVirtualizer
{
public ItemVirtualizerNone(ItemsPresenter owner)
: base(owner)
{
}
public override bool IsLogicalScrollEnabled => false;
public override Size Extent
{
get
{
throw new NotSupportedException();
}
}
public override Size Viewport
{
get
{
throw new NotSupportedException();
}
}
public override void Arranging(Size finalSize)
{
// We don't need to do anything here.
}
public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
{
base.ItemsChanged(items, e);
var generator = Owner.ItemContainerGenerator;
var panel = Owner.Panel;
// TODO: Handle Move and Replace etc.
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex + e.NewItems.Count < Items.Count())
{
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
}
AddContainers(e.NewStartingIndex, e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
break;
case NotifyCollectionChangedAction.Replace:
RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
var containers = AddContainers(e.NewStartingIndex, e.NewItems);
var i = e.NewStartingIndex;
foreach (var container in containers)
{
panel.Children[i++] = container.ContainerControl;
}
break;
case NotifyCollectionChangedAction.Move:
// TODO: Implement Move in a more efficient manner.
case NotifyCollectionChangedAction.Reset:
RemoveContainers(generator.Clear());
if (Items != null)
{
AddContainers(0, Items);
}
break;
}
Owner.InvalidateMeasure();
}
private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items)
{
var generator = Owner.ItemContainerGenerator;
var result = new List<ItemContainerInfo>();
var panel = Owner.Panel;
foreach (var item in items)
{
var i = generator.Materialize(index++, item, Owner.MemberSelector);
if (i.ContainerControl != null)
{
if (i.Index < panel.Children.Count)
{
// TODO: This will insert at the wrong place when there are null items.
panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
panel.Children.Add(i.ContainerControl);
}
}
result.Add(i);
}
return result;
}
private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
{
var panel = Owner.Panel;
foreach (var i in items)
{
if (i.ContainerControl != null)
{
panel.Children.Remove(i.ContainerControl);
}
}
}
}
}

93
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -0,0 +1,93 @@
// 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;
using System.Linq;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Presenters
{
internal class ItemVirtualizerSimple : ItemVirtualizer
{
public ItemVirtualizerSimple(ItemsPresenter owner)
: base(owner)
{
}
public override bool IsLogicalScrollEnabled => true;
public override Size Extent
{
get
{
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
{
return new Size(0, Items.Count());
}
else
{
return new Size(Items.Count(), 0);
}
}
}
public override Size Viewport
{
get
{
var panel = VirtualizingPanel;
if (panel.ScrollDirection == Orientation.Vertical)
{
return new Size(0, panel.Children.Count);
}
else
{
return new Size(panel.Children.Count, 0);
}
}
}
public override void Arranging(Size finalSize)
{
CreateRemoveContainers();
}
private void CreateRemoveContainers()
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
if (!panel.IsFull)
{
var index = LastIndex + 1;
var items = Items.Cast<object>().Skip(index);
var memberSelector = Owner.MemberSelector;
foreach (var item in items)
{
var materialized = generator.Materialize(index++, item, memberSelector);
panel.Children.Add(materialized.ContainerControl);
if (panel.IsFull)
{
break;
}
}
LastIndex = index - 1;
}
if (panel.OverflowCount > 0)
{
var count = panel.OverflowCount;
var index = panel.Children.Count - count;
panel.Children.RemoveRange(index, count);
generator.Dematerialize(index, count);
LastIndex -= count;
}
}
}
}

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

@ -26,7 +26,7 @@ namespace Avalonia.Controls.Presenters
nameof(VirtualizationMode), nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.Simple); defaultValue: ItemVirtualizationMode.Simple);
private VirtualizationInfo _virt; private ItemVirtualizer _virtualizer;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class. /// Initializes static members of the <see cref="ItemsPresenter"/> class.
@ -50,44 +50,20 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
bool IScrollable.IsLogicalScrollEnabled bool IScrollable.IsLogicalScrollEnabled
{ {
get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; } get { return _virtualizer?.IsLogicalScrollEnabled ?? false; }
} }
/// <inheritdoc/> /// <inheritdoc/>
Action IScrollable.InvalidateScroll { get; set; } Action IScrollable.InvalidateScroll { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
Size IScrollable.Extent Size IScrollable.Extent => _virtualizer.Extent;
{
get
{
switch (VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
return new Size(0, Items?.Count() ?? 0);
default:
return default(Size);
}
}
}
/// <inheritdoc/> /// <inheritdoc/>
Vector IScrollable.Offset { get; set; } Vector IScrollable.Offset { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
Size IScrollable.Viewport Size IScrollable.Viewport => _virtualizer.Viewport;
{
get
{
switch (VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
return new Size(0, (_virt.LastIndex - _virt.FirstIndex) + 1);
default:
return default(Size);
}
}
}
/// <inheritdoc/> /// <inheritdoc/>
Size IScrollable.ScrollSize => new Size(0, 1); Size IScrollable.ScrollSize => new Size(0, 1);
@ -99,25 +75,14 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
var result = base.ArrangeOverride(finalSize); var result = base.ArrangeOverride(finalSize);
_virtualizer.Arranging(finalSize);
if (_virt != null)
{
CreateRemoveVirtualizedContainers();
((IScrollable)this).InvalidateScroll();
}
return result; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void PanelCreated(IPanel panel) protected override void PanelCreated(IPanel panel)
{ {
if (((IScrollable)this).InvalidateScroll != null) _virtualizer = ItemVirtualizer.Create(this);
{
var virtualizingPanel = Panel as IVirtualizingPanel;
_virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
}
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{ {
@ -133,155 +98,7 @@ namespace Avalonia.Controls.Presenters
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{ {
if (_virt == null) _virtualizer?.ItemsChanged(Items, e);
{
ItemsChangedNonVirtualized(e);
}
else
{
ItemsChangedVirtualized(e);
}
}
private void ItemsChangedNonVirtualized(NotifyCollectionChangedEventArgs e)
{
var generator = ItemContainerGenerator;
// TODO: Handle Move and Replace etc.
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex + e.NewItems.Count < Items.Count())
{
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
}
AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
break;
case NotifyCollectionChangedAction.Replace:
RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
var containers = AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems);
var i = e.NewStartingIndex;
foreach (var container in containers)
{
Panel.Children[i++] = container.ContainerControl;
}
break;
case NotifyCollectionChangedAction.Move:
// TODO: Implement Move in a more efficient manner.
case NotifyCollectionChangedAction.Reset:
RemoveContainers(generator.Clear());
if (Items != null)
{
AddContainersNonVirtualized(0, Items);
}
break;
}
InvalidateMeasure();
}
private void ItemsChangedVirtualized(NotifyCollectionChangedEventArgs e)
{
}
private IList<ItemContainerInfo> AddContainersNonVirtualized(int index, IEnumerable items)
{
var generator = ItemContainerGenerator;
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var i = generator.Materialize(index++, item, MemberSelector);
if (i.ContainerControl != null)
{
if (i.Index < this.Panel.Children.Count)
{
// TODO: This will insert at the wrong place when there are null items.
this.Panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
this.Panel.Children.Add(i.ContainerControl);
}
}
result.Add(i);
}
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 count = panel.OverflowCount;
var index = panel.Children.Count - count;
panel.Children.RemoveRange(index, count);
generator.Dematerialize(index, count);
_virt.LastIndex -= count;
}
}
private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
this.Panel.Children.Remove(i.ContainerControl);
}
}
}
private class VirtualizationInfo
{
public VirtualizationInfo(IVirtualizingPanel panel)
{
Panel = panel;
}
public IVirtualizingPanel Panel { get; }
public int FirstIndex { get; set; }
public int LastIndex { get; set; } = -1;
} }
} }
} }

19
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils
public static int Count(this IEnumerable items) public static int Count(this IEnumerable items)
{ {
Contract.Requires<ArgumentNullException>(items != null); if (items != null)
var collection = items as ICollection;
if (collection != null)
{ {
return collection.Count; var collection = items as ICollection;
if (collection != null)
{
return collection.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());
}
} }
else else
{ {
return Enumerable.Count(items.Cast<object>()); return 0;
} }
} }

2
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -26,6 +26,8 @@ namespace Avalonia.Controls
int IVirtualizingPanel.OverflowCount => _canBeRemoved; int IVirtualizingPanel.OverflowCount => _canBeRemoved;
Orientation IVirtualizingPanel.ScrollDirection => Orientation;
double IVirtualizingPanel.AverageItemSize => _averageItemSize; double IVirtualizingPanel.AverageItemSize => _averageItemSize;
double IVirtualizingPanel.PixelOffset double IVirtualizingPanel.PixelOffset

7
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject

@ -17,10 +17,11 @@
<DetectStackOverflow>true</DetectStackOverflow> <DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace> <IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout> <DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration /> <UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform /> <UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath /> <ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture> <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState> <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture> <BuildProcessArchitecture>x86</BuildProcessArchitecture>
<HiddenWarnings>LongTestTimesWithoutParallelExecution</HiddenWarnings>
</ProjectConfiguration> </ProjectConfiguration>

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

@ -6,7 +6,6 @@ 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;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters namespace Avalonia.Controls.UnitTests.Presenters
@ -124,7 +123,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
public class Simple public class Simple
{ {
[Fact] [Fact]
public void Should_Return_Items_Count_For_Extent() public void Should_Return_Items_Count_For_Extent_Vertical()
{ {
var target = CreateTarget(); var target = CreateTarget();
@ -134,7 +133,17 @@ namespace Avalonia.Controls.UnitTests.Presenters
} }
[Fact] [Fact]
public void Should_Have_Number_Of_Visible_Items_As_Viewport() public void Should_Return_Items_Count_For_Extent_Horizontal()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
target.ApplyTemplate();
Assert.Equal(new Size(20, 0), ((IScrollable)target).Extent);
}
[Fact]
public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical()
{ {
var target = CreateTarget(); var target = CreateTarget();
@ -142,7 +151,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, ((IScrollable)target).Viewport.Height); Assert.Equal(new Size(0, 10), ((IScrollable)target).Viewport);
}
[Fact]
public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(10, 0), ((IScrollable)target).Viewport);
} }
[Fact] [Fact]
@ -165,6 +186,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
private static ItemsPresenter CreateTarget( private static ItemsPresenter CreateTarget(
ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, ItemVirtualizationMode mode = ItemVirtualizationMode.Simple,
Orientation orientation = Orientation.Vertical,
int itemCount = 20) int itemCount = 20)
{ {
ItemsPresenter result; ItemsPresenter result;
@ -175,7 +197,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
Content = result = new ItemsPresenter Content = result = new ItemsPresenter
{ {
Items = items, Items = items,
ItemsPanel = VirtualizingPanelTemplate(), ItemsPanel = VirtualizingPanelTemplate(orientation),
ItemTemplate = ItemTemplate(), ItemTemplate = ItemTemplate(),
VirtualizationMode = mode, VirtualizationMode = mode,
} }
@ -190,13 +212,18 @@ namespace Avalonia.Controls.UnitTests.Presenters
{ {
return new FuncDataTemplate<string>(x => new Canvas return new FuncDataTemplate<string>(x => new Canvas
{ {
Width = 10,
Height = 10, Height = 10,
}); });
} }
private static ITemplate<IPanel> VirtualizingPanelTemplate() private static ITemplate<IPanel> VirtualizingPanelTemplate(
Orientation orientation = Orientation.Vertical)
{ {
return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel()); return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel
{
Orientation = orientation,
});
} }
} }
} }

Loading…
Cancel
Save