Browse Source

WIP: Adding virtualization to ItemsPresenter.

pull/554/head
Steven Kirk 11 years ago
parent
commit
f9b3d2ac03
  1. 3
      src/Avalonia.Controls/Avalonia.Controls.csproj
  2. 22
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  3. 6
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  4. 84
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  5. 6
      src/Avalonia.Controls/Generators/ItemContainerInfo.cs
  6. 6
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  7. 6
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  8. 21
      src/Avalonia.Controls/ItemVirtualizationMode.cs
  9. 8
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  10. 127
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  11. 2
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  12. 1
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  13. 31
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  14. 19
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  15. 103
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

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

@ -50,13 +50,14 @@
<Compile Include="Design.cs" /> <Compile Include="Design.cs" />
<Compile Include="DockPanel.cs" /> <Compile Include="DockPanel.cs" />
<Compile Include="Expander.cs" /> <Compile Include="Expander.cs" />
<Compile Include="Generators\ItemContainer.cs" /> <Compile Include="Generators\ItemContainerInfo.cs" />
<Compile Include="Generators\TreeContainerIndex.cs" /> <Compile Include="Generators\TreeContainerIndex.cs" />
<Compile Include="HotkeyManager.cs" /> <Compile Include="HotkeyManager.cs" />
<Compile Include="IApplicationLifecycle.cs" /> <Compile Include="IApplicationLifecycle.cs" />
<Compile Include="INameScope.cs" /> <Compile Include="INameScope.cs" />
<Compile Include="IPseudoClasses.cs" /> <Compile Include="IPseudoClasses.cs" />
<Compile Include="DropDownItem.cs" /> <Compile Include="DropDownItem.cs" />
<Compile Include="ItemVirtualizationMode.cs" />
<Compile Include="IVirtualizingPanel.cs" /> <Compile Include="IVirtualizingPanel.cs" />
<Compile Include="LayoutTransformControl.cs" /> <Compile Include="LayoutTransformControl.cs" />
<Compile Include="Mixins\ContentControlMixin.cs" /> <Compile Include="Mixins\ContentControlMixin.cs" />

22
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -16,7 +16,7 @@ namespace Avalonia.Controls.Generators
/// <summary> /// <summary>
/// Gets the currently realized containers. /// Gets the currently realized containers.
/// </summary> /// </summary>
IEnumerable<ItemContainer> Containers { get; } IEnumerable<ItemContainerInfo> Containers { get; }
/// <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.
@ -34,17 +34,17 @@ namespace Avalonia.Controls.Generators
event EventHandler<ItemContainerEventArgs> Dematerialized; event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary> /// <summary>
/// Creates container controls for a collection of items. /// Creates a container control for an item.
/// </summary> /// </summary>
/// <param name="startingIndex"> /// <param name="index">
/// The index of the first item of the data in the containing collection. /// The index of the item of the data in the containing collection.
/// </param> /// </param>
/// <param name="items">The items.</param> /// <param name="item">The item.</param>
/// <param name="selector">An optional member selector.</param> /// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns> /// <returns>The created controls.</returns>
IEnumerable<ItemContainer> Materialize( ItemContainerInfo Materialize(
int startingIndex, int index,
IEnumerable items, object item,
IMemberSelector selector); IMemberSelector selector);
/// <summary> /// <summary>
@ -55,7 +55,7 @@ namespace Avalonia.Controls.Generators
/// </param> /// </param>
/// <param name="count">The the number of items to remove.</param> /// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns> /// <returns>The removed containers.</returns>
IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count); IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count);
/// <summary> /// <summary>
/// Inserts space for newly inserted containers in the index. /// Inserts space for newly inserted containers in the index.
@ -73,13 +73,13 @@ namespace Avalonia.Controls.Generators
/// </param> /// </param>
/// <param name="count">The the number of items to remove.</param> /// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns> /// <returns>The removed containers.</returns>
IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count); IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
/// <summary> /// <summary>
/// Clears all created containers and returns the removed controls. /// Clears all created containers and returns the removed controls.
/// </summary> /// </summary>
/// <returns>The removed controls.</returns> /// <returns>The removed controls.</returns>
IEnumerable<ItemContainer> Clear(); IEnumerable<ItemContainerInfo> Clear();
/// <summary> /// <summary>
/// Gets the container control representing the item with the specified index. /// Gets the container control representing the item with the specified index.

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

