Преглед изворни кода

Merge pull request #3424 from AvaloniaUI/port/items-repeater

Port ItemsRepeater changes from WinUI
Steven Kirk пре 5 година
родитељ
комит
155e9d4bbd

+ 18 - 16
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -562,33 +562,35 @@ 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
-                    {
-                        _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)
+                    else if (Layout is NonVirtualizingLayout nonVirtualLayout)
                     {
-                        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();
                     }
                 }
+                finally
+                {
+                    _processingItemsSourceChange = null;
+                }
 
                 InvalidateMeasure();
             }

+ 61 - 16
src/Avalonia.Controls/Repeater/ViewManager.cs

@@ -109,11 +109,22 @@ 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);
 
+            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();
 
             if (_lastFocusedElement == element)
@@ -121,9 +132,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)
@@ -190,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;
@@ -515,21 +526,52 @@ 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 providedElementFactory = _owner.ItemTemplateShim;
+
+            ItemTemplateWrapper GetElementFactory()
+            {
+                if (providedElementFactory == null)
+                {
+                    var factory = FuncDataTemplate.Default;
+                    _owner.ItemTemplate = factory;
+                    return _owner.ItemTemplateShim;
+                }
 
-            var itemTemplateFactory = _owner.ItemTemplateShim;
-            if (itemTemplateFactory == null)
+                return providedElementFactory;
+            }
+
+            IControl GetElement()
             {
-                // If no ItemTemplate was provided, use a default 
-                var factory = FuncDataTemplate.Default;
-                _owner.ItemTemplate = factory;
-                itemTemplateFactory = _owner.ItemTemplateShim;
+                if (providedElementFactory == null)
+                {
+                    if (data is IControl dataAsElement)
+                    {
+                        return dataAsElement;
+                    }
+                }
+
+                var elementFactory = GetElementFactory();
+                return elementFactory.GetElement(_owner, data);
             }
 
-            var data = _owner.ItemsSourceView.GetAt(index);
-            var element = itemTemplateFactory.GetElement(_owner, data);
+            var element = GetElement();
 
             var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
             if (virtInfo == null)
@@ -537,8 +579,11 @@ namespace Avalonia.Controls
                 virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
             }
 
-            // Prepare the element
-            element.DataContext = data;
+            if (data != element)
+            {
+                // Prepare the element
+                element.DataContext = data;
+            }
 
             virtInfo.MoveOwnershipToLayoutFromElementFactory(
                 index,

+ 23 - 9
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();
@@ -115,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),
@@ -270,6 +272,7 @@ namespace Avalonia.Layout
             Size availableSize,
             double minItemSpacing,
             double lineSpacing,
+            int maxItemsPerLine,
             string layoutId)
         {
             if (anchorIndex != -1)
@@ -280,7 +283,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 +304,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 +342,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);
@@ -544,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 
@@ -563,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);
@@ -580,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);
                 }
             }
         }
@@ -594,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)
@@ -659,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);
             }

+ 8 - 8
src/Avalonia.Layout/NonVirtualizingLayout.cs

@@ -20,25 +20,25 @@ namespace Avalonia.Layout
         /// <inheritdoc/>
         public sealed override void InitializeForContext(LayoutContext context)
         {
-            InitializeForContextCore((VirtualizingLayoutContext)context);
+            InitializeForContextCore((NonVirtualizingLayoutContext)context);
         }
 
         /// <inheritdoc/>
         public sealed override void UninitializeForContext(LayoutContext context)
         {
-            UninitializeForContextCore((VirtualizingLayoutContext)context);
+            UninitializeForContextCore((NonVirtualizingLayoutContext)context);
         }
 
         /// <inheritdoc/>
         public sealed override Size Measure(LayoutContext context, Size availableSize)
         {
-            return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
+            return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
         }
 
         /// <inheritdoc/>
         public sealed override Size Arrange(LayoutContext context, Size finalSize)
         {
-            return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
+            return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
         }
 
         /// <summary>
@@ -49,7 +49,7 @@ namespace Avalonia.Layout
         /// The context object that facilitates communication between the layout and its host
         /// container.
         /// </param>
-        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.
         /// </param>
-        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.
         /// </returns>
-        protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+        protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
 
         /// <summary>
         /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@@ -98,6 +98,6 @@ namespace Avalonia.Layout
         /// its children.
         /// </param>
         /// <returns>The actual size that is used after the element is arranged in layout.</returns>
-        protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+        protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
     }
 }

+ 14 - 0
src/Avalonia.Layout/NonVirtualizingLayoutContext.cs

@@ -0,0 +1,14 @@
+// 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
+{
+    /// <summary>
+    /// Represents the base class for layout context types that do not support virtualization.
+    /// </summary>
+    public abstract class NonVirtualizingLayoutContext : LayoutContext
+    {
+    }
+}

+ 2 - 0
src/Avalonia.Layout/StackLayout.cs

@@ -267,6 +267,7 @@ namespace Avalonia.Layout
                 false,
                 0,
                 Spacing,
+                int.MaxValue,
                 _orientation.ScrollOrientation,
                 LayoutId);
 
@@ -278,6 +279,7 @@ namespace Avalonia.Layout
             var value = GetFlowAlgorithm(context).Arrange(
                finalSize,
                context,
+               false,
                FlowLayoutAlgorithm.LineAlignment.Start,
                LayoutId);
 

+ 43 - 11
src/Avalonia.Layout/UniformGridLayout.cs

@@ -110,6 +110,12 @@ namespace Avalonia.Layout
         public static readonly StyledProperty<double> MinRowSpacingProperty =
             AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing));
 
+        /// <summary>
+        /// Defines the <see cref="MaximumRowsOrColumnsProperty"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaximumRowsOrColumnsProperty =
+            AvaloniaProperty.Register<UniformGridLayout, int>(nameof(MinItemWidth));
+
         /// <summary>
         /// Defines the <see cref="Orientation"/> property.
         /// </summary>
@@ -123,6 +129,7 @@ namespace Avalonia.Layout
         private double _minColumnSpacing;
         private UniformGridLayoutItemsJustification _itemsJustification;
         private UniformGridLayoutItemsStretch _itemsStretch;
+        private int _maximumRowsOrColumns = int.MaxValue;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UniformGridLayout"/> class.
@@ -219,6 +226,15 @@ namespace Avalonia.Layout
             set => SetValue(MinRowSpacingProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets the maximum row or column count.
+        /// </summary>
+        public int MaximumRowsOrColumns
+        {
+            get => GetValue(MaximumRowsOrColumnsProperty);
+            set => SetValue(MaximumRowsOrColumnsProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the axis along which items are laid out.
         /// </summary>
@@ -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,17 +349,21 @@ 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)
             {
                 _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));
@@ -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);
 
@@ -421,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);
@@ -471,6 +497,10 @@ namespace Avalonia.Layout
             {
                 _minItemHeight = (double)args.NewValue;
             }
+            else if (args.Property == MaximumRowsOrColumnsProperty)
+            {
+                _maximumRowsOrColumns = (int)args.NewValue;
+            }
 
             InvalidateLayout();
         }
@@ -499,7 +529,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);
 

+ 26 - 8
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)
             {