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\IItemsPresenterHost.cs" />
<Compile Include="Presenters\ItemsPresenterBase.cs" />
<Compile Include="Presenters\ItemVirtualizerNone.cs" />
<Compile Include="Presenters\ItemVirtualizerSimple.cs" />
<Compile Include="Presenters\ThingamybobPresenter.cs" />
<Compile Include="Presenters\ItemVirtualizer.cs" />
<Compile Include="Primitives\HeaderedSelectingControl.cs" />
<Compile Include="Primitives\IScrollable.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
{
/// <summary>
/// A panel that can be used to virtualize items.
/// </summary>
public interface IVirtualizingPanel : IPanel
{
/// <summary>
/// Gets a value indicating whether the panel is full.
/// </summary>
bool IsFull { get; }
/// <summary>
/// Gets the number of items that can be removed while keeping the panel full.
/// </summary>
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; }
/// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary>
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),
defaultValue: ItemVirtualizationMode.Simple);
private VirtualizationInfo _virt;
private ItemVirtualizer _virtualizer;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
@ -50,44 +50,20 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
bool IScrollable.IsLogicalScrollEnabled
{
get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; }
get { return _virtualizer?.IsLogicalScrollEnabled ?? false; }
}
/// <inheritdoc/>
Action IScrollable.InvalidateScroll { get; set; }
/// <inheritdoc/>
Size IScrollable.Extent
{
get
{
switch (VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
return new Size(0, Items?.Count() ?? 0);
default:
return default(Size);
}
}
}
Size IScrollable.Extent => _virtualizer.Extent;
/// <inheritdoc/>
Vector IScrollable.Offset { get; set; }
/// <inheritdoc/>
Size IScrollable.Viewport
{
get
{
switch (VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
return new Size(0, (_virt.LastIndex - _virt.FirstIndex) + 1);
default:
return default(Size);
}
}
}
Size IScrollable.Viewport => _virtualizer.Viewport;
/// <inheritdoc/>
Size IScrollable.ScrollSize => new Size(0, 1);
@ -99,25 +75,14 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
if (_virt != null)
{
CreateRemoveVirtualizedContainers();
((IScrollable)this).InvalidateScroll();
}
_virtualizer.Arranging(finalSize);
return result;
}
/// <inheritdoc/>
protected override void PanelCreated(IPanel panel)
{
if (((IScrollable)this).InvalidateScroll != null)
{
var virtualizingPanel = Panel as IVirtualizingPanel;
_virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
}
_virtualizer = ItemVirtualizer.Create(this);
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{
@ -133,155 +98,7 @@ namespace Avalonia.Controls.Presenters
protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
{
if (_virt == null)
{
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;
_virtualizer?.ItemsChanged(Items, e);
}
}
}

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

@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils
public static int Count(this IEnumerable items)
{
Contract.Requires<ArgumentNullException>(items != null);
var collection = items as ICollection;
if (collection != null)
if (items != null)
{
return collection.Count;
var collection = items as ICollection;
if (collection != null)
{
return collection.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());
}
}
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;
Orientation IVirtualizingPanel.ScrollDirection => Orientation;
double IVirtualizingPanel.AverageItemSize => _averageItemSize;
double IVirtualizingPanel.PixelOffset

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

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

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

@ -6,7 +6,6 @@ using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
@ -124,7 +123,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
public class Simple
{
[Fact]
public void Should_Return_Items_Count_For_Extent()
public void Should_Return_Items_Count_For_Extent_Vertical()
{
var target = CreateTarget();
@ -134,7 +133,17 @@ namespace Avalonia.Controls.UnitTests.Presenters
}
[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();
@ -142,7 +151,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(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]
@ -165,6 +186,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
private static ItemsPresenter CreateTarget(
ItemVirtualizationMode mode = ItemVirtualizationMode.Simple,
Orientation orientation = Orientation.Vertical,
int itemCount = 20)
{
ItemsPresenter result;
@ -175,7 +197,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
Content = result = new ItemsPresenter
{
Items = items,
ItemsPanel = VirtualizingPanelTemplate(),
ItemsPanel = VirtualizingPanelTemplate(orientation),
ItemTemplate = ItemTemplate(),
VirtualizationMode = mode,
}
@ -190,13 +212,18 @@ namespace Avalonia.Controls.UnitTests.Presenters
{
return new FuncDataTemplate<string>(x => new Canvas
{
Width = 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