From 5d1f9f4a0f387589b2aecd585e49a43b45b859d3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 25 Nov 2022 20:39:27 +0100 Subject: [PATCH] Removed VirtualizationMode. If you want a different virtualization mode, use a different panel. --- src/Avalonia.Controls/ComboBox.cs | 15 - .../ItemVirtualizationMode.cs | 23 - src/Avalonia.Controls/ListBox.cs | 16 - .../VirtualizingStackPanel.Smooth.cs | 662 ----------------- .../VirtualizingStackPanel.cs | 668 ++++++++++++++++-- .../Diagnostics/Views/ConsoleView.xaml | 3 +- .../Controls/ManagedFileChooser.xaml | 1 - .../Controls/ManagedFileChooser.xaml | 3 +- .../ComboBoxTests.cs | 1 - .../ListBoxTests.cs | 6 - .../Primitives/SelectingItemsControlTests.cs | 1 - .../VirtualizingStackPanelTests_Smooth.cs | 1 - tests/Avalonia.LeakTests/ControlTests.cs | 1 - .../Xaml/StyleTests.cs | 1 - 14 files changed, 617 insertions(+), 785 deletions(-) delete mode 100644 src/Avalonia.Controls/ItemVirtualizationMode.cs delete mode 100644 src/Avalonia.Controls/VirtualizingStackPanel.Smooth.cs diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f8b0ebbbce..41c17bd3e1 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -53,12 +53,6 @@ namespace Avalonia.Controls public static readonly DirectProperty SelectionBoxItemProperty = AvaloniaProperty.RegisterDirect(nameof(SelectionBoxItem), o => o.SelectionBoxItem); - /// - /// Defines the property. - /// - public static readonly StyledProperty VirtualizationModeProperty = - VirtualizingStackPanel.VirtualizationModeProperty.AddOwner(); - /// /// Defines the property. /// @@ -146,15 +140,6 @@ namespace Avalonia.Controls set { SetValue(PlaceholderForegroundProperty, value); } } - /// - /// Gets or sets the virtualization mode for the items. - /// - public ItemVirtualizationMode VirtualizationMode - { - get { return GetValue(VirtualizationModeProperty); } - set { SetValue(VirtualizationModeProperty, value); } - } - /// /// Gets or sets the horizontal alignment of the content within the control. /// diff --git a/src/Avalonia.Controls/ItemVirtualizationMode.cs b/src/Avalonia.Controls/ItemVirtualizationMode.cs deleted file mode 100644 index 66dd32d28c..0000000000 --- a/src/Avalonia.Controls/ItemVirtualizationMode.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Avalonia.Controls -{ - /// - /// Describes the item virtualization method to use for a list. - /// - public enum ItemVirtualizationMode - { - /// - /// Do not virtualize items. - /// - None, - - /// - /// Virtualize items without smooth scrolling. - /// - Simple, - - /// - /// Virtualize items with smooth scrolling. - /// - Smooth, - } -} diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index cb9f2f42ef..a8a926691a 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -47,12 +47,6 @@ namespace Avalonia.Controls public static readonly new StyledProperty SelectionModeProperty = SelectingItemsControl.SelectionModeProperty; - /// - /// Defines the property. - /// - public static readonly StyledProperty VirtualizationModeProperty = - VirtualizingStackPanel.VirtualizationModeProperty.AddOwner(); - private IScrollable? _scroll; /// @@ -61,7 +55,6 @@ namespace Avalonia.Controls static ListBox() { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); - VirtualizationModeProperty.OverrideDefaultValue(ItemVirtualizationMode.Smooth); } /// @@ -100,15 +93,6 @@ namespace Avalonia.Controls set { base.SelectionMode = value; } } - /// - /// Gets or sets the virtualization mode for the items. - /// - public ItemVirtualizationMode VirtualizationMode - { - get { return GetValue(VirtualizationModeProperty); } - set { SetValue(VirtualizationModeProperty, value); } - } - /// /// Selects all items in the . /// diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.Smooth.cs b/src/Avalonia.Controls/VirtualizingStackPanel.Smooth.cs deleted file mode 100644 index 1f6d8319fb..0000000000 --- a/src/Avalonia.Controls/VirtualizingStackPanel.Smooth.cs +++ /dev/null @@ -1,662 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using Avalonia.Controls.Utils; -using Avalonia.Layout; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Controls -{ - public partial class VirtualizingStackPanel : VirtualizingPanel - { - private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0); - private readonly Action _recycleElement; - private readonly Action _updateElementIndex; - private int _anchorIndex = -1; - private Control? _anchorElement; - private bool _isWaitingForViewportUpdate; - private double _lastEstimatedElementSizeU = 25; - private RealizedElementList? _measureElements; - private RealizedElementList? _realizedElements; - private Rect _viewport = s_invalidViewport; - - private Size MeasureOverrideSmooth(Size availableSize) - { - if (!IsEffectivelyVisible) - return default; - - var items = ItemsControl?.Items as IList; - - if (items is null || items.Count == 0) - { - Children.Clear(); - return default; - } - - var orientation = Orientation; - - _realizedElements ??= new(); - _measureElements ??= new(); - - // If we're bringing an item into view, ignore any layout passes until we receive a new - // effective viewport. - if (_isWaitingForViewportUpdate) - { - var sizeV = orientation == Orientation.Horizontal ? DesiredSize.Height : DesiredSize.Width; - return CalculateDesiredSize(orientation, items, sizeV); - } - - // We handle horizontal and vertical layouts here so X and Y are abstracted to: - // - Horizontal layouts: U = horizontal, V = vertical - // - Vertical layouts: U = vertical, V = horizontal - var viewport = CalculateMeasureViewport(items); - - // Recycle elements outside of the expected range. - _realizedElements.RecycleElementsBefore(viewport.firstIndex, _recycleElement); - _realizedElements.RecycleElementsAfter(viewport.estimatedLastIndex, _recycleElement); - - // Do the measure, creating/recycling elements as necessary to fill the viewport. Don't - // write to _realizedElements yet, only _measureElements. - GenerateElements(availableSize, ref viewport); - - // Now we know what definitely fits, recycle anything left over. - _realizedElements.RecycleElementsAfter(_measureElements.LastModelIndex, _recycleElement); - - // And swap the measureElements and realizedElements collection. - (_measureElements, _realizedElements) = (_realizedElements, _measureElements); - _measureElements.ResetForReuse(); - - return CalculateDesiredSize(orientation, items, viewport.measuredV); - } - - private Size ArrangeOverrideSmooth(Size finalSize) - { - Debug.Assert(_realizedElements is not null); - - var orientation = Orientation; - var u = _realizedElements!.StartU; - - for (var i = 0; i < _realizedElements.Count; ++i) - { - var e = _realizedElements.Elements[i]; - - if (e is object) - { - var sizeU = _realizedElements.SizeU[i]; - var rect = orientation == Orientation.Horizontal ? - new Rect(u, 0, sizeU, finalSize.Height) : - new Rect(0, u, finalSize.Width, sizeU); - e.Arrange(rect); - u += orientation == Orientation.Horizontal ? rect.Width : rect.Height; - } - } - - return finalSize; - } - - private MeasureViewport CalculateMeasureViewport(IList items) - { - Debug.Assert(_realizedElements is not null); - - // If the control has not yet been laid out then the effective viewport won't have been set. - // Try to work it out from an ancestor control. - var viewport = _viewport != s_invalidViewport ? _viewport : EstimateViewport(); - - // Get the viewport in the orientation direction. - var viewportStart = Orientation == Orientation.Horizontal ? viewport.X : viewport.Y; - var viewportEnd = Orientation == Orientation.Horizontal ? viewport.Right : viewport.Bottom; - - var (firstIndex, firstIndexU) = _realizedElements!.GetElementAt(viewportStart); - var (lastIndex, _) = _realizedElements.GetElementAt(viewportEnd); - var estimatedElementSize = -1.0; - var itemCount = items?.Count ?? 0; - - if (firstIndex == -1) - { - estimatedElementSize = EstimateElementSizeU(); - firstIndex = (int)(viewportStart / estimatedElementSize); - firstIndexU = firstIndex * estimatedElementSize; - } - - if (lastIndex == -1) - { - if (estimatedElementSize == -1) - estimatedElementSize = EstimateElementSizeU(); - lastIndex = (int)(viewportEnd / estimatedElementSize); - } - - return new MeasureViewport - { - firstIndex = MathUtilities.Clamp(firstIndex, 0, itemCount - 1), - estimatedLastIndex = MathUtilities.Clamp(lastIndex, 0, itemCount - 1), - viewportUStart = viewportStart, - viewportUEnd = viewportEnd, - startU = firstIndexU, - }; - } - - private Size CalculateDesiredSize(Orientation orientation, IList items, double sizeV) - { - var sizeU = EstimateElementSizeU() * items.Count; - - if (double.IsInfinity(sizeU) || double.IsNaN(sizeU)) - throw new InvalidOperationException("Invalid calculated size."); - - return orientation == Orientation.Horizontal ? - new Size(sizeU, sizeV) : - new Size(sizeV, sizeU); - } - - private double EstimateElementSizeU() - { - var count = _realizedElements!.Count; - var divisor = 0.0; - var total = 0.0; - - for (var i = 0; i < count; ++i) - { - if (_realizedElements.Elements[i] is object) - { - total += _realizedElements.SizeU[i]; - ++divisor; - } - } - - if (divisor == 0 || total == 0) - return _lastEstimatedElementSizeU; - - _lastEstimatedElementSizeU = total / divisor; - return _lastEstimatedElementSizeU; - } - - private Rect EstimateViewport() - { - var c = this.GetVisualParent(); - var viewport = new Rect(); - - if (c is null) - { - return viewport; - } - - while (c is not null) - { - if (!c.Bounds.IsEmpty && c.TransformToVisual(this) is Matrix transform) - { - viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height) - .TransformToAABB(transform); - break; - } - - c = c?.GetVisualParent(); - } - - - return viewport; - } - - private void GenerateElements(Size availableSize, ref MeasureViewport viewport) - { - Debug.Assert(ItemsControl?.Items is IList); - Debug.Assert(_measureElements is not null); - - var items = (IList)ItemsControl!.Items; - var horizontal = Orientation == Orientation.Horizontal; - var index = viewport.firstIndex; - var u = viewport.startU; - - do - { - var e = GetOrCreateElement(items, index); - e.Measure(availableSize); - - var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height; - var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width; - - _measureElements!.Add(index, e, u, sizeU); - viewport.measuredV = Math.Max(viewport.measuredV, sizeV); - - u += sizeU; - ++index; - } while (u < viewport.viewportUEnd && index < items.Count); - } - - private Control GetOrCreateElement(IList items, int index) - { - var e = GetRealizedElement(index) ?? GetRecycledOrCreateElement(items, index); - InvalidateHack(e); - return e; - } - - private Control? GetRealizedElement(int index) - { - Debug.Assert(_realizedElements is not null); - - if (_anchorIndex == index) - return _anchorElement; - return _realizedElements!.GetElement(index); - } - - private Control GetRecycledOrCreateElement(IList items, int index) - { - Debug.Assert(ItemsControl is not null); - - var c = ItemsControl!.ItemContainerGenerator.Materialize(index, items[index]!).ContainerControl; - Children.Add(c); - return c; - } - - private void RecycleElement(Control element) - { - Debug.Assert(ItemsControl is not null); - - var index = ItemsControl.ItemContainerGenerator!.IndexFromContainer(element); - ItemsControl!.ItemContainerGenerator.Dematerialize(index, 1); - Children.Remove(element); - } - - private void UpdateElementIndex(Control element, int index) - { - // TODO: Implement this after we refactor ItemContainerGenerator. - } - - private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) - { - _viewport = e.EffectiveViewport; - _isWaitingForViewportUpdate = false; - InvalidateMeasure(); - } - - private void OnItemsChangedSmooth(NotifyCollectionChangedEventArgs e) - { - if (_realizedElements is null) - return; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - _realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex); - ItemsControl!.ItemContainerGenerator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count); - break; - case NotifyCollectionChangedAction.Remove: - _realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElement); - ItemsControl!.ItemContainerGenerator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count); - break; - case NotifyCollectionChangedAction.Reset: - ////RecycleAllElements(); - break; - } - - InvalidateMeasure(); - } - - private static void InvalidateHack(Control c) - { - bool HasInvalidations(Control c) - { - if (!c.IsMeasureValid) - return true; - - for (var i = 0; i < c.VisualChildren.Count; ++i) - { - if (c.VisualChildren[i] is Control child) - { - if (!child.IsMeasureValid || HasInvalidations(child)) - return true; - } - } - - return false; - } - - void Invalidate(Control c) - { - c.InvalidateMeasure(); - for (var i = 0; i < c.VisualChildren.Count; ++i) - { - if (c.VisualChildren[i] is Control child) - Invalidate(child); - } - } - - if (HasInvalidations(c)) - Invalidate(c); - } - - /// - /// Stores the realized element state for a in smooth mode. - /// - internal class RealizedElementList - { - private int _firstIndex; - private List? _elements; - private List? _sizes; - private double _startU; - - /// - /// Gets the number of realized elements. - /// - public int Count => _elements?.Count ?? 0; - - /// - /// Gets the model index of the first realized element, or -1 if no elements are realized. - /// - public int FirstModelIndex => _elements?.Count > 0 ? _firstIndex : -1; - - /// - /// Gets the model index of the last realized element, or -1 if no elements are realized. - /// - public int LastModelIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1; - - /// - /// Gets the elements. - /// - public IReadOnlyList Elements => _elements ??= new List(); - - /// - /// Gets the sizes of the elements on the primary axis. - /// - public IReadOnlyList SizeU => _sizes ??= new List(); - - /// - /// Gets the position of the first element on the primary axis. - /// - public double StartU => _startU; - - /// - /// Adds a newly realized element to the collection. - /// - /// The model index of the element. - /// The element. - /// The position of the elemnt on the primary axis. - /// The size of the element on the primary axis. - public void Add(int modelIndex, Control element, double u, double sizeU) - { - if (modelIndex < 0) - throw new ArgumentOutOfRangeException(nameof(modelIndex)); - - _elements ??= new List(); - _sizes ??= new List(); - - if (Count == 0) - { - _elements.Add(element); - _sizes.Add(sizeU); - _startU = u; - _firstIndex = modelIndex; - } - else if (modelIndex == LastModelIndex + 1) - { - _elements.Add(element); - _sizes.Add(sizeU); - } - else if (modelIndex == FirstModelIndex - 1) - { - --_firstIndex; - _elements.Insert(0, element); - _sizes.Insert(0, sizeU); - _startU = u; - } - else - { - throw new NotSupportedException("Can only add items to the beginning or end of realized elements."); - } - } - - /// - /// Gets the element at the specified model index, if realized. - /// - /// The index in the source collection of the element to get. - /// The element if realized; otherwise null. - public Control? GetElement(int modelIndex) - { - var index = modelIndex - FirstModelIndex; - if (index >= 0 && index < _elements?.Count) - return _elements[index]; - return null; - } - - /// - /// Gets the element at the specified position on the primary axis, if realized. - /// - /// The position. - /// - /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the - /// primary axis. - /// - public (int index, double position) GetElementAt(double position) - { - if (_sizes is null || position < StartU) - return (-1, 0); - - var u = StartU; - var i = FirstModelIndex; - - foreach (var size in _sizes) - { - var endU = u + size; - if (position < endU) - return (i, u); - u += size; - ++i; - } - - return (-1, 0); - } - - /// - /// Updates the elements in response to items being inserted into the source collection. - /// - /// The index in the source collection of the insert. - /// The number of items inserted. - /// A method used to update the element indexes. - public void ItemsInserted(int modelIndex, int count, Action updateElementIndex) - { - if (modelIndex < 0) - throw new ArgumentOutOfRangeException(nameof(modelIndex)); - if (_elements is null || _elements.Count == 0) - return; - - // Get the index within the realized _elements collection. - var first = FirstModelIndex; - var index = modelIndex - first; - - if (index < Count) - { - // The insertion point affects the realized elements. Update the index of the - // elements after the insertion point. - var elementCount = _elements.Count; - var start = Math.Max(index, 0); - - for (var i = start; i < elementCount; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, first + i + count); - } - - if (index <= 0) - { - // The insertion point was before the first element, update the first index. - _firstIndex += count; - } - else - { - // The insertion point was within the realized elements, insert an empty space - // in _elements and _sizes. - _elements!.InsertMany(index, null, count); - _sizes!.InsertMany(index, 0.0, count); - } - } - } - - /// - /// Updates the elements in response to items being removed from the source collection. - /// - /// The index in the source collection of the remove. - /// The number of items removed. - /// A method used to update the element indexes. - /// A method used to recycle elements. - public void ItemsRemoved( - int modelIndex, - int count, - Action updateElementIndex, - Action recycleElement) - { - if (modelIndex < 0) - throw new ArgumentOutOfRangeException(nameof(modelIndex)); - if (_elements is null || _elements.Count == 0) - return; - - // Get the removal start and end index within the realized _elements collection. - var first = FirstModelIndex; - var last = LastModelIndex; - var startIndex = modelIndex - first; - var endIndex = (modelIndex + count) - first; - - if (endIndex < 0) - { - // The removed range was before the realized elements. Update the first index and - // the indexes of the realized elements. - _firstIndex -= count; - - for (var i = 0; i < _elements.Count; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, _firstIndex + i); - } - } - else if (startIndex < _elements.Count) - { - // Recycle and remove the affected elements. - var start = Math.Max(startIndex, 0); - var end = Math.Min(endIndex, _elements.Count); - - for (var i = start; i < end; ++i) - { - if (_elements[i] is Control element) - recycleElement(element); - } - - _elements.RemoveRange(start, end - start); - _sizes!.RemoveRange(start, end - start); - - // If the remove started before and ended within our realized elements, then our new - // first index will be the index where the remove started. - if (startIndex <= 0 && end < last) - _firstIndex = first = modelIndex; - - // Update the indexes of the elements after the removed range. - end = _elements.Count; - for (var i = start; i < end; ++i) - { - if (_elements[i] is Control element) - updateElementIndex(element, first + i); - } - } - } - - /// - /// Recycles elements before a specific index. - /// - /// The index in the source collection of new first element. - /// A method used to recycle elements. - public void RecycleElementsBefore(int modelIndex, Action recycleElement) - { - if (modelIndex <= FirstModelIndex || _elements is null || _elements.Count == 0) - return; - - if (modelIndex > LastModelIndex) - { - RecycleAllElements(recycleElement); - } - else - { - var endIndex = modelIndex - FirstModelIndex; - - for (var i = 0; i < endIndex; ++i) - { - if (_elements[i] is Control e) - recycleElement(e); - } - - _elements.RemoveRange(0, endIndex); - _sizes!.RemoveRange(0, endIndex); - _firstIndex = modelIndex; - } - } - - /// - /// Recycles elements after a specific index. - /// - /// The index in the source collection of new last element. - /// A method used to recycle elements. - public void RecycleElementsAfter(int modelIndex, Action recycleElement) - { - if (modelIndex >= LastModelIndex || _elements is null || _elements.Count == 0) - return; - - if (modelIndex < FirstModelIndex) - { - RecycleAllElements(recycleElement); - } - else - { - var startIndex = (modelIndex + 1) - FirstModelIndex; - var count = _elements.Count; - - for (var i = startIndex; i < count; ++i) - { - if (_elements[i] is Control e) - recycleElement(e); - } - - _elements.RemoveRange(startIndex, _elements.Count - startIndex); - _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex); - } - } - - /// - /// Recycles all realized elements. - /// - /// A method used to recycle elements. - public void RecycleAllElements(Action recycleElement) - { - if (_elements is null || _elements.Count == 0) - return; - - foreach (var e in _elements) - { - if (e is object) - recycleElement(e); - } - - _startU = _firstIndex = 0; - _elements?.Clear(); - _sizes?.Clear(); - } - - /// - /// Resets the element list and prepares it for reuse. - /// - public void ResetForReuse() - { - _startU = _firstIndex = 0; - _elements?.Clear(); - _sizes?.Clear(); - } - } - - private struct MeasureViewport - { - public int firstIndex; - public int estimatedLastIndex; - public double viewportUStart; - public double viewportUEnd; - public double measuredV; - public double startU; - } - } -} diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 78839c44b8..5d9b3b56b1 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -2,11 +2,15 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; +using Avalonia.Controls.Utils; using Avalonia.Layout; +using Avalonia.Utilities; +using Avalonia.VisualTree; namespace Avalonia.Controls { - public partial class VirtualizingStackPanel : VirtualizingPanel + public class VirtualizingStackPanel : VirtualizingPanel { /// /// Defines the property. @@ -14,19 +18,22 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = StackLayout.OrientationProperty.AddOwner(); - /// - /// Defines the VirtualizationMode attached property. - /// - public static readonly AttachedProperty VirtualizationModeProperty = - AvaloniaProperty.RegisterAttached( - "VirtualizationMode", ItemVirtualizationMode.None); - - private ItemVirtualizationMode _virtualizationMode; + private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0); + private readonly Action _recycleElement; + private readonly Action _updateElementIndex; + private int _anchorIndex = -1; + private Control? _anchorElement; + private bool _isWaitingForViewportUpdate; + private double _lastEstimatedElementSizeU = 25; + private RealizedElementList? _measureElements; + private RealizedElementList? _realizedElements; + private Rect _viewport = s_invalidViewport; public VirtualizingStackPanel() { _recycleElement = RecycleElement; _updateElementIndex = UpdateElementIndex; + EffectiveViewportChanged += OnEffectiveViewportChanged; } /// @@ -42,90 +49,645 @@ namespace Avalonia.Controls set => SetValue(OrientationProperty, value); } - /// - /// Gets the current in use for the panel. - /// - public ItemVirtualizationMode VirtualizationMode + protected override Size MeasureOverride(Size availableSize) { - get => _virtualizationMode; - set + if (!IsEffectivelyVisible) + return default; + + var items = ItemsControl?.Items as IList; + + if (items is null || items.Count == 0) { - if (_virtualizationMode != value) - { - if (_virtualizationMode == ItemVirtualizationMode.Smooth) - EffectiveViewportChanged -= OnEffectiveViewportChanged; + Children.Clear(); + return default; + } + + var orientation = Orientation; + + _realizedElements ??= new(); + _measureElements ??= new(); + + // If we're bringing an item into view, ignore any layout passes until we receive a new + // effective viewport. + if (_isWaitingForViewportUpdate) + { + var sizeV = orientation == Orientation.Horizontal ? DesiredSize.Height : DesiredSize.Width; + return CalculateDesiredSize(orientation, items, sizeV); + } + + // We handle horizontal and vertical layouts here so X and Y are abstracted to: + // - Horizontal layouts: U = horizontal, V = vertical + // - Vertical layouts: U = vertical, V = horizontal + var viewport = CalculateMeasureViewport(items); + + // Recycle elements outside of the expected range. + _realizedElements.RecycleElementsBefore(viewport.firstIndex, _recycleElement); + _realizedElements.RecycleElementsAfter(viewport.estimatedLastIndex, _recycleElement); + + // Do the measure, creating/recycling elements as necessary to fill the viewport. Don't + // write to _realizedElements yet, only _measureElements. + GenerateElements(availableSize, ref viewport); + + // Now we know what definitely fits, recycle anything left over. + _realizedElements.RecycleElementsAfter(_measureElements.LastModelIndex, _recycleElement); + + // And swap the measureElements and realizedElements collection. + (_measureElements, _realizedElements) = (_realizedElements, _measureElements); + _measureElements.ResetForReuse(); + + return CalculateDesiredSize(orientation, items, viewport.measuredV); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Debug.Assert(_realizedElements is not null); - _virtualizationMode = value; + var orientation = Orientation; + var u = _realizedElements!.StartU; - if (_virtualizationMode == ItemVirtualizationMode.Smooth) - EffectiveViewportChanged += OnEffectiveViewportChanged; + for (var i = 0; i < _realizedElements.Count; ++i) + { + var e = _realizedElements.Elements[i]; - Children.Clear(); + if (e is object) + { + var sizeU = _realizedElements.SizeU[i]; + var rect = orientation == Orientation.Horizontal ? + new Rect(u, 0, sizeU, finalSize.Height) : + new Rect(0, u, finalSize.Width, sizeU); + e.Arrange(rect); + u += orientation == Orientation.Horizontal ? rect.Width : rect.Height; } } + + return finalSize; } - public static ItemVirtualizationMode GetVirtualizationMode(Control c) + protected override void OnItemsChanged(IList items, NotifyCollectionChangedEventArgs e) { - return c.GetValue(VirtualizationModeProperty); + if (_realizedElements is null) + return; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + _realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex); + ItemsControl!.ItemContainerGenerator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count); + break; + case NotifyCollectionChangedAction.Remove: + _realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElement); + ItemsControl!.ItemContainerGenerator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count); + break; + case NotifyCollectionChangedAction.Reset: + ////RecycleAllElements(); + break; + } + + InvalidateMeasure(); } - public static void SetVirtualizationMode(Control c, ItemVirtualizationMode value) + internal IReadOnlyList GetRealizedElements() { - c.SetValue(VirtualizationModeProperty, value); + return _realizedElements?.Elements ?? Array.Empty(); } - protected override Size MeasureOverride(Size availableSize) + private MeasureViewport CalculateMeasureViewport(IList items) { - return VirtualizationMode switch + Debug.Assert(_realizedElements is not null); + + // If the control has not yet been laid out then the effective viewport won't have been set. + // Try to work it out from an ancestor control. + var viewport = _viewport != s_invalidViewport ? _viewport : EstimateViewport(); + + // Get the viewport in the orientation direction. + var viewportStart = Orientation == Orientation.Horizontal ? viewport.X : viewport.Y; + var viewportEnd = Orientation == Orientation.Horizontal ? viewport.Right : viewport.Bottom; + + var (firstIndex, firstIndexU) = _realizedElements!.GetElementAt(viewportStart); + var (lastIndex, _) = _realizedElements.GetElementAt(viewportEnd); + var estimatedElementSize = -1.0; + var itemCount = items?.Count ?? 0; + + if (firstIndex == -1) + { + estimatedElementSize = EstimateElementSizeU(); + firstIndex = (int)(viewportStart / estimatedElementSize); + firstIndexU = firstIndex * estimatedElementSize; + } + + if (lastIndex == -1) + { + if (estimatedElementSize == -1) + estimatedElementSize = EstimateElementSizeU(); + lastIndex = (int)(viewportEnd / estimatedElementSize); + } + + return new MeasureViewport { - ItemVirtualizationMode.Smooth => MeasureOverrideSmooth(availableSize), - _ => throw new NotImplementedException() + firstIndex = MathUtilities.Clamp(firstIndex, 0, itemCount - 1), + estimatedLastIndex = MathUtilities.Clamp(lastIndex, 0, itemCount - 1), + viewportUStart = viewportStart, + viewportUEnd = viewportEnd, + startU = firstIndexU, }; } - protected override Size ArrangeOverride(Size finalSize) + private Size CalculateDesiredSize(Orientation orientation, IList items, double sizeV) { - return VirtualizationMode switch - { - ItemVirtualizationMode.Smooth => ArrangeOverrideSmooth(finalSize), - _ => throw new NotImplementedException() - }; + var sizeU = EstimateElementSizeU() * items.Count; + + if (double.IsInfinity(sizeU) || double.IsNaN(sizeU)) + throw new InvalidOperationException("Invalid calculated size."); + + return orientation == Orientation.Horizontal ? + new Size(sizeU, sizeV) : + new Size(sizeV, sizeU); } - protected override void OnItemsControlChanged(ItemsControl? oldValue) + private double EstimateElementSizeU() { - base.OnItemsControlChanged(oldValue); - VirtualizationMode = ItemsControl is null ? ItemVirtualizationMode.None : GetVirtualizationMode(ItemsControl); + var count = _realizedElements!.Count; + var divisor = 0.0; + var total = 0.0; + + for (var i = 0; i < count; ++i) + { + if (_realizedElements.Elements[i] is object) + { + total += _realizedElements.SizeU[i]; + ++divisor; + } + } + + if (divisor == 0 || total == 0) + return _lastEstimatedElementSizeU; + + _lastEstimatedElementSizeU = total / divisor; + return _lastEstimatedElementSizeU; } - protected override void OnItemsChanged(IList items, NotifyCollectionChangedEventArgs e) + private Rect EstimateViewport() { - switch (VirtualizationMode) + var c = this.GetVisualParent(); + var viewport = new Rect(); + + if (c is null) { - case ItemVirtualizationMode.Smooth: - OnItemsChangedSmooth(e); + return viewport; + } + + while (c is not null) + { + if (!c.Bounds.IsEmpty && c.TransformToVisual(this) is Matrix transform) + { + viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height) + .TransformToAABB(transform); break; + } + + c = c?.GetVisualParent(); } + + + return viewport; } - internal IReadOnlyList GetRealizedElements() + private void GenerateElements(Size availableSize, ref MeasureViewport viewport) { - return VirtualizationMode switch + Debug.Assert(ItemsControl?.Items is IList); + Debug.Assert(_measureElements is not null); + + var items = (IList)ItemsControl!.Items; + var horizontal = Orientation == Orientation.Horizontal; + var index = viewport.firstIndex; + var u = viewport.startU; + + do { - ItemVirtualizationMode.Smooth => _realizedElements?.Elements ?? Array.Empty(), - _ => Children, - }; + var e = GetOrCreateElement(items, index); + e.Measure(availableSize); + + var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height; + var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width; + + _measureElements!.Add(index, e, u, sizeU); + viewport.measuredV = Math.Max(viewport.measuredV, sizeV); + + u += sizeU; + ++index; + } while (u < viewport.viewportUEnd && index < items.Count); + } + + private Control GetOrCreateElement(IList items, int index) + { + var e = GetRealizedElement(index) ?? GetRecycledOrCreateElement(items, index); + InvalidateHack(e); + return e; + } + + private Control? GetRealizedElement(int index) + { + Debug.Assert(_realizedElements is not null); + + if (_anchorIndex == index) + return _anchorElement; + return _realizedElements!.GetElement(index); + } + + private Control GetRecycledOrCreateElement(IList items, int index) + { + Debug.Assert(ItemsControl is not null); + + var c = ItemsControl!.ItemContainerGenerator.Materialize(index, items[index]!).ContainerControl; + Children.Add(c); + return c; + } + + private void RecycleElement(Control element) + { + Debug.Assert(ItemsControl is not null); + + var index = ItemsControl.ItemContainerGenerator!.IndexFromContainer(element); + ItemsControl!.ItemContainerGenerator.Dematerialize(index, 1); + Children.Remove(element); + } + + private void UpdateElementIndex(Control element, int index) + { + // TODO: Implement this after we refactor ItemContainerGenerator. + } + + private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) + { + _viewport = e.EffectiveViewport; + _isWaitingForViewportUpdate = false; + InvalidateMeasure(); + } + + private static void InvalidateHack(Control c) + { + bool HasInvalidations(Control c) + { + if (!c.IsMeasureValid) + return true; + + for (var i = 0; i < c.VisualChildren.Count; ++i) + { + if (c.VisualChildren[i] is Control child) + { + if (!child.IsMeasureValid || HasInvalidations(child)) + return true; + } + } + + return false; + } + + void Invalidate(Control c) + { + c.InvalidateMeasure(); + for (var i = 0; i < c.VisualChildren.Count; ++i) + { + if (c.VisualChildren[i] is Control child) + Invalidate(child); + } + } + + if (HasInvalidations(c)) + Invalidate(c); } - private protected override void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + /// + /// Stores the realized element state for a in smooth mode. + /// + internal class RealizedElementList { - base.OnItemsControlPropertyChanged(sender, e); + private int _firstIndex; + private List? _elements; + private List? _sizes; + private double _startU; + + /// + /// Gets the number of realized elements. + /// + public int Count => _elements?.Count ?? 0; + + /// + /// Gets the model index of the first realized element, or -1 if no elements are realized. + /// + public int FirstModelIndex => _elements?.Count > 0 ? _firstIndex : -1; + + /// + /// Gets the model index of the last realized element, or -1 if no elements are realized. + /// + public int LastModelIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1; + + /// + /// Gets the elements. + /// + public IReadOnlyList Elements => _elements ??= new List(); + + /// + /// Gets the sizes of the elements on the primary axis. + /// + public IReadOnlyList SizeU => _sizes ??= new List(); + + /// + /// Gets the position of the first element on the primary axis. + /// + public double StartU => _startU; + + /// + /// Adds a newly realized element to the collection. + /// + /// The model index of the element. + /// The element. + /// The position of the elemnt on the primary axis. + /// The size of the element on the primary axis. + public void Add(int modelIndex, Control element, double u, double sizeU) + { + if (modelIndex < 0) + throw new ArgumentOutOfRangeException(nameof(modelIndex)); + + _elements ??= new List(); + _sizes ??= new List(); - if (e.Property == VirtualizationModeProperty) + if (Count == 0) + { + _elements.Add(element); + _sizes.Add(sizeU); + _startU = u; + _firstIndex = modelIndex; + } + else if (modelIndex == LastModelIndex + 1) + { + _elements.Add(element); + _sizes.Add(sizeU); + } + else if (modelIndex == FirstModelIndex - 1) + { + --_firstIndex; + _elements.Insert(0, element); + _sizes.Insert(0, sizeU); + _startU = u; + } + else + { + throw new NotSupportedException("Can only add items to the beginning or end of realized elements."); + } + } + + /// + /// Gets the element at the specified model index, if realized. + /// + /// The index in the source collection of the element to get. + /// The element if realized; otherwise null. + public Control? GetElement(int modelIndex) { - VirtualizationMode = e.GetNewValue(); + var index = modelIndex - FirstModelIndex; + if (index >= 0 && index < _elements?.Count) + return _elements[index]; + return null; } + + /// + /// Gets the element at the specified position on the primary axis, if realized. + /// + /// The position. + /// + /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the + /// primary axis. + /// + public (int index, double position) GetElementAt(double position) + { + if (_sizes is null || position < StartU) + return (-1, 0); + + var u = StartU; + var i = FirstModelIndex; + + foreach (var size in _sizes) + { + var endU = u + size; + if (position < endU) + return (i, u); + u += size; + ++i; + } + + return (-1, 0); + } + + /// + /// Updates the elements in response to items being inserted into the source collection. + /// + /// The index in the source collection of the insert. + /// The number of items inserted. + /// A method used to update the element indexes. + public void ItemsInserted(int modelIndex, int count, Action updateElementIndex) + { + if (modelIndex < 0) + throw new ArgumentOutOfRangeException(nameof(modelIndex)); + if (_elements is null || _elements.Count == 0) + return; + + // Get the index within the realized _elements collection. + var first = FirstModelIndex; + var index = modelIndex - first; + + if (index < Count) + { + // The insertion point affects the realized elements. Update the index of the + // elements after the insertion point. + var elementCount = _elements.Count; + var start = Math.Max(index, 0); + + for (var i = start; i < elementCount; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, first + i + count); + } + + if (index <= 0) + { + // The insertion point was before the first element, update the first index. + _firstIndex += count; + } + else + { + // The insertion point was within the realized elements, insert an empty space + // in _elements and _sizes. + _elements!.InsertMany(index, null, count); + _sizes!.InsertMany(index, 0.0, count); + } + } + } + + /// + /// Updates the elements in response to items being removed from the source collection. + /// + /// The index in the source collection of the remove. + /// The number of items removed. + /// A method used to update the element indexes. + /// A method used to recycle elements. + public void ItemsRemoved( + int modelIndex, + int count, + Action updateElementIndex, + Action recycleElement) + { + if (modelIndex < 0) + throw new ArgumentOutOfRangeException(nameof(modelIndex)); + if (_elements is null || _elements.Count == 0) + return; + + // Get the removal start and end index within the realized _elements collection. + var first = FirstModelIndex; + var last = LastModelIndex; + var startIndex = modelIndex - first; + var endIndex = (modelIndex + count) - first; + + if (endIndex < 0) + { + // The removed range was before the realized elements. Update the first index and + // the indexes of the realized elements. + _firstIndex -= count; + + for (var i = 0; i < _elements.Count; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, _firstIndex + i); + } + } + else if (startIndex < _elements.Count) + { + // Recycle and remove the affected elements. + var start = Math.Max(startIndex, 0); + var end = Math.Min(endIndex, _elements.Count); + + for (var i = start; i < end; ++i) + { + if (_elements[i] is Control element) + recycleElement(element); + } + + _elements.RemoveRange(start, end - start); + _sizes!.RemoveRange(start, end - start); + + // If the remove started before and ended within our realized elements, then our new + // first index will be the index where the remove started. + if (startIndex <= 0 && end < last) + _firstIndex = first = modelIndex; + + // Update the indexes of the elements after the removed range. + end = _elements.Count; + for (var i = start; i < end; ++i) + { + if (_elements[i] is Control element) + updateElementIndex(element, first + i); + } + } + } + + /// + /// Recycles elements before a specific index. + /// + /// The index in the source collection of new first element. + /// A method used to recycle elements. + public void RecycleElementsBefore(int modelIndex, Action recycleElement) + { + if (modelIndex <= FirstModelIndex || _elements is null || _elements.Count == 0) + return; + + if (modelIndex > LastModelIndex) + { + RecycleAllElements(recycleElement); + } + else + { + var endIndex = modelIndex - FirstModelIndex; + + for (var i = 0; i < endIndex; ++i) + { + if (_elements[i] is Control e) + recycleElement(e); + } + + _elements.RemoveRange(0, endIndex); + _sizes!.RemoveRange(0, endIndex); + _firstIndex = modelIndex; + } + } + + /// + /// Recycles elements after a specific index. + /// + /// The index in the source collection of new last element. + /// A method used to recycle elements. + public void RecycleElementsAfter(int modelIndex, Action recycleElement) + { + if (modelIndex >= LastModelIndex || _elements is null || _elements.Count == 0) + return; + + if (modelIndex < FirstModelIndex) + { + RecycleAllElements(recycleElement); + } + else + { + var startIndex = (modelIndex + 1) - FirstModelIndex; + var count = _elements.Count; + + for (var i = startIndex; i < count; ++i) + { + if (_elements[i] is Control e) + recycleElement(e); + } + + _elements.RemoveRange(startIndex, _elements.Count - startIndex); + _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex); + } + } + + /// + /// Recycles all realized elements. + /// + /// A method used to recycle elements. + public void RecycleAllElements(Action recycleElement) + { + if (_elements is null || _elements.Count == 0) + return; + + foreach (var e in _elements) + { + if (e is object) + recycleElement(e); + } + + _startU = _firstIndex = 0; + _elements?.Clear(); + _sizes?.Clear(); + } + + /// + /// Resets the element list and prepares it for reuse. + /// + public void ResetForReuse() + { + _startU = _firstIndex = 0; + _elements?.Clear(); + _sizes?.Clear(); + } + } + + private struct MeasureViewport + { + public int firstIndex; + public int estimatedLastIndex; + public double viewportUStart; + public double viewportUEnd; + public double measuredV; + public double startU; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml index c0ba508179..3bef23a160 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml @@ -40,8 +40,7 @@ BorderBrush="{DynamicResource ThemeControlMidBrush}" BorderThickness="0,0,0,1" FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf" - Items="{Binding History}" - VirtualizationMode="None"> + Items="{Binding History}"> diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index cdfca65ff6..9a8f41ac8a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -216,7 +216,6 @@ + SelectionMode="{Binding SelectionMode}"> diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 6eaef4c849..e206d809d3 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -285,7 +285,6 @@ namespace Avalonia.Controls.UnitTests var target = new ComboBox { Template = GetTemplate(), - VirtualizationMode = ItemVirtualizationMode.None }; target.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index b6375278b1..280f140a79 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -369,7 +369,6 @@ namespace Avalonia.Controls.UnitTests [Fact] public void LayoutManager_Should_Measure_Arrange_All() { - var virtualizationMode = ItemVirtualizationMode.Simple; using (UnitTestApplication.Start(TestServices.StyledWindow)) { var items = new AvaloniaList(Enumerable.Range(1, 7).Select(v => v.ToString())); @@ -387,7 +386,6 @@ namespace Avalonia.Controls.UnitTests target.Height = 110; target.Width = 50; target.DataContext = items; - target.VirtualizationMode = virtualizationMode; target.ItemTemplate = new FuncDataTemplate((c, _) => { @@ -448,7 +446,6 @@ namespace Avalonia.Controls.UnitTests AutoScrollToSelectedItem = true, Height = 100, Width = 50, - VirtualizationMode = ItemVirtualizationMode.Simple, ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), Items = items, }; @@ -488,7 +485,6 @@ namespace Avalonia.Controls.UnitTests Items = items, ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), SelectionMode = SelectionMode.AlwaysSelected, - VirtualizationMode = ItemVirtualizationMode.None, }; Prepare(target); @@ -532,7 +528,6 @@ namespace Avalonia.Controls.UnitTests VerticalAlignment = Layout.VerticalAlignment.Top, AutoScrollToSelectedItem = true, Width = 50, - VirtualizationMode = ItemVirtualizationMode.Simple, ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), Items = items, }; @@ -705,7 +700,6 @@ namespace Avalonia.Controls.UnitTests Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), SelectionMode = SelectionMode.AlwaysSelected, - VirtualizationMode = ItemVirtualizationMode.None }; Prepare(target); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 2c57c20b6c..d89e88b2ba 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1475,7 +1475,6 @@ namespace Avalonia.Controls.UnitTests.Primitives Height = 10 }), AutoScrollToSelectedItem = true, - VirtualizationMode = ItemVirtualizationMode.Simple, }; var root = new TestRoot(true, target); diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests_Smooth.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests_Smooth.cs index 7f7a956f2c..54b76b1931 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests_Smooth.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests_Smooth.cs @@ -161,7 +161,6 @@ namespace Avalonia.Controls.UnitTests Template = new FuncControlTemplate((_, _) => scroll), ItemsPanel = new FuncTemplate(() => target), ItemTemplate = new FuncDataTemplate((x, _) => new Canvas { Width = 100, Height = 10 }), - [VirtualizingStackPanel.VirtualizationModeProperty] = ItemVirtualizationMode.Smooth, }; var root = new TestRoot(true, itemsControl); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index ae8df6168d..9170c68e11 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -836,7 +836,6 @@ namespace Avalonia.LeakTests { Width = 100, Height = 100, - VirtualizationMode = ItemVirtualizationMode.None, // Create a button with binding to the KeyGesture in the template and add it to references list ItemTemplate = new FuncDataTemplate(typeof(KeyGesture), (o, scope) => { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 70a5295008..6361dcdbea 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -372,7 +372,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml }; var list = window.FindControl("list"); - list.VirtualizationMode = ItemVirtualizationMode.Simple; list.Items = collection; window.Show();