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 🤔
I can reproduce the issue consistently when the item to select has a width larger than the `ListBox` (by resizing the window enough).

This causes the inner `ScrollViewer` (the `ListBox`'s one) to incorrectly handle the `RequestBringIntoView` for the item, preventing the outer one (inside `MainWindow)` from scrolling to the correct position. This in turn prevents the focused item from being part of the realized item list, duplicating it.

Here's a PR to fix that: #14900

@CodeDevAM can you please confirm that this fixes your issue? Several of your videos don't seem to have the item larger than the viewport, so maybe there's still something else going on.
That fix can be tested on 11.2.999-cibuild0046020-beta builds, just in case.
> @CodeDevAM can you please confirm that this fixes your issue? Several of your videos don't seem to have the item larger than the viewport, so maybe there's still something else going on.

Unfortunately there is no improvement with 11.2.999-cibuild0046020-beta. But when I go to official 11.0.10 the issue disappears in my minimal example app.

Good case with 11.2.999-cibuild0046020-beta:
https://github.com/AvaloniaUI/Avalonia/assets/125025268/344451a1-7db1-4f0d-a510-6e4e65221e82

Bad case with 11.2.999-cibuild0046020-beta:
https://github.com/AvaloniaUI/Avalonia/assets/125025268/690948ca-991d-4749-a982-9612512a356f

minimal example app with 11.2.999-cibuild0046020-beta: 
[ListViewAutoScrollBug.zip](https://github.com/AvaloniaUI/Avalonia/files/14555511/ListViewAutoScrollBug.zip)

Good case with 11.0.10:
https://github.com/AvaloniaUI/Avalonia/assets/125025268/167b5f02-aade-4178-955f-1ce0727c5608

"Bad" case that went well with 11.0.10
https://github.com/AvaloniaUI/Avalonia/assets/125025268/4eafb866-e1aa-4680-a39b-e655b3daccfb

minimal example app with 11.0.10: 
[ListViewAutoScrollBug.zip](https://github.com/AvaloniaUI/Avalonia/files/14555568/ListViewAutoScrollBug.zip)


I recommend not to close this issue yet unless it's clear whats the root cause of this issue.

Note: I have minimized the example app to a simple list where adding 13 elements to the list causes the issue but adding 12 elements to the list does not. That's at least how it behaves on my machine.

Patch:
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 04f6b1d8a12..aa62eaaf065 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -285,8 +285,12 @@ public bool BringDescendantIntoView(Visual target, Rect targetRect)
                 return false;
             }
 
+            var oldOffset = Offset;
             SetCurrentValue(OffsetProperty, offset);
-            return true;
+
+            // It's possible that the Offset coercion has changed the offset back to its previous value,
+            // this is common for floating point rounding errors.
+            return !Offset.NearlyEquals(oldOffset);
         }
 
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)


Test patch:
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
index 274bee15284..e70ab520439 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
@@ -439,6 +439,56 @@ public void Nested_Presenters_Should_Scroll_Outer_When_Content_Exceeds_Viewport(
             Assert.Equal(new Vector(0, 0), innerPresenter.Offset);
         }
 
+        [Fact]
+        public void Nested_Presenters_Should_Scroll_Outer_When_Viewports_Are_Close()
+        {
+            ScrollContentPresenter innerPresenter;
+            Border border;
+
+            var outerPresenter = new ScrollContentPresenter
+            {
+                CanHorizontallyScroll = true,
+                CanVerticallyScroll = true,
+                Width = 100,
+                Height = 170.0568181818182,
+                UseLayoutRounding = false,
+                Content = innerPresenter = new ScrollContentPresenter
+                {
+                    CanHorizontallyScroll = true,
+                    CanVerticallyScroll = true,
+                    Width = 100,
+                    Height = 493.2613636363636,
+                    UseLayoutRounding = false,
+                    Content = new StackPanel
+                    {
+                        Children =
+                        {
+                            new Border
+                            {
+                                Height = 455.31818181818187,
+                                UseLayoutRounding = false
+                            },
+                            (border = new Border {
+                                Width = 100,
+                                Height = 37.94318181818182,
+                                UseLayoutRounding = false
+                            })
+                        }
+                    }
+                }
+            };
+
+            innerPresenter.UpdateChild();
+            outerPresenter.UpdateChild();
+            outerPresenter.Measure(new Size(100, 170.0568181818182));
+            outerPresenter.Arrange(new Rect(0, 0, 100, 170.0568181818182));
+
+            border.BringIntoView();
+
+            Assert.Equal(new Vector(0, 323.20454545454544), outerPresenter.Offset);
+            Assert.Equal(new Vector(0, 0), innerPresenter.Offset);
+        }
+
         private class TestControl : Control
         {
             public Size AvailableSize { get; private set; }