@ -19,7 +19,7 @@ namespace Avalonia.Controls.Generators
/// <param name="container">The container.</param> /// <param name="container">The container.</param>
public ItemContainerEventArgs( public ItemContainerEventArgs(
int startingIndex, int startingIndex,
ItemContainer container) ItemContainerInfo container)
{ {
StartingIndex = startingIndex; StartingIndex = startingIndex;
Containers = new[] { container }; Containers = new[] { container };
@ -32,7 +32,7 @@ namespace Avalonia.Controls.Generators
/// <param name="containers">The containers.</param> /// <param name="containers">The containers.</param>
public ItemContainerEventArgs( public ItemContainerEventArgs(
int startingIndex, int startingIndex,
IList<ItemContainer> containers) IList<ItemContainerInfo> containers)
{ {
StartingIndex = startingIndex; StartingIndex = startingIndex;
Containers = containers; Containers = containers;
@ -41,7 +41,7 @@ namespace Avalonia.Controls.Generators
/// <summary> /// <summary>
/// Gets the containers. /// Gets the containers.
/// </summary> /// </summary>
public IList<ItemContainer> Containers { get; } public IList<ItemContainerInfo> Containers { get; }
/// <summary> /// <summary>
/// Gets the index of the first container in the source items. /// Gets the index of the first container in the source items.

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

@ -15,7 +15,7 @@ namespace Avalonia.Controls.Generators
/// </summary> /// </summary>
public class ItemContainerGenerator : IItemContainerGenerator public class ItemContainerGenerator : IItemContainerGenerator
{ {
private List<ItemContainer> _containers = new List<ItemContainer>(); private List<ItemContainerInfo> _containers = new List<ItemContainerInfo>();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class. /// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
@ -29,7 +29,7 @@ namespace Avalonia.Controls.Generators
} }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<ItemContainer> Containers => _containers.Where(x => x != null); public IEnumerable<ItemContainerInfo> Containers => _containers.Where(x => x != null);
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Materialized; public event EventHandler<ItemContainerEventArgs> Materialized;
@ -48,33 +48,24 @@ namespace Avalonia.Controls.Generators
public IControl Owner { get; } public IControl Owner { get; }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<ItemContainer> Materialize( public ItemContainerInfo Materialize(
int startingIndex, int index,
IEnumerable items, object item,
IMemberSelector selector) IMemberSelector selector)
{ {
Contract.Requires<ArgumentNullException>(items != null); var i = selector != null ? selector.Select(item) : item;
var container = new ItemContainerInfo(CreateContainer(i), item, index);
int index = startingIndex; AddContainer(container);
var result = new List<ItemContainer>(); Materialized?.Invoke(this, new ItemContainerEventArgs(index, container));
foreach (var item in items) return container;
{
var i = selector != null ? selector.Select(item) : item;
var container = new ItemContainer(CreateContainer(i), item, index++);
result.Add(container);
}
AddContainers(result);
Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result.Where(x => x != null).ToList();
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count) public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{ {
var result = new List<ItemContainer>(); var result = new List<ItemContainerInfo>();
for (int i = startingIndex; i < startingIndex + count; ++i) for (int i = startingIndex; i < startingIndex + count; ++i)
{ {
@ -93,13 +84,13 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/> /// <inheritdoc/>
public virtual void InsertSpace(int index, int count) public virtual void InsertSpace(int index, int count)
{ {
_containers.InsertRange(index, Enumerable.Repeat<ItemContainer>(null, count)); _containers.InsertRange(index, Enumerable.Repeat<ItemContainerInfo>(null, count));
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count) public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{ {
List<ItemContainer> result = new List<ItemContainer>(); List<ItemContainerInfo> result = new List<ItemContainerInfo>();
if (startingIndex < _containers.Count) if (startingIndex < _containers.Count)
{ {
@ -112,10 +103,10 @@ namespace Avalonia.Controls.Generators
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual IEnumerable<ItemContainer> Clear() public virtual IEnumerable<ItemContainerInfo> Clear()
{ {
var result = _containers.Where(x => x != null).ToList(); var result = _containers.Where(x => x != null).ToList();
_containers = new List<ItemContainer>(); _containers = new List<ItemContainerInfo>();
if (result.Count > 0) if (result.Count > 0)
{ {
@ -172,32 +163,29 @@ namespace Avalonia.Controls.Generators
} }
/// <summary> /// <summary>
/// Adds a collection of containers to the index. /// Adds a container to the index.
/// </summary> /// </summary>
/// <param name="containers">The containers.</param> /// <param name="container">The container.</param>
protected void AddContainers(IList<ItemContainer> containers) protected void AddContainer(ItemContainerInfo container)
{ {
Contract.Requires<ArgumentNullException>(containers != null); Contract.Requires<ArgumentNullException>(container != null);
foreach (var c in containers) while (_containers.Count < container.Index)
{ {
while (_containers.Count < c.Index) _containers.Add(null);
{ }
_containers.Add(null);
}
if (_containers.Count == c.Index) if (_containers.Count == container.Index)
{ {
_containers.Add(c); _containers.Add(container);
} }
else if (_containers[c.Index] == null) else if (_containers[container.Index] == null)
{ {
_containers[c.Index] = c; _containers[container.Index] = container;
} }
else else
{ {
throw new InvalidOperationException("Container already created."); throw new InvalidOperationException("Container already created.");
}
} }
} }
@ -207,7 +195,7 @@ namespace Avalonia.Controls.Generators
/// <param name="index">The first index.</param> /// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param> /// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns> /// <returns>The containers.</returns>
protected IEnumerable<ItemContainer> GetContainerRange(int index, int count) protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count)
{ {
return _containers.GetRange(index, count); return _containers.GetRange(index, count);
} }

6
src/Avalonia.Controls/Generators/ItemContainer.cs → src/Avalonia.Controls/Generators/ItemContainerInfo.cs

@ -7,17 +7,17 @@ namespace Avalonia.Controls.Generators
/// Holds information about an item container generated by an /// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>. /// <see cref="IItemContainerGenerator"/>.
/// </summary> /// </summary>
public class ItemContainer public class ItemContainerInfo
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ItemContainer"/> class. /// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
/// </summary> /// </summary>
/// <param name="container">The container control.</param> /// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param> /// <param name="item">The item that the container represents.</param>
/// <param name="index"> /// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection. /// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param> /// </param>
public ItemContainer(IControl container, object item, int index) public ItemContainerInfo(IControl container, object item, int index)
{ {
ContainerControl = container; ContainerControl = container;
Item = item; Item = item;

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

@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators
Materialized?.Invoke( Materialized?.Invoke(
this, this,
new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0)));
} }
/// <summary> /// <summary>
@ -62,14 +62,14 @@ namespace Avalonia.Controls.Generators
Dematerialized?.Invoke( Dematerialized?.Invoke(
this, this,
new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0)));
} }
/// <summary> /// <summary>
/// Removes a set of containers from the index. /// Removes a set of containers from the index.
/// </summary> /// </summary>
/// <param name="containers">The item containers.</param> /// <param name="containers">The item containers.</param>
public void Remove(IEnumerable<ItemContainer> containers) public void Remove(IEnumerable<ItemContainerInfo> containers)
{ {
foreach (var container in containers) foreach (var container in containers)
{ {

6
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -99,20 +99,20 @@ namespace Avalonia.Controls.Generators
} }
} }
public override IEnumerable<ItemContainer> Clear() public override IEnumerable<ItemContainerInfo> Clear()
{ {
var items = base.Clear(); var items = base.Clear();
Index.Remove(items); Index.Remove(items);
return items; return items;
} }
public override IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count) public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{ {
Index.Remove(GetContainerRange(startingIndex, count)); Index.Remove(GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count); return base.Dematerialize(startingIndex, count);
} }
public override IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count) public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{ {
Index.Remove(GetContainerRange(startingIndex, count)); Index.Remove(GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count); return base.RemoveRange(startingIndex, count);

21
src/Avalonia.Controls/ItemVirtualizationMode.cs

@ -0,0 +1,21 @@
// 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.
namespace Avalonia.Controls
{
/// <summary>
/// Describes the item virtualization method to use for a list.
/// </summary>
public enum ItemVirtualizationMode
{
/// <summary>
/// Do not virtualize items.
/// </summary>
None,
/// <summary>
/// Virtualize items without smooth scrolling.
/// </summary>
Simple,
}
}

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

@ -175,12 +175,8 @@ namespace Avalonia.Controls.Presenters
if (container == null) if (container == null)
{ {
var item = Items.Cast<object>().ElementAt(index); var item = Items.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize( var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
index, Panel.Children.Add(materialized.ContainerControl);
new[] { item },
MemberSelector);
container = materialized.First().ContainerControl;
Panel.Children.Add(container);
} }
return container; return container;

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

@ -1,9 +1,12 @@
// 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;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Input; using Avalonia.Input;
@ -12,8 +15,18 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Displays items inside an <see cref="ItemsControl"/>. /// Displays items inside an <see cref="ItemsControl"/>.
/// </summary> /// </summary>
public class ItemsPresenter : ItemsPresenterBase public class ItemsPresenter : ItemsPresenterBase, IScrollable
{ {
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>(
nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.Simple);
private VirtualizationInfo _virt;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class. /// Initializes static members of the <see cref="ItemsPresenter"/> class.
/// </summary> /// </summary>
@ -24,11 +37,72 @@ namespace Avalonia.Controls.Presenters
KeyboardNavigationMode.Once); KeyboardNavigationMode.Once);
} }
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
/// <inheritdoc/>
bool IScrollable.IsLogicalScrollEnabled
{
get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; }
}
/// <inheritdoc/>
Action IScrollable.InvalidateScroll { get; set; }
Size IScrollable.Extent
{
get
{
switch (VirtualizationMode)
{
case ItemVirtualizationMode.Simple:
return new Size(0, Items?.Count() ?? 0);
default:
return default(Size);
}
}
}
Vector IScrollable.Offset { get; set; }
Size IScrollable.Viewport
{
get
{
throw new NotImplementedException();
}
}
Size IScrollable.ScrollSize
{
get
{
throw new NotImplementedException();
}
}
Size IScrollable.PageScrollSize
{
get
{
throw new NotImplementedException();
}
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void CreatePanel() protected override void CreatePanel()
{ {
base.CreatePanel(); base.CreatePanel();
var virtualizingPanel = Panel as IVirtualizingPanel;
_virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null;
if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty))
{ {
KeyboardNavigation.SetDirectionalNavigation( KeyboardNavigation.SetDirectionalNavigation(
@ -55,7 +129,7 @@ namespace Avalonia.Controls.Presenters
generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count);
} }
AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); AddContainers(e.NewStartingIndex, e.NewItems);
break; break;
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
@ -64,8 +138,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 = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); var containers = AddContainers(e.NewStartingIndex, e.NewItems);
AddContainers(containers);
var i = e.NewStartingIndex; var i = e.NewStartingIndex;
@ -83,7 +156,7 @@ namespace Avalonia.Controls.Presenters
if (Items != null) if (Items != null)
{ {
AddContainers(generator.Materialize(0, Items, MemberSelector)); AddContainers(0, Items);
} }
break; break;
@ -92,17 +165,20 @@ namespace Avalonia.Controls.Presenters
InvalidateMeasure(); InvalidateMeasure();
} }
private void AddContainersToPanel(IEnumerable<ItemContainer> items) private IList<ItemContainerInfo> AddContainers(int index, IEnumerable items)
{ {
foreach (var i in 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.ContainerControl != null)
{ {
if (i.Index < this.Panel.Children.Count) if (i.Index < this.Panel.Children.Count)
{ {
// HACK: This will insert at the wrong place when there are null items, // TODO: This will insert at the wrong place when there are null items.
// but all of this will need to be rewritten when we implement
// virtualization so hope no-one notices until then :)
this.Panel.Children.Insert(i.Index, i.ContainerControl); this.Panel.Children.Insert(i.Index, i.ContainerControl);
} }
else else
@ -110,39 +186,34 @@ namespace Avalonia.Controls.Presenters
this.Panel.Children.Add(i.ContainerControl); this.Panel.Children.Add(i.ContainerControl);
} }
} }
result.Add(i);
} }
return result;
} }
private void AddContainers(IEnumerable<ItemContainer> items) private void RemoveContainers(IEnumerable<ItemContainerInfo> items)
{ {
foreach (var i in items) foreach (var i in items)
{ {
if (i.ContainerControl != null) if (i.ContainerControl != null)
{ {
if (i.Index < this.Panel.Children.Count) this.Panel.Children.Remove(i.ContainerControl);
{
// HACK: This will insert at the wrong place when there are null items,
// but all of this will need to be rewritten when we implement
// virtualization so hope no-one notices until then :)
this.Panel.Children.Insert(i.Index, i.ContainerControl);
}
else
{
this.Panel.Children.Add(i.ContainerControl);
}
} }
} }
} }
private void RemoveContainers(IEnumerable<ItemContainer> items) private class VirtualizationInfo
{ {
foreach (var i in items) public VirtualizationInfo(IVirtualizingPanel panel)
{ {
if (i.ContainerControl != null) Panel = panel;
{
this.Panel.Children.Remove(i.ContainerControl);
}
} }
public IVirtualizingPanel Panel { get; }
public int FirstIndex { get; set; }
public int LastIndex { get; set; }
} }
} }
} }

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

