Procházet zdrojové kódy

Merge pull request #3068 from AvaloniaUI/fixes/3041-itemsrepeater-stuck

Fix ItemsRepeater scrolling getting stuck
Jumar Macato před 6 roky
rodič
revize
04e2e460d2

+ 2 - 1
samples/ControlCatalog/MainView.xaml

@@ -34,7 +34,8 @@
       <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
       <TabItem Header="Image"><pages:ImagePage/></TabItem>
       <TabItem Header="ItemsRepeater"
-               ScrollViewer.VerticalScrollBarVisibility="Disabled">
+               ScrollViewer.VerticalScrollBarVisibility="Disabled"
+               ScrollViewer.HorizontalScrollBarVisibility="Disabled">
         <pages:ItemsRepeaterPage/>
       </TabItem>
       <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>

+ 8 - 1
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@@ -14,12 +14,19 @@
         <ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
       </ComboBox>
       <Button Command="{Binding AddItem}">Add Item</Button>
+      <Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
     </StackPanel>
     <Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
       <ScrollViewer Name="scroller"
                     HorizontalScrollBarVisibility="Auto"
                     VerticalScrollBarVisibility="Auto">
-        <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}"/>
+        <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
+          <ItemsRepeater.ItemTemplate>
+            <DataTemplate>
+              <TextBlock Height="{Binding Height}" Text="{Binding Text}"/>
+            </DataTemplate>
+          </ItemsRepeater.ItemTemplate>
+        </ItemsRepeater>
       </ScrollViewer>
     </Border>
   </DockPanel>

+ 1 - 1
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@@ -74,7 +74,7 @@ namespace ControlCatalog.Pages
 
         private void RepeaterClick(object sender, PointerPressedEventArgs e)
         {
-            var item = (e.Source as TextBlock)?.DataContext as string;
+            var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
             ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item;
         }
     }

+ 33 - 6
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@@ -1,4 +1,5 @@
-using System.Collections.ObjectModel;
+using System;
+using System.Collections.ObjectModel;
 using System.Linq;
 using ReactiveUI;
 
@@ -10,18 +11,44 @@ namespace ControlCatalog.ViewModels
 
         public ItemsRepeaterPageViewModel()
         {
-            Items = new ObservableCollection<string>(
-                Enumerable.Range(1, 100000).Select(i => $"Item {i.ToString()}"));
+            Items = new ObservableCollection<Item>(
+                Enumerable.Range(1, 100000).Select(i => new Item
+                {
+                    Text = $"Item {i.ToString()}",
+                }));
         }
 
-        public ObservableCollection<string> Items { get; }
+        public ObservableCollection<Item> Items { get; }
 
-        public string SelectedItem { get; set; }
+        public Item SelectedItem { get; set; }
 
         public void AddItem()
         {
             var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
-            Items.Insert(index + 1, $"New Item {newItemIndex++}");
+            Items.Insert(index + 1, new Item { Text = $"New Item {newItemIndex++}" });
+        }
+
+        public void RandomizeHeights()
+        {
+            var random = new Random();
+
+            foreach (var i in Items)
+            {
+                i.Height = random.Next(240) + 10;
+            }
+        }
+
+        public class Item : ReactiveObject
+        {
+            private double _height = double.NaN;
+
+            public string Text { get; set; }
+            
+            public double Height 
+            {
+                get => _height;
+                set => this.RaiseAndSetIfChanged(ref _height, value);
+            }
         }
     }
 }

+ 7 - 4
src/Avalonia.Controls/Repeater/ViewManager.cs

@@ -581,13 +581,16 @@ namespace Avalonia.Controls
 
         private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
         {
-            if (_isDataSourceStableResetPending)
+            bool moveToPinnedPool =
+                !isClearedDueToCollectionChange && virtInfo.IsPinned;
+
+            if (moveToPinnedPool)
             {
-                _resetPool.Add(element);
-                virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
+                _pinnedPool.Add(new PinnedElementInfo(element));
+                virtInfo.MoveOwnershipToPinnedPool();
             }
 
-            return _isDataSourceStableResetPending;
+            return moveToPinnedPool;
         }
 
         private void UpdateFocusedElement()

+ 1 - 1
src/Avalonia.Layout/LayoutQueue.cs

@@ -60,7 +60,7 @@ namespace Avalonia.Layout
 
         public void EndLoop()
         {
-            var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray();
+            var notfinalized = _loopQueueInfo.Where(v => v.Value.Count >= _maxEnqueueCountPerLoop).ToArray();
 
             _loopQueueInfo.Clear();
 

+ 33 - 0
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@@ -341,5 +341,38 @@ namespace Avalonia.Layout.UnitTests
             //layoutmanager should process properly other visuals
             Assert.All(targets, c => Assert.True(c.Arranged));
         }
+
+
+        [Fact]
+        public void LayoutManager_Should_Recover_From_Infinite_Loop_On_Measure()
+        {
+            // Test for issue #3041.
+            var control = new LayoutTestControl();
+            var root = new LayoutTestRoot { Child = control };
+
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+            control.Measured = false;
+
+            control.DoMeasureOverride = (l, s) =>
+            {
+                control.InvalidateMeasure();
+                return new Size(100, 100);
+            };
+
+            control.InvalidateMeasure();
+            root.LayoutManager.ExecuteLayoutPass();
+
+            // This is the important part: running a second layout pass in which we exceed the maximum
+            // retries causes LayoutQueue<T>.Info.Count to exceed _maxEnqueueCountPerLoop.
+            root.LayoutManager.ExecuteLayoutPass();
+
+            control.Measured = false;
+            control.DoMeasureOverride = null;
+
+            root.LayoutManager.ExecuteLayoutPass();
+
+            Assert.True(control.Measured);
+            Assert.True(control.IsMeasureValid);
+        }
     }
 }