From 4cca4f0e4cbec66b90b16a4e22681fd814a5fb7a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 08:35:28 +0100 Subject: [PATCH 01/11] update to repeater to allow passing in uielements in itemssource * minor update to repeater to allow passing in uielements in itemssource * minor fixes Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/95d5ff00768030e52c3a2b56cde99c36ff694168 --- src/Avalonia.Controls/Repeater/ViewManager.cs | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 51c14d47d6..e4fc1158ef 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -109,11 +109,10 @@ namespace Avalonia.Controls public void ClearElementToElementFactory(IControl element) { - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); - var clearedIndex = virtInfo.Index; _owner.OnElementClearing(element); - _owner.ItemTemplateShim.RecycleElement(_owner, element); + _owner.ItemTemplateShim?.RecycleElement(_owner, element); + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); virtInfo.MoveOwnershipToElementFactory(); if (_lastFocusedElement == element) @@ -121,9 +120,8 @@ namespace Avalonia.Controls // Focused element is going away. Remove the tracked last focused element // and pick a reasonable next focus if we can find one within the layout // realized elements. - MoveFocusFromClearedIndex(clearedIndex); + MoveFocusFromClearedIndex(virtInfo.Index); } - } private void MoveFocusFromClearedIndex(int clearedIndex) @@ -518,18 +516,32 @@ namespace Avalonia.Controls private IControl GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. - + var data = _owner.ItemsSourceView.GetAt(index); var itemTemplateFactory = _owner.ItemTemplateShim; + IControl element = null; + var itemsSourceContainsElements = false; + if (itemTemplateFactory == null) { - // If no ItemTemplate was provided, use a default - var factory = FuncDataTemplate.Default; - _owner.ItemTemplate = factory; - itemTemplateFactory = _owner.ItemTemplateShim; + element = data as IControl; + + // No item template provided and ItemsSource contains objects derived from UIElement. + // In this case, just use the data directly as elements. + itemsSourceContainsElements = element != null; } - var data = _owner.ItemsSourceView.GetAt(index); - var element = itemTemplateFactory.GetElement(_owner, data); + if (element == null) + { + if (itemTemplateFactory == null) + { + // If no ItemTemplate was provided, use a default + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + itemTemplateFactory = _owner.ItemTemplateShim; + } + + element = itemTemplateFactory.GetElement(_owner, data); + } var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) @@ -537,8 +549,11 @@ namespace Avalonia.Controls virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } - // Prepare the element - element.DataContext = data; + if (!itemsSourceContainsElements) + { + // Prepare the element + element.DataContext = data; + } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, From 36fdb8e534f3a43a07a8380d3acb34189e94412e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 09:24:28 +0100 Subject: [PATCH 02/11] Add a MaximumRowsOrColumns to Uniformgridlayout * Add a MaximumRowsOrColumns property to UniformGridLayout * Fix nit * Fix bugs Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/6bd03c98f770fed69312ed5efb6cc06b428fa758?w=1 --- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 14 +++--- src/Avalonia.Layout/StackLayout.cs | 1 + src/Avalonia.Layout/UniformGridLayout.cs | 49 +++++++++++++++---- src/Avalonia.Layout/UniformGridLayoutState.cs | 34 ++++++++++--- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 615ce725bd..7343f4a6e9 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -72,6 +72,7 @@ namespace Avalonia.Layout bool isWrapping, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, ScrollOrientation orientation, string layoutId) { @@ -94,14 +95,14 @@ namespace Avalonia.Layout _elementManager.OnBeginMeasure(orientation); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); - Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); - Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); if (isWrapping && IsReflowRequired()) { var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); - Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); } RaiseLineArranged(); @@ -270,6 +271,7 @@ namespace Avalonia.Layout Size availableSize, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, string layoutId) { if (anchorIndex != -1) @@ -280,7 +282,7 @@ namespace Avalonia.Layout var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); - int countInLine = 1; + var countInLine = 1; int count = 0; bool lineNeedsReposition = false; @@ -301,7 +303,7 @@ namespace Avalonia.Layout if (direction == GenerateDirection.Forward) { double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // No more space in this row. wrap to next row. _orientation.SetMinorStart(ref currentBounds, 0); @@ -339,7 +341,7 @@ namespace Avalonia.Layout { // Backward double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // Does not fit, wrap to the previous row var availableSizeMinor = _orientation.Minor(availableSize); diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e9735b9b31..a3fe80fab7 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -267,6 +267,7 @@ namespace Avalonia.Layout false, 0, Spacing, + int.MaxValue, _orientation.ScrollOrientation, LayoutId); diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index edc2042922..16d21508a9 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -110,6 +110,12 @@ namespace Avalonia.Layout public static readonly StyledProperty MinRowSpacingProperty = AvaloniaProperty.Register(nameof(MinRowSpacing)); + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumRowsOrColumnsProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + /// /// Defines the property. /// @@ -123,6 +129,7 @@ namespace Avalonia.Layout private double _minColumnSpacing; private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + private int _maximumRowsOrColumns = int.MaxValue; /// /// Initializes a new instance of the class. @@ -219,6 +226,15 @@ namespace Avalonia.Layout set => SetValue(MinRowSpacingProperty, value); } + /// + /// Gets or sets the maximum row or column count. + /// + public int MaximumRowsOrColumns + { + get => GetValue(MaximumRowsOrColumnsProperty); + set => SetValue(MaximumRowsOrColumnsProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -269,15 +285,17 @@ namespace Avalonia.Layout { var gridState = (UniformGridLayoutState)context.LayoutState; var lastExtent = gridState.FlowAlgorithm.LastExtent; - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); - double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + var itemsPerLine = Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, (uint)_maximumRowsOrColumns)); + var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) { double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); } } @@ -299,7 +317,9 @@ namespace Avalonia.Layout int count = context.ItemCount; if (targetIndex >= 0 && targetIndex < count) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; index = indexOfFirstInLine; var state = context.LayoutState as UniformGridLayoutState; @@ -329,8 +349,12 @@ namespace Avalonia.Layout // Constants int itemsCount = context.ItemCount; double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? - (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); + int itemsPerLine = + (int)Math.Min( // note use of unsigned ints + Math.Max(1u, !double.IsInfinity(availableSizeMinor) + ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) + : (uint)itemsCount), + Math.Max(1u, _maximumRowsOrColumns)); double lineSize = GetMajorSizeWithSpacing(context); if (itemsCount > 0) @@ -398,7 +422,7 @@ namespace Avalonia.Layout // Set the width and height on the grid state. If the user already set them then use the preset. // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. var gridState = (UniformGridLayoutState)context.LayoutState; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing); + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, @@ -406,6 +430,7 @@ namespace Avalonia.Layout true, MinItemSpacing, LineSpacing, + _maximumRowsOrColumns, _orientation.ScrollOrientation, LayoutId); @@ -471,6 +496,10 @@ namespace Avalonia.Layout { _minItemHeight = (double)args.NewValue; } + else if (args.Property == MaximumRowsOrColumnsProperty) + { + _maximumRowsOrColumns = (int)args.NewValue; + } InvalidateLayout(); } @@ -499,7 +528,9 @@ namespace Avalonia.Layout Rect lastExtent, VirtualizingLayoutContext context) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( //note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int rowIndex = (int)(index / itemsPerLine); int indexInRow = index - (rowIndex * itemsPerLine); diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index e6d75bcf35..62c5174775 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -48,8 +48,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + if (context.ItemCount > 0) { // If the first element is realized we don't need to cache it or to get it from the context @@ -57,7 +63,7 @@ namespace Avalonia.Layout if (realizedElement != null) { realizedElement.Measure(availableSize); - SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); _cachedFirstElement = null; } else @@ -72,7 +78,7 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); @@ -92,8 +98,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); @@ -101,11 +113,17 @@ namespace Avalonia.Layout var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight; - itemSizeMinor += minorItemSpacing; - var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor)); - var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor); - var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn; + double extraMinorPixelsForEachItem = 0.0; + if (!double.IsInfinity(availableSizeMinor)) + { + var numItemsPerColumn = Math.Min( + maxItemsPerLine, + Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing))); + var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing; + var remainingSpace = ((int)(availableSizeMinor - usedSpace)); + extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn); + } if (stretch == UniformGridLayoutItemsStretch.Fill) { From 55275e09def1f8830ce3cafd071529ab600ea0f0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 10:01:53 +0100 Subject: [PATCH 03/11] =?UTF-8?q?Fix=20stack=20layout=20arrange=20when=20i?= =?UTF-8?q?tems=20use=20stretch=20and=20final=20size=20is=20mor=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …e than desired size. Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/f37ebd5bb21e6d4d7b3bb4228fb22a47d3c33b85 --- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 18 +++++++++++++++--- src/Avalonia.Layout/StackLayout.cs | 1 + src/Avalonia.Layout/UniformGridLayout.cs | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 7343f4a6e9..7f44c80a64 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -116,10 +116,11 @@ namespace Avalonia.Layout public Size Arrange( Size finalSize, VirtualizingLayoutContext context, + bool isWrapping, LineAlignment lineAlignment, string layoutId) { - ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId); + ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); return new Size( Math.Max(finalSize.Width, _lastExtent.Width), @@ -546,6 +547,7 @@ namespace Avalonia.Layout private void ArrangeVirtualizingLayout( Size finalSize, LineAlignment lineAlignment, + bool isWrapping, string layoutId) { // Walk through the realized elements one line at a time and @@ -565,7 +567,7 @@ namespace Avalonia.Layout if (_orientation.MajorStart(currentBounds) != currentLineOffset) { spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); spaceAtLineStart = _orientation.MinorStart(currentBounds); countInLine = 0; currentLineOffset = _orientation.MajorStart(currentBounds); @@ -582,7 +584,7 @@ namespace Avalonia.Layout if (countInLine > 0) { var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); - PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId); + PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId); } } } @@ -596,6 +598,8 @@ namespace Avalonia.Layout double spaceAtLineEnd, double lineSize, LineAlignment lineAlignment, + bool isWrapping, + Size finalSize, string layoutId) { for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex) @@ -661,6 +665,14 @@ namespace Avalonia.Layout } bounds = bounds.Translate(-_lastExtent.Position); + + if (!isWrapping) + { + _orientation.SetMinorSize( + ref bounds, + Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize))); + } + var element = _elementManager.GetAt(rangeIndex); element.Arrange(bounds); } diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index a3fe80fab7..3c3729272c 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -279,6 +279,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + false, FlowLayoutAlgorithm.LineAlignment.Start, LayoutId); diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 16d21508a9..86dc21bd19 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -446,6 +446,7 @@ namespace Avalonia.Layout var value = GetFlowAlgorithm(context).Arrange( finalSize, context, + true, (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, LayoutId); return new Size(value.Width, value.Height); From 261e4775229d84bcee3a93ba51c38f21bdd5e2b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 10:23:29 +0100 Subject: [PATCH 04/11] ItemsRepeater fix. * Remove item from repeater children if there is no itemtemplate when being recycled. Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/c4d4d9995a0ee7df27aac11f875538dd4fbe14fb --- src/Avalonia.Controls/Repeater/ViewManager.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index e4fc1158ef..f4588787e8 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -110,7 +110,19 @@ namespace Avalonia.Controls public void ClearElementToElementFactory(IControl element) { _owner.OnElementClearing(element); - _owner.ItemTemplateShim?.RecycleElement(_owner, element); + + if (_owner.ItemTemplateShim != null) + { + _owner.ItemTemplateShim.RecycleElement(_owner, element); + } + else + { + // No ItemTemplate to recycle to, remove the element from the children collection. + if (!_owner.Children.Remove(element)) + { + throw new InvalidOperationException("ItemsRepeater's child not found in its Children collection."); + } + } var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); virtInfo.MoveOwnershipToElementFactory(); From 1f46076f16deb2e0a60146b41b3fa01586b6c0d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 10:35:15 +0100 Subject: [PATCH 05/11] Use NonVirtualizingLayoutContext. - Add and use `NonVirtualizingLayoutContext` in `NonVirtualizingLayout` - Return -1 instead of throwing if element is not a child of `ItemsRepeater` Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/22adf4dc624baf339a1da3f9c7d83e125f2f1b01 --- src/Avalonia.Controls/Repeater/ViewManager.cs | 3 ++- src/Avalonia.Layout/NonVirtualizingLayout.cs | 8 ++++---- src/Avalonia.Layout/NonVirtualizingLayoutContext.cs | 11 +++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Layout/NonVirtualizingLayoutContext.cs diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index f4588787e8..d670aa077c 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -200,7 +200,8 @@ namespace Avalonia.Controls { if (virtInfo == null) { - throw new ArgumentException("Element is not a child of this ItemsRepeater."); + //Element is not a child of this ItemsRepeater. + return -1; } return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index fba91e66c7..3917830eb7 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -32,7 +32,7 @@ namespace Avalonia.Layout /// public sealed override Size Measure(LayoutContext context, Size availableSize) { - return MeasureOverride((VirtualizingLayoutContext)context, availableSize); + return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize); } /// @@ -49,7 +49,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void InitializeForContextCore(LayoutContext context) { } @@ -61,7 +61,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + protected virtual void UninitializeForContextCore(LayoutContext context) { } @@ -83,7 +83,7 @@ namespace Avalonia.Layout /// of the allocated sizes for child objects or based on other considerations such as a /// fixed container size. /// - protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs new file mode 100644 index 0000000000..201ffa7d36 --- /dev/null +++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs @@ -0,0 +1,11 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +namespace Avalonia.Layout +{ + public abstract class NonVirtualizingLayoutContext : LayoutContext + { + } +} From 09297dbb089110c2c70c675eb4b671d10834ef46 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 10:37:44 +0100 Subject: [PATCH 06/11] Fix repeater holding onto elements If a consumer changes the ItemsSource with a non recycling layout then the items from the old source would be perpetually held by repeater, a potentially substantial leak. Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/e158eec3e93f7c3839f5a9e5226f0f4e0103f2a6 --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 0e2136a6f3..685d3e44f2 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -588,6 +588,8 @@ namespace Avalonia.Controls ClearElementImpl(element); } } + + Children.Clear(); } InvalidateMeasure(); From bb7276d568e2d51127b2134013d63f0d410226f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 10:46:13 +0100 Subject: [PATCH 07/11] =?UTF-8?q?Extend=20the=20fix=20for=20Recycling=20th?= =?UTF-8?q?e=20focused=20element=20to=20non=20virtualized=20l=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ayouts * Ensure that we set the m_processingItesmSourceChange flag for non-virtualizing layouts as well as virtualizing ones. Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/cc335ac3915ed37ac7ee95237b789622287d2f5a --- .../Repeater/ItemsRepeater.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 685d3e44f2..cbac1d6c1b 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -562,34 +562,34 @@ namespace Avalonia.Controls if (Layout != null) { - if (Layout is VirtualizingLayout virtualLayout) - { - var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + try + { _processingItemsSourceChange = args; - try + if (Layout is VirtualizingLayout virtualLayout) { virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } - finally + else if (Layout is NonVirtualizingLayout nonVirtualLayout) { - _processingItemsSourceChange = null; - } - } - else if (Layout is NonVirtualizingLayout nonVirtualLayout) - { - // Walk through all the elements and make sure they are cleared for - // non-virtualizing layouts. - foreach (var element in Children) - { - if (GetVirtualizationInfo(element).IsRealized) + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) { - ClearElementImpl(element); + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } } - } - Children.Clear(); + Children.Clear(); + } + } + finally + { + _processingItemsSourceChange = null; } InvalidateMeasure(); From 0236ced64fc31ba998177b05f3cd23623595e723 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 11:32:39 +0100 Subject: [PATCH 08/11] Fix ItemsRepeater overwriting DataContext Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/0e24e05dbdb231aa0f4e3ec22e13ad528f155eb6 --- src/Avalonia.Controls/Repeater/ViewManager.cs | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index d670aa077c..7d005a30b4 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -526,43 +526,60 @@ namespace Avalonia.Controls return element; } + // There are several cases handled here with respect to which element gets returned and when DataContext is modified. + // + // 1. If there is no ItemTemplate: + // 1.1 If data is an IControl -> the data is returned + // 1.2 If data is not an IControl -> a default DataTemplate is used to fetch element and DataContext is set to data + // + // 2. If there is an ItemTemplate: + // 2.1 If data is not an IControl -> Element is fetched from ElementFactory and DataContext is set to the data + // 2.2 If data is an IControl: + // 2.2.1 If Element returned by the ElementFactory is the same as the data -> Element (a.k.a. data) is returned as is + // 2.2.2 If Element returned by the ElementFactory is not the same as the data + // -> Element that is fetched from the ElementFactory is returned and + // DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself private IControl GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. var data = _owner.ItemsSourceView.GetAt(index); - var itemTemplateFactory = _owner.ItemTemplateShim; - IControl element = null; - var itemsSourceContainsElements = false; + var providedElementFactory = _owner.ItemTemplateShim; - if (itemTemplateFactory == null) + ItemTemplateWrapper GetElementFactory() { - element = data as IControl; + if (providedElementFactory == null) + { + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + return _owner.ItemTemplateShim; + } - // No item template provided and ItemsSource contains objects derived from UIElement. - // In this case, just use the data directly as elements. - itemsSourceContainsElements = element != null; + return providedElementFactory; } - if (element == null) + IControl GetElement() { - if (itemTemplateFactory == null) + if (providedElementFactory == null) { - // If no ItemTemplate was provided, use a default - var factory = FuncDataTemplate.Default; - _owner.ItemTemplate = factory; - itemTemplateFactory = _owner.ItemTemplateShim; + if (data is IControl dataAsElement) + { + return dataAsElement; + } } - element = itemTemplateFactory.GetElement(_owner, data); + var elementFactory = GetElementFactory(); + return elementFactory.GetElement(_owner, data); } + var element = GetElement(); + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) { virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } - if (!itemsSourceContainsElements) + if (data != element) { // Prepare the element element.DataContext = data; From c6e6ad5678e88380ffb4e0aaa9005a931cf52f9e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 11:35:05 +0100 Subject: [PATCH 09/11] Fix bug with UniformGridLayout MaximumRowsOrColumns and requested size Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/007ab33a66642acb3be343f5aaf35df650513df5 --- src/Avalonia.Layout/UniformGridLayout.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 86dc21bd19..11a521ed1e 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -361,9 +361,9 @@ namespace Avalonia.Layout { _orientation.SetMinorSize( ref extent, - !double.IsInfinity(availableSizeMinor) ? + !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? availableSizeMinor : - Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); + Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); _orientation.SetMajorSize( ref extent, Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); From 155915134e15b95029a9360347ad2129fed82231 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jan 2020 11:01:16 +0100 Subject: [PATCH 10/11] Use correct context type. --- src/Avalonia.Layout/NonVirtualizingLayout.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index 3917830eb7..5d27ba9199 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -20,13 +20,13 @@ namespace Avalonia.Layout /// public sealed override void InitializeForContext(LayoutContext context) { - InitializeForContextCore((VirtualizingLayoutContext)context); + InitializeForContextCore((NonVirtualizingLayoutContext)context); } /// public sealed override void UninitializeForContext(LayoutContext context) { - UninitializeForContextCore((VirtualizingLayoutContext)context); + UninitializeForContextCore((NonVirtualizingLayoutContext)context); } /// @@ -38,7 +38,7 @@ namespace Avalonia.Layout /// public sealed override Size Arrange(LayoutContext context, Size finalSize) { - return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); + return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize); } /// @@ -98,6 +98,6 @@ namespace Avalonia.Layout /// its children. /// /// The actual size that is used after the element is arranged in layout. - protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize; } } From ef42e9350d0fd9fe983fb15d025e3384b52e4c9c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jan 2020 11:01:32 +0100 Subject: [PATCH 11/11] Add doc comment for NonVirtualizingLayoutContext. --- src/Avalonia.Layout/NonVirtualizingLayoutContext.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs index 201ffa7d36..d3dec83e9b 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs @@ -5,6 +5,9 @@ namespace Avalonia.Layout { + /// + /// Represents the base class for layout context types that do not support virtualization. + /// public abstract class NonVirtualizingLayoutContext : LayoutContext { }