@ -206,7 +206,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/> /// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, or the items collection has been modified. /// has been set, the items collection has been modified, or the panel has been created.
/// </summary> /// </summary>
/// <param name="e">A description of the change.</param> /// <param name="e">A description of the change.</param>
protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);

1
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@ -91,6 +91,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="ClassesTests.cs" /> <Compile Include="ClassesTests.cs" />
<Compile Include="LayoutTransformControlTests.cs" /> <Compile Include="LayoutTransformControlTests.cs" />
<Compile Include="Presenters\ItemsPresenterTests_Virtualization.cs" />
<Compile Include="TextBoxTests_ValidationState.cs" /> <Compile Include="TextBoxTests_ValidationState.cs" />
<Compile Include="UserControlTests.cs" /> <Compile Include="UserControlTests.cs" />
<Compile Include="DockPanelTests.cs" /> <Compile Include="DockPanelTests.cs" />

31
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs

@ -1,8 +1,11 @@
// 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;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests.Generators namespace Avalonia.Controls.UnitTests.Generators
@ -15,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" }; var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator(); var owner = new Decorator();
var target = new ItemContainerGenerator(owner); var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null); var containers = Materialize(target, 0, items);
var result = containers var result = containers
.Select(x => x.ContainerControl) .Select(x => x.ContainerControl)
.OfType<TextBlock>() .OfType<TextBlock>()
@ -31,20 +34,36 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" }; var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator(); var owner = new Decorator();
var target = new ItemContainerGenerator(owner); var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList(); var containers = Materialize(target, 0, items);
Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0));
Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1)); Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1));
Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2));
} }
private IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item, null);
result.Add(container);
}
return result;
}
[Fact] [Fact]
public void IndexFromContainer_Should_Return_Index() public void IndexFromContainer_Should_Return_Index()
{ {
var items = new[] { "foo", "bar", "baz" }; var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator(); var owner = new Decorator();
var target = new ItemContainerGenerator(owner); var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList(); var containers = Materialize(target, 0, items);
Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl)); Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl));
Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl)); Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl));
@ -57,7 +76,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" }; var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator(); var owner = new Decorator();
var target = new ItemContainerGenerator(owner); var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList(); var containers = Materialize(target, 0, items);
target.Dematerialize(1, 1); target.Dematerialize(1, 1);
@ -72,7 +91,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" }; var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator(); var owner = new Decorator();
var target = new ItemContainerGenerator(owner); var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null); var containers = Materialize(target, 0, items);
var expected = target.Containers.Take(2).ToList(); var expected = target.Containers.Take(2).ToList();
var result = target.Dematerialize(0, 2); var result = target.Dematerialize(0, 2);
@ -85,7 +104,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" }; var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator(); var owner = new Decorator();
var target = new ItemContainerGenerator(owner); var target = new ItemContainerGenerator(owner);
var containers = target.Materialize(0, items, null).ToList(); var containers = Materialize(target, 0, items);
var removed = target.RemoveRange(1, 1).Single(); var removed = target.RemoveRange(1, 1).Single();

