diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 4d71cc8d4f..1b2d0b1ca6 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -441,6 +441,34 @@ namespace Avalonia.Controls
/// The container element.
protected internal virtual void ClearContainerForItemOverride(Control container)
{
+ if (container is HeaderedContentControl hcc)
+ {
+ if (hcc.Content is Control)
+ hcc.Content = null;
+ if (hcc.Header is Control)
+ hcc.Header = null;
+ }
+ else if (container is ContentControl cc)
+ {
+ if (cc.Content is Control)
+ cc.Content = null;
+ }
+ else if (container is ContentPresenter p)
+ {
+ if (p.Content is Control)
+ p.Content = null;
+ }
+ else if (container is HeaderedItemsControl hic)
+ {
+ if (hic.Header is Control)
+ hic.Header = null;
+ }
+ else if (container is HeaderedSelectingItemsControl hsic)
+ {
+ if (hsic.Header is Control)
+ hsic.Header = null;
+ }
+
// Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at
// the WPF source it seems that this isn't done there.
}
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index 98662055ea..8d07a60b26 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -6,11 +6,13 @@ using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
+using Avalonia.Markup.Xaml.Templates;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@@ -786,6 +788,46 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, raised);
}
+ [Fact]
+ public void Handles_Recycling_Control_Items_Inside_Containers()
+ {
+ // Issue #10825
+ using var app = Start();
+
+ // The items must be controls but not of the container type.
+ var items = Enumerable.Range(0, 100).Select(x => new TextBlock
+ {
+ Text = $"Item {x}",
+ Width = 100,
+ Height = 100,
+ }).ToList();
+
+ // Virtualization is required
+ var itemsPanel = new FuncTemplate(() => new VirtualizingStackPanel());
+
+ // Create an ItemsControl which uses containers, and provide a scroll viewer.
+ var target = CreateTarget(
+ items: items,
+ itemsPanel: itemsPanel,
+ scrollViewer: true);
+ var scroll = target.FindAncestorOfType();
+
+ Assert.NotNull(scroll);
+ Assert.Equal(10, target.GetRealizedContainers().Count());
+
+ // Scroll so that half a container is visible: an extra container is generated.
+ scroll.Offset = new(0, 2050);
+ Layout(target);
+
+ // Scroll so that the extra container is no longer needed and recycled.
+ scroll.Offset = new(0, 2100);
+ Layout(target);
+
+ // Scroll back: issue #10825 triggered.
+ scroll.Offset = new(0, 2000);
+ Layout(target);
+ }
+
private static ItemsControl CreateTarget(
object? dataContext = null,
IBinding? displayMemberBinding = null,
@@ -796,13 +838,37 @@ namespace Avalonia.Controls.UnitTests
IEnumerable? dataTemplates = null,
bool performLayout = true)
{
- var target = new ItemsControl
+ return CreateTarget(
+ dataContext: dataContext,
+ displayMemberBinding: displayMemberBinding,
+ items: items,
+ itemsSource: itemsSource,
+ itemContainerTheme: itemContainerTheme,
+ itemTemplate: itemTemplate,
+ dataTemplates: dataTemplates,
+ performLayout: performLayout);
+ }
+
+ private static T CreateTarget(
+ object? dataContext = null,
+ IBinding? displayMemberBinding = null,
+ IList? items = null,
+ IList? itemsSource = null,
+ ControlTheme? itemContainerTheme = null,
+ IDataTemplate? itemTemplate = null,
+ ITemplate? itemsPanel = null,
+ IEnumerable? dataTemplates = null,
+ bool performLayout = true,
+ bool scrollViewer = false)
+ where T : ItemsControl, new()
+ {
+ var target = new T
{
DataContext = dataContext,
DisplayMemberBinding = displayMemberBinding,
ItemContainerTheme = itemContainerTheme,
- ItemsSource = itemsSource,
ItemTemplate = itemTemplate,
+ ItemsSource = itemsSource,
};
if (items is not null)
@@ -811,7 +877,11 @@ namespace Avalonia.Controls.UnitTests
target.Items.Add(item);
}
- var root = CreateRoot(target);
+ if (itemsPanel is not null)
+ target.ItemsPanel = itemsPanel;
+
+ var scroll = scrollViewer ? new ScrollViewer { Content = target } : null;
+ var root = CreateRoot(scroll ?? (Control)target);
if (dataTemplates is not null)
{
@@ -831,12 +901,36 @@ namespace Avalonia.Controls.UnitTests
{
Resources =
{
+ { typeof(ContentControl), CreateContentControlTheme() },
{ typeof(ItemsControl), CreateItemsControlTheme() },
+ { typeof(ScrollViewer), CreateScrollViewerTheme() },
},
Child = child,
};
}
+ private static ControlTheme CreateContentControlTheme()
+ {
+ return new ControlTheme(typeof(ContentControl))
+ {
+ Setters =
+ {
+ new Setter(TreeView.TemplateProperty, CreateContentControlTemplate()),
+ },
+ };
+ }
+
+ private static FuncControlTemplate CreateContentControlTemplate()
+ {
+ return new FuncControlTemplate((parent, scope) =>
+ new ContentPresenter
+ {
+ Name = "PART_ContentPresenter",
+ [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty],
+ [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty],
+ }.RegisterInNameScope(scope));
+ }
+
private static ControlTheme CreateItemsControlTheme()
{
return new ControlTheme(typeof(ItemsControl))
@@ -858,11 +952,50 @@ namespace Avalonia.Controls.UnitTests
Child = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
+ [~ItemsPresenter.ItemsPanelProperty] = parent[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(scope)
};
});
}
+ private static ControlTheme CreateScrollViewerTheme()
+ {
+ return new ControlTheme(typeof(ScrollViewer))
+ {
+ Setters =
+ {
+ new Setter(TreeView.TemplateProperty, CreateScrollViewerTemplate()),
+ },
+ };
+ }
+
+ private static FuncControlTemplate CreateScrollViewerTemplate()
+ {
+ return new FuncControlTemplate((parent, scope) =>
+ new Panel
+ {
+ Children =
+ {
+ new ScrollContentPresenter
+ {
+ Name = "PART_ContentPresenter",
+ [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
+ [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
+ [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
+ [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
+ [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty],
+ [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty],
+ }.RegisterInNameScope(scope),
+ new ScrollBar
+ {
+ Name = "verticalScrollBar",
+ [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty],
+ [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty],
+ }
+ }
+ });
+ }
+
private static void Layout(Control c)
{
(c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass();
@@ -891,6 +1024,26 @@ namespace Avalonia.Controls.UnitTests
textShaperImpl: new MockTextShaperImpl()));
}
+ private class ItemsControlWithContainer : ItemsControl, IStyleable
+ {
+ Type IStyleable.StyleKey => typeof(ItemsControl);
+
+ protected internal override Control CreateContainerForItemOverride()
+ {
+ return new ContainerControl();
+ }
+
+ protected internal override bool IsItemItsOwnContainerOverride(Control item)
+ {
+ return item is ContainerControl;
+ }
+ }
+
+ private class ContainerControl : ContentControl, IStyleable
+ {
+ Type IStyleable.StyleKey => typeof(ContentControl);
+ }
+
private record Item(string Caption, string? Value = null);
}
}