Browse Source

Fix virtualized list to taking up available space.

Previous commit was slightly wrong: too many controls were being created
for the list in VirtualizingTest.
pull/545/head
Steven Kirk 10 years ago
parent
commit
f660da41c7
  1. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  2. 22
      src/Avalonia.Controls/IVirtualizingController.cs
  3. 12
      src/Avalonia.Controls/IVirtualizingPanel.cs
  4. 45
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  5. 6
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  6. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  7. 8
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  8. 38
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  9. 2
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  10. 18
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  11. 32
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

1
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -60,6 +60,7 @@
<Compile Include="DropDownItem.cs" /> <Compile Include="DropDownItem.cs" />
<Compile Include="ISetInheritanceParent.cs" /> <Compile Include="ISetInheritanceParent.cs" />
<Compile Include="ItemVirtualizationMode.cs" /> <Compile Include="ItemVirtualizationMode.cs" />
<Compile Include="IVirtualizingController.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/IVirtualizingController.cs

@ -0,0 +1,22 @@
// 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>
/// Interface implemented by controls that act as controllers for an
/// <see cref="IVirtualizingPanel"/>.
/// </summary>
public interface IVirtualizingController
{
/// <summary>
/// Called when the <see cref="IVirtualizingPanel"/>'s controls should be updated.
/// </summary>
/// <remarks>
/// The controller should respond to this method being called by either adding
/// children up until <see cref="IVirtualizingPanel.IsFull"/> becomes true or
/// removing <see cref="IVirtualizingPanel.OverflowCount"/> controls.
/// </remarks>
void UpdateControls();
}
}

12
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -2,6 +2,7 @@
// 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;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -10,6 +11,17 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public interface IVirtualizingPanel : IPanel public interface IVirtualizingPanel : IPanel
{ {
/// <summary>
/// Gets or sets the controller for the virtualizing panel.
/// </summary>
/// <remarks>
/// A virtualizing controller is responsible for maintaing the controls in the virtualizing
/// panel. This property will be set by the controller when virtualization is initialized.
/// Note that this property may remain null if the panel is added to a control that does
/// not act as a virtualizing controller.
/// </remarks>
IVirtualizingController Controller { get; set; }
/// <summary> /// <summary>
/// Gets a value indicating whether the panel is full. /// Gets a value indicating whether the panel is full.
/// </summary> /// </summary>

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

@ -13,7 +13,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Base class for classes which handle virtualization for an <see cref="ItemsPresenter"/>. /// Base class for classes which handle virtualization for an <see cref="ItemsPresenter"/>.
/// </summary> /// </summary>
internal abstract class ItemVirtualizer internal abstract class ItemVirtualizer : IVirtualizingController
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class. /// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
@ -115,34 +115,34 @@ namespace Avalonia.Controls.Presenters
{ {
var virtualizingPanel = owner.Panel as IVirtualizingPanel; var virtualizingPanel = owner.Panel as IVirtualizingPanel;
var scrollable = (ILogicalScrollable)owner; var scrollable = (ILogicalScrollable)owner;
ItemVirtualizer result = null;
if (virtualizingPanel != null && scrollable.InvalidateScroll != null) if (virtualizingPanel != null && scrollable.InvalidateScroll != null)
{ {
switch (owner.VirtualizationMode) switch (owner.VirtualizationMode)
{ {
case ItemVirtualizationMode.Simple: case ItemVirtualizationMode.Simple:
return new ItemVirtualizerSimple(owner); result = new ItemVirtualizerSimple(owner);
break;
} }
} }
return new ItemVirtualizerNone(owner); if (result == null)
} {
result = new ItemVirtualizerNone(owner);
}
/// <summary> if (virtualizingPanel != null)
/// Called by the <see cref="Owner"/> when it carries out an arrange. {
/// </summary> virtualizingPanel.Controller = result;
/// <param name="finalSize">The final size passed to the arrange.</param> }
public abstract void Arranging(Size finalSize);
/// <summary> return result;
/// Called when a request is made to bring an item into view. }
/// </summary>
/// <param name="target">The item to bring into view.</param> /// <inheritdoc/>
/// <param name="targetRect">The rect on the item to bring into view.</param> public virtual void UpdateControls()
/// <returns>True if the request was handled; otherwise false.</returns>
public virtual bool BringIntoView(IVisual target, Rect targetRect)
{ {
return false;
} }
/// <summary> /// <summary>
@ -157,5 +157,16 @@ namespace Avalonia.Controls.Presenters
Items = items; Items = items;
ItemCount = items.Count(); ItemCount = items.Count();
} }
/// <summary>
/// Called when a request is made to bring an item into view.
/// </summary>
/// <param name="target">The item to bring into view.</param>
/// <param name="targetRect">The rect on the item to bring into view.</param>
/// <returns>True if the request was handled; otherwise false.</returns>
public virtual bool BringIntoView(IVisual target, Rect targetRect)
{
return false;
}
} }
} }

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