19
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs

@ -1,6 +1,7 @@
// 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 System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Xunit; using Xunit;
@ -15,7 +16,7 @@ namespace Avalonia.Controls.UnitTests.Generators
var items = new[] { "foo", "bar", "baz" }; var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator(); var owner = new Decorator();
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null); var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var containers = target.Materialize(0, items, null); var containers = Materialize(target, 0, items);
var result = containers var result = containers
.Select(x => x.ContainerControl) .Select(x => x.ContainerControl)
.OfType<ListBoxItem>() .OfType<ListBoxItem>()
@ -24,5 +25,21 @@ namespace Avalonia.Controls.UnitTests.Generators
Assert.Equal(items, result); Assert.Equal(items, result);
} }
private IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item, null);
result.Add(container);
}
return result;
}
} }
} }

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

@ -0,0 +1,103 @@
// 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 Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests.Presenters
{
public class ItemsPresenterTests_Virtualization
{
[Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel()
{
var target = new ItemsPresenter
{
};
target.ApplyTemplate();
Assert.False(((IScrollable)target).IsLogicalScrollEnabled);
}
[Fact]
public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None()
{
var target = new ItemsPresenter
{
ItemsPanel = VirtualizingPanelTemplate(),
VirtualizationMode = ItemVirtualizationMode.None,
};
target.ApplyTemplate();
Assert.False(((IScrollable)target).IsLogicalScrollEnabled);
}
[Fact]
public void Should_Return_IsLogicalScrollEnabled_True_When_Has_Virtualizing_Panel()
{
var target = new ItemsPresenter
{
ItemsPanel = VirtualizingPanelTemplate(),
};
target.ApplyTemplate();
Assert.True(((IScrollable)target).IsLogicalScrollEnabled);
}
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,
};
target.ApplyTemplate();
Assert.Equal(new Size(0, 10), ((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,
};
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, ((IScrollable)target).Viewport.Height);
}
}
private static IDataTemplate ItemTemplate()
{
return new FuncDataTemplate<string>(x => new TextBlock
{
Text = x,
Height = 10,
});
}
private static ITemplate<IPanel> VirtualizingPanelTemplate()
{
return new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
}
}
}
Loading…
Cancel
Save