Repository: AvaloniaUI/Avalonia

Issue:
Rendering artifacts in ListView
## Describe the bug
I am experiencing strange rendering artefacts when using a `ListView`.

## To Reproduce
Steps to reproduce the behavior:

See attached minimal example.
[ListViewAutoScrollBug.zip](https://github.com/AvaloniaUI/Avalonia/files/13349670/ListViewAutoScrollBug.zip)

A clear and concise description of what you expected to happen.

## Screenshots

https://github.com/AvaloniaUI/Avalonia/assets/125025268/56b2cfc4-e568-4f26-bc02-63c9ad0ed28f


## Environment <!-- (please complete the following information): -->

- OS: Windows 11
- Avalonia-Version: 11.0.5

## Additional context
I want to render a hierarchical tree in a ListView. Every change of the tree triggers a redraw of the ListView so that the content of the ListView is recreated every time a node is expanded or collapsed.

For illustration purposes in this demo every expand of a node creates 20 new children while the last of them shall be selected and become visible. (`AutoScrollToSelectedItem`) This works most of the time. But sometimes the ListView "overdraws" the item with some old content that does not disappear anymore. In these cases the autoscroll does not work either.



Hints:
Seems same as [#12744](https://github.com/AvaloniaUI/Avalonia/issues/12744).
consider using TreeDataGrid
> consider using TreeDataGrid

I can‘t say for sure but I think I had basically the same issue with TreeDataGrid. That‘s was one of two reasons to go with this approach.
I can also confirm TreeDataGrid has virtualization issue. 
Btw seems like you are using semi avalonia❤️
y, the underlaying issue is applying to all virtualized controls. So if anyone finds the root cause, a PR is more than welcome. Maybe @grokys has some ideas but he is more than busy rn. 
I think this is indeed a duplicate, so closing it in favor of the original issue. If someone disagrees, let me know. 
I don't know if this is helpful but I noticed that it is reproducable that exacly the same node seems to be affected every time you run the app. (Just expand the last node until the issue appears)

And the it seems like a "broken" node is added additionally to the other nodes before the actual new nodes are added. This broken node also appears in the logical tree.

![grafik](https://github.com/AvaloniaUI/Avalonia/assets/125025268/834618d4-d2f2-4ded-93d1-6d2fccfde2bd)

The issue seems to occur only when `AutoScrollToSelectedItem` is true.
At least in my example it only occurs when the last item gets selected by code.
In my tests the issue disappeared when `UseLayoutRounding` is set to `false` on `ListBox`.
Should be fixed by #13765 if you'd like to test.
I have tested the attached example with 11.0.6 and the issue is still present.
@CodeDevAM can you also check nightly? It has fixes that 11.0.6 doesn't have
No difference with 11.1.999-cibuild0043367-beta
Reopening for now. 

/cc @grokys 
The issue is still present with 11.0.9.

Unfortunately this is a big blocker for my app.

Maybe this helps finding the root cause: I have provided an _Reset View_ button the the user for the situation when the issue occurs in my app. So the user can manually recreate the ListView Control. Then issue does not appear for the newly created control. But it may appear again after some modifications to the list.
My current investigation results:

- Occurance depands on the size of Controls. A slightly different size results in different bahaviour.

https://github.com/AvaloniaUI/Avalonia/assets/125025268/b6cc4178-ad80-49ff-b7a0-1a5b3120ab02


https://github.com/AvaloniaUI/Avalonia/assets/125025268/7e4e594d-66fe-470d-9277-da793c012b8e

- `ScrollIntoView()` from `VirtualizingStackPanel` seems to add Items that are not initialized correctly. Not all `VisualChildren` are cleared corretly afterwards. This results in overlaying Controls.

- Different settings for `UseLayoutRounding` have an impact on the behaviour but the issue **does not** disappear.
I don't have a minimal reproduction scenario yet but `TreeDataGrid` seems to have the same or a very similar issue. (Version 11.0.2)
Since I don't have a solution for `AutoScrollToSelectedItem` with a `TreeDataGrid` yet the issue also occurs independently of this feature.

![grafik](https://github.com/AvaloniaUI/Avalonia/assets/125025268/61cc9b54-5bc5-4eb4-a3b8-5a280bf23f15)


@CodeDevAM I'd suggest you put your sample into `src/samples/Sandbox` and try to debug the virtulization logic in source directly, given you spend some time on that issue anyways. Maybe you find the right place to clean up recycle items. 
That's exactly what I was doing. I spent many hours to figure out the root cause of this issue. (at least for a `ListBox`) Unfortunatly it's a very complicated piece of code and I was not able to figure it out so far.

The problem that I have is that changing the underlying collection of a `ListBox` causes a run through a hierarchy of Controls multiple times in different modes that changes the behaviour of the relevant functions slightly. In the end it is very hard to follow. Long story short: I think it needs an expert that is more familiar with the inner details of a `ListBox`.
This check could be problematic because it is prone to floating point rounding errors.

https://github.com/AvaloniaUI/Avalonia/blob/11f5203ab24515a4094d7140d0d757d5016e2007/src/Avalonia.Controls/VirtualizingStackPanel.cs#L406

I noticed that the Bound checks behave differently in a good case and a bad case.
Like a Bottom of `798.8636363636365` is not considered to be within the bounds `798.8636363636364`.

@CodeDevAM if you give a kind of "epsilon" to the contains method, would that solve your issue? 

`Bounds.Contains(rect, 0.0001)` where Contains needs a new overload accepting that epsilon.  
A check with a certain tolerance would be a good idea. 

Unfortunately it does not solve the issue. With my analysis I figured out so far that `_viewport` of the `VirtualizingStackPanel` is calculated differently in a good case and a bad case at
https://github.com/AvaloniaUI/Avalonia/blob/11f5203ab24515a4094d7140d0d757d5016e2007/src/Avalonia.Controls/VirtualizingStackPanel.cs#L422

As a result the `anchorIndex` is is not calculated correctly. https://github.com/AvaloniaUI/Avalonia/blob/11f5203ab24515a4094d7140d0d757d5016e2007/src/Avalonia.Controls/VirtualizingStackPanel.cs#L465
Another idea would be to use Inflate on the Bounds and friends 🤔

Patch:
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 4a54c1b35c8..04f6b1d8a12 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -259,38 +259,34 @@ public bool BringDescendantIntoView(Visual target, Rect targetRect)
 
             var rect = targetRect.TransformToAABB(transform.Value);
             var offset = Offset;
-            var result = false;
 
             if (rect.Bottom > offset.Y + Viewport.Height)
             {
                 offset = offset.WithY((rect.Bottom - Viewport.Height) + Child.Margin.Top);
-                result = true;
             }
 
             if (rect.Y < offset.Y)
             {
                 offset = offset.WithY(rect.Y);
-                result = true;
             }
 
             if (rect.Right > offset.X + Viewport.Width)
             {
                 offset = offset.WithX((rect.Right - Viewport.Width) + Child.Margin.Left);
-                result = true;
             }
 
             if (rect.X < offset.X)
             {
                 offset = offset.WithX(rect.X);
-                result = true;
             }
 
-            if (result)
+            if (Offset.NearlyEquals(offset))
             {
-                SetCurrentValue(OffsetProperty, offset);
+                return false;
             }
 
-            return result;
+            SetCurrentValue(OffsetProperty, offset);
+            return true;
         }
 
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index f702f09e8ae..65410021a89 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -453,8 +453,6 @@ private MeasureViewport CalculateMeasureViewport(IReadOnlyList<object?> 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;
 
             // Get the viewport in the orientation direction.


Test patch:
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
index 30628b1af85..274bee15284 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
@@ -2,7 +2,6 @@
 using System.Collections.Generic;
 using System.Reactive.Linq;
 using Avalonia.Controls.Presenters;
-using Avalonia.Controls.Primitives;
 using Avalonia.Layout;
 using Avalonia.UnitTests;
 using Xunit;
@@ -366,7 +365,7 @@ public void BringDescendantIntoView_Should_Update_Offset()
             target.UpdateChild();
             target.Measure(Size.Infinity);
             target.Arrange(new Rect(0, 0, 100, 100));
-            target.BringDescendantIntoView(target.Child, new Rect(200, 200, 0, 0));
+            target.BringDescendantIntoView(target.Child!, new Rect(200, 200, 0, 0));
 
             Assert.Equal(new Vector(100, 100), target.Offset);
         }
@@ -400,6 +399,46 @@ public void BringDescendantIntoView_Should_Handle_Child_Margin()
             Assert.Equal(new Vector(150, 150), target.Offset);
         }
 
+        [Fact]
+        public void Nested_Presenters_Should_Scroll_Outer_When_Content_Exceeds_Viewport()
+        {
+            ScrollContentPresenter innerPresenter;
+            Border border;
+
+            var outerPresenter = new ScrollContentPresenter
+            {
+                CanHorizontallyScroll = true,
+                CanVerticallyScroll = true,
+                Width = 100,
+                Height = 100,
+                Content = innerPresenter = new ScrollContentPresenter
+                {
+                    CanHorizontallyScroll = true,
+                    CanVerticallyScroll = true,
+                    Width = 100,
+                    Height = 200,
+                    Content = border = new Border
+                    {
+                        Width = 200, // larger than viewport
+                        Height = 25,
+                        HorizontalAlignment = HorizontalAlignment.Left,
+                        VerticalAlignment = VerticalAlignment.Top,
+                        Margin = new Thickness(0, 120, 0, 0)
+                    }
+                }
+            };
+
+            innerPresenter.UpdateChild();
+            outerPresenter.UpdateChild();
+            outerPresenter.Measure(new Size(100, 100));
+            outerPresenter.Arrange(new Rect(0, 0, 100, 100));
+
+            border.BringIntoView();
+
+            Assert.Equal(new Vector(0, 45), outerPresenter.Offset);
+            Assert.Equal(new Vector(0, 0), innerPresenter.Offset);
+        }
+
         private class TestControl : Control
         {
             public Size AvailableSize { get; private set; }