@ -52,12 +52,6 @@ namespace Avalonia.Controls.Presenters
get { throw new NotSupportedException(); } get { throw new NotSupportedException(); }
} }
/// <inheritdoc/>
public override void Arranging(Size finalSize)
{
// We don't need to do anything here.
}
/// <inheritdoc/> /// <inheritdoc/>
public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
{ {

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

@ -84,7 +84,7 @@ namespace Avalonia.Controls.Presenters
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Arranging(Size finalSize) public override void UpdateControls()
{ {
CreateAndRemoveContainers(); CreateAndRemoveContainers();
((ILogicalScrollable)Owner).InvalidateScroll(); ((ILogicalScrollable)Owner).InvalidateScroll();

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

@ -78,14 +78,6 @@ namespace Avalonia.Controls.Presenters
return _virtualizer?.BringIntoView(target, targetRect) ?? false; return _virtualizer?.BringIntoView(target, targetRect) ?? false;
} }
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
_virtualizer.Arranging(finalSize);
return result;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void PanelCreated(IPanel panel) protected override void PanelCreated(IPanel panel)
{ {

38
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -4,11 +4,13 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel
{ {
private Size _availableSpace;
private double _takenSpace; private double _takenSpace;
private int _canBeRemoved; private int _canBeRemoved;
private double _averageItemSize; private double _averageItemSize;
@ -20,15 +22,14 @@ namespace Avalonia.Controls
get get
{ {
return Orientation == Orientation.Horizontal ? return Orientation == Orientation.Horizontal ?
_takenSpace >= AvailableSpace.Width : _takenSpace >= _availableSpace.Width :
_takenSpace >= AvailableSpace.Height; _takenSpace >= _availableSpace.Height;
} }
} }
IVirtualizingController IVirtualizingPanel.Controller { get; set; }
int IVirtualizingPanel.OverflowCount => _canBeRemoved; int IVirtualizingPanel.OverflowCount => _canBeRemoved;
Orientation IVirtualizingPanel.ScrollDirection => Orientation; Orientation IVirtualizingPanel.ScrollDirection => Orientation;
double IVirtualizingPanel.AverageItemSize => _averageItemSize; double IVirtualizingPanel.AverageItemSize => _averageItemSize;
double IVirtualizingPanel.PixelOverflow double IVirtualizingPanel.PixelOverflow
@ -55,16 +56,30 @@ namespace Avalonia.Controls
} }
} }
// TODO: We need to put a reasonable limit on this, probably based on the max window size. private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller;
private Size AvailableSpace => ((ILayoutable)this).PreviousMeasure ?? Bounds.Size;
protected override Size MeasureOverride(Size availableSize)
{
if (availableSize != ((ILayoutable)this).PreviousMeasure)
{
// TODO: We need to put a reasonable limit on this, probably based on the max
// window size.
_availableSpace = availableSize;
Controller?.UpdateControls();
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
_availableSpace = finalSize;
_canBeRemoved = 0; _canBeRemoved = 0;
_takenSpace = 0; _takenSpace = 0;
_averageItemSize = 0; _averageItemSize = 0;
_averageCount = 0; _averageCount = 0;
var result = base.ArrangeOverride(finalSize); var result = base.ArrangeOverride(finalSize);
Controller?.UpdateControls();
return result; return result;
} }
@ -103,7 +118,7 @@ namespace Avalonia.Controls
rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height);
child.Arrange(rect); child.Arrange(rect);
if (rect.Y >= AvailableSpace.Height) if (rect.Y >= _availableSpace.Height)
{ {
++_canBeRemoved; ++_canBeRemoved;
} }
@ -120,7 +135,7 @@ namespace Avalonia.Controls
rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height);
child.Arrange(rect); child.Arrange(rect);
if (rect.X >= AvailableSpace.Width) if (rect.X >= _availableSpace.Width)
{ {
++_canBeRemoved; ++_canBeRemoved;
} }
@ -139,7 +154,7 @@ namespace Avalonia.Controls
var bounds = Bounds; var bounds = Bounds;
var gap = Gap; var gap = Gap;
child.Measure(AvailableSpace); child.Measure(_availableSpace);
++_averageCount; ++_averageCount;
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
@ -173,6 +188,11 @@ namespace Avalonia.Controls
_takenSpace -= width + gap; _takenSpace -= width + gap;
RemoveFromAverageItemSize(width); RemoveFromAverageItemSize(width);
} }
if (_canBeRemoved > 0)
{
--_canBeRemoved;
}
} }
private void AddToAverageItemSize(double value) private void AddToAverageItemSize(double value)

