Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/Avalonia.Controls/VirtualizingStackPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,19 @@ protected internal override int IndexFromContainer(Control container)
// hence the width extent should be correct now, and we can try to scroll again.
scrollToElement.BringIntoView();

_scrollToElement = null;
// The layout pass normally adopts the temporary element into the realized range.
// When it doesn't (for example, while its containing pane has no usable width),
// it is still an internal child. Recycle it before dropping the reference so it
// cannot remain as a visible, unindexed "ghost" element.
if (_scrollToElement is { } unadoptedScrollToElement)
{
var unadoptedScrollToIndex = _scrollToIndex;
_scrollToElement = null;
_scrollToIndex = -1;
RecycleElement(unadoptedScrollToElement, unadoptedScrollToIndex);
return null;
}

_scrollToIndex = -1;
return scrollToElement;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,60 @@ namespace Avalonia.Controls.UnitTests;

public class ListBoxVirtualizationIssueTests : ScopedTestBase
{
[Fact]
public void Expanding_ListBox_After_Scrolling_In_Zero_Width_Pane_Does_Not_Show_Unrealized_Containers()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);

var items = new[]
{
new SizedItem(196, 331),
new SizedItem(186, 258),
new SizedItem(196, 321),
new SizedItem(186, 296),
new SizedItem(150, 340),
new SizedItem(196, 319),
};
var target = new ListBox
{
Width = 0,
Height = 774,
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = items,
ItemTemplate = new FuncDataTemplate<SizedItem>((item, _) => new Border
{
Width = item?.Width ?? 0,
Height = item?.Height ?? 0,
}),
ItemsPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel()),
SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected,
};

var root = new TestRoot(target) { ClientSize = new Size(300, 774) };
root.LayoutManager.ExecuteInitialLayoutPass();

// Scroll the selected item into view while the SplitView pane is effectively hidden.
for (var index = 1; index <= 3; ++index)
{
target.SelectedIndex = index;
root.LayoutManager.ExecuteLayoutPass();
}

// Opening the pane increases the ListBox viewport to 300.
target.Width = 300;
root.LayoutManager.ExecuteLayoutPass();

var panel = Assert.IsType<VirtualizingStackPanel>(target.Presenter!.Panel);
var realized = target.GetRealizedContainers().ToHashSet();
var visibleChildren = panel.Children.Where(x => x.IsVisible).ToList();

Assert.All(visibleChildren, child =>
{
Assert.NotEqual(-1, target.IndexFromContainer(child));
Assert.Contains(child, realized);
});
}

[Fact]
public void Removing_First_Item_After_Scrolling_To_End_Should_Allow_Scrolling_To_Start()
{
Expand Down Expand Up @@ -358,4 +412,16 @@ public Item(int id)
}
public int Id { get; }
}

private class SizedItem
{
public SizedItem(double width, double height)
{
Width = width;
Height = height;
}

public double Width { get; }
public double Height { get; }
}
}