diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.cs b/samples/XamlTestApplicationPcl/Views/MainWindow.cs index 3df8bdeea3..ed5cb8f73a 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.cs +++ b/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("exitMenu"); _exitMenu.Click += (s, e) => Application.Current.Exit(); - var vadd = this.FindControl - + diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index f771a6b9fe..ca7ffae145 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -11,7 +11,5 @@ namespace Avalonia.Controls double AverageItemSize { get; } double PixelOffset { get; set; } - - Action ArrangeCompleted { get; set; } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3a77f61f1b..ef090bc697 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -25,7 +25,6 @@ namespace Avalonia.Controls /// /// The default value for the property. /// - [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")] private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 0f32bc91da..f61e2fa8d3 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/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 /// public class ListBox : SelectingItemsControl { + /// + /// The default value for the property. + /// + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new VirtualizingStackPanel()); + /// /// Defines the property. /// @@ -28,6 +35,14 @@ namespace Avalonia.Controls public static readonly new AvaloniaProperty SelectionModeProperty = SelectingItemsControl.SelectionModeProperty; + /// + /// Initializes static members of the class. + /// + static ListBox() + { + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + } + /// public new IList SelectedItems => base.SelectedItems; diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 29488c8b80..2b7442597b 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters } /// - protected override void CreatePanel() + protected override void PanelCreated(IPanel panel) { - base.CreatePanel(); var task = MoveToPage(-1, SelectedIndex); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 1c4443811d..21d2bcada9 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/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 /// Action IScrollable.InvalidateScroll { get; set; } + /// Size IScrollable.Extent { get @@ -69,39 +71,53 @@ namespace Avalonia.Controls.Presenters } } + /// Vector IScrollable.Offset { get; set; } + /// 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(); - } - } + /// + Size IScrollable.ScrollSize => new Size(0, 1); + + /// + Size IScrollable.PageScrollSize => new Size(0, 1); - Size IScrollable.PageScrollSize + /// + protected override Size ArrangeOverride(Size finalSize) { - get + var result = base.ArrangeOverride(finalSize); + + if (_virt != null) { - throw new NotImplementedException(); + CreateRemoveVirtualizedContainers(); + ((IScrollable)this).InvalidateScroll(); + } + + return result; } /// - 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)); } - /// 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 AddContainers(int index, IEnumerable items) + private void ItemsChangedVirtualized(NotifyCollectionChangedEventArgs e) + { + } + + private IList AddContainersNonVirtualized(int index, IEnumerable items) { var generator = ItemContainerGenerator; var result = new List(); @@ -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().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 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; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 718c6f018f..5fbed4d553 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/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; } + /// + /// Called when the is created. + /// + /// The panel. + protected virtual void PanelCreated(IPanel panel) + { + } + + /// + /// Called when the items for the presenter change, either because + /// has been set, the items collection has been modified, or the panel has been created. + /// + /// A description of the change. + protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); + /// /// Creates the when is called for the first /// time. /// - 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)); } - /// - /// Called when the items for the presenter change, either because - /// has been set, the items collection has been modified, or the panel has been created. - /// - /// A description of the change. - protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); - /// /// Called when the collection changes. /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index abb606435d..5d295d7fc8 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/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(); diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index 2171e87c51..c4d9dc34c8 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/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(); } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 1364bb1250..865ad5d895 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/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; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index ff602bbbb4..aba9dbb7cf 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/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)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)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)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(x => new TextBlock + return new FuncDataTemplate(x => new Canvas { - Text = x, Height = 10, }); }