2
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests
// and re-arrange everything. // and re-arrange everything.
foreach (IControl i in target.GetSelfAndVisualDescendents()) foreach (IControl i in target.GetSelfAndVisualDescendents())
{ {
i.InvalidateArrange(); i.InvalidateMeasure();
} }
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));

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

@ -65,9 +65,11 @@ namespace Avalonia.Controls.UnitTests.Presenters
var target = CreateTarget(); var target = CreateTarget();
target.ApplyTemplate(); target.ApplyTemplate();
target.Measure(new Size(100, 100)); 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, 100));
Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(10, target.Panel.Children.Count);
} }
@ -83,6 +85,20 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(2, target.Panel.Children.Count); Assert.Equal(2, target.Panel.Children.Count);
} }
[Fact]
public void Should_Expand_To_Fit_Containers_When_Flexible_Size()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(10, 200), target.DesiredSize);
Assert.Equal(new Size(10, 200), target.Bounds.Size);
Assert.Equal(20, target.Panel.Children.Count);
}
[Fact] [Fact]
public void Initial_Item_DataContexts_Should_Be_Correct() public void Initial_Item_DataContexts_Should_Be_Correct()
{ {

32
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.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 Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
@ -9,25 +10,52 @@ namespace Avalonia.Controls.UnitTests
{ {
public class Vertical public class Vertical
{ {
[Fact]
public void Measure_Invokes_Controller_UpdateControls()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>();
target.Controller = controller.Object;
target.Measure(new Size(100, 100));
controller.Verify(x => x.UpdateControls());
}
[Fact]
public void Arrange_Invokes_Controller_UpdateControls()
{
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
var controller = new Mock<IVirtualizingController>();
target.Controller = controller.Object;
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 110, 110));
controller.Verify(x => x.UpdateControls());
}
[Fact] [Fact]
public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() public void Reports_IsFull_False_Until_Measure_Height_Is_Reached()
{ {
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); var target = (IVirtualizingPanel)new VirtualizingStackPanel();
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(0, 0), target.Bounds.Size); Assert.Equal(new Size(0, 0), target.Bounds.Size);
Assert.False(target.IsFull); Assert.False(target.IsFull);
Assert.Equal(0, target.OverflowCount);
target.Children.Add(new Canvas { Width = 50, Height = 50 }); target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.False(target.IsFull); Assert.False(target.IsFull);
Assert.Equal(0, target.OverflowCount);
target.Children.Add(new Canvas { Width = 50, Height = 50 }); target.Children.Add(new Canvas { Width = 50, Height = 50 });
Assert.True(target.IsFull); Assert.True(target.IsFull);
Assert.Equal(0, target.OverflowCount);
} }
[Fact] [Fact]
public void Reports_Overflow_Only_After_Arrange() public void Reports_Overflow_After_Arrange()
{ {
var target = (IVirtualizingPanel)new VirtualizingStackPanel(); var target = (IVirtualizingPanel)new VirtualizingStackPanel();

Loading…
Cancel
Save