Skip to content

Commit 2c7e2b3

Browse files
alexander.marekclaude
andcommitted
#20259 - three things a reviewer would have found first
ContentVirtualizationDiagnostics -> ContainerVirtualization: a behaviour kill switch does not belong in a class named for diagnostics. Pure rename, no call site changed shape. DataTemplate.MinPoolSizePerKey gets a setter. It is documented as the warmup-depth knob and was unreachable from the markup meant to configure it, while MaxPoolSizePerKey one line above was settable and FuncDataTemplate.MinPoolSizePerKey already was. AdjustElementSize stops being a protected internal virtual test seam. The protected half made it public API by accident and nothing in production wants to change a measured size, so it is now the internal ElementSizeAdjustmentForTesting delegate, applied in GetElementSizeU where the method was called. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 587b461 commit 2c7e2b3

5 files changed

Lines changed: 114 additions & 82 deletions

File tree

src/Avalonia.Controls/ItemsControl.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ protected internal virtual void ClearContainerForItemOverride(Control container)
474474
// the same condition NeedsContainer<T> keys on.
475475
// When we skip clearing, the Child stays attached to this container
476476
var shouldSkipClear = cc.Presenter != null &&
477-
ContentVirtualizationDiagnostics.IsEnabled &&
477+
ContainerVirtualization.IsEnabled &&
478478
Presenter?.Panel is VirtualizingStackPanel &&
479479
cc.ContentTemplate is IVirtualizingDataTemplate vdt &&
480480
vdt.GetKey(cc.Content) != null;
@@ -494,7 +494,7 @@ cc.ContentTemplate is IVirtualizingDataTemplate vdt &&
494494
// only for a template that opted in by handing out a recycle key for this item -
495495
// the same condition NeedsContainer<T> keys on.
496496
// When we skip clearing, the Child stays attached to this container
497-
var shouldSkipClear = ContentVirtualizationDiagnostics.IsEnabled &&
497+
var shouldSkipClear = ContainerVirtualization.IsEnabled &&
498498
Presenter?.Panel is VirtualizingStackPanel &&
499499
p.ContentTemplate is IVirtualizingDataTemplate vdt &&
500500
vdt.GetKey(p.Content) != null;
@@ -576,7 +576,7 @@ protected bool NeedsContainer<T>(object? item, out object? recycleKey) where T :
576576
// for this item. Keys partition the container pool so a container is only ever reused
577577
// for data that its retained Child can display. Anything else falls through to stock
578578
// behaviour - a single shared pool under DefaultRecycleKey.
579-
if (ContentVirtualizationDiagnostics.IsEnabled &&
579+
if (ContainerVirtualization.IsEnabled &&
580580
Presenter?.Panel is VirtualizingStackPanel &&
581581
GetEffectiveItemTemplate() is IVirtualizingDataTemplate vdt &&
582582
vdt.GetKey(item) is { } key)
@@ -982,9 +982,9 @@ bool IChildIndexProvider.TryGetTotalCount(out int count)
982982
}
983983

984984
/// <summary>
985-
/// Provides diagnostics for content virtualization.
985+
/// Global switch for opt-in container-level virtualization.
986986
/// </summary>
987-
public static class ContentVirtualizationDiagnostics
987+
public static class ContainerVirtualization
988988
{
989989
/// <summary>
990990
/// Gets or sets whether container-level virtualization is globally enabled.

src/Avalonia.Controls/VirtualizingStackPanel.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,29 +1526,29 @@ private double GetOrEstimateElementU(int index)
15261526
}
15271527

15281528
/// <summary>
1529-
/// Called after each element is measured during realization. Override in tests
1530-
/// to simulate non-deterministic measurement (async image loading, text wrapping)
1531-
/// by returning a modified size. The default implementation returns the measured
1532-
/// size unchanged.
1529+
/// Test-only seam applied to every size the panel reads off an element, so a test can
1530+
/// simulate non-deterministic measurement (async image loading, text wrapping) without a
1531+
/// template that actually behaves that way. Takes the item index and the measured size in
1532+
/// the layout orientation, and returns the size the panel should use.
15331533
/// </summary>
1534-
/// <param name="index">The item index.</param>
1535-
/// <param name="measuredSizeU">The element's measured size in the layout orientation.</param>
1536-
/// <returns>The size to use for layout. Defaults to <paramref name="measuredSizeU"/>.</returns>
1537-
protected internal virtual double AdjustElementSize(int index, double measuredSizeU)
1538-
=> measuredSizeU;
1534+
/// <remarks>
1535+
/// Deliberately internal and not a <c>protected virtual</c> method: nothing in production
1536+
/// wants to change a measured size, so this must not become public API.
1537+
/// </remarks>
1538+
internal Func<int, double, double>? ElementSizeAdjustmentForTesting { get; set; }
15391539

15401540
/// <summary>
15411541
/// The panel's single view of an element's size along the layout axis. Every place that
15421542
/// records or re-checks a size must go through here: if size *recording* applied
1543-
/// <see cref="AdjustElementSize"/> but size *checking* did not, the two would disagree by
1544-
/// the adjustment on every pass and each pass would look like a fresh resize.
1543+
/// <see cref="ElementSizeAdjustmentForTesting"/> but size *checking* did not, the two would
1544+
/// disagree by the adjustment on every pass and each pass would look like a fresh resize.
15451545
/// </summary>
15461546
private double GetElementSizeU(Control element, int index)
15471547
{
15481548
var sizeU = Orientation == Orientation.Horizontal
15491549
? element.DesiredSize.Width
15501550
: element.DesiredSize.Height;
1551-
return AdjustElementSize(index, sizeU);
1551+
return ElementSizeAdjustmentForTesting is { } adjust ? adjust(index, sizeU) : sizeU;
15521552
}
15531553

15541554
private void RealizeElements(

src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ public class DataTemplate : IRecyclingDataTemplate, ITypedDataTemplate, IVirtual
2828
public int MaxPoolSizePerKey { get; set; } = 5;
2929

3030
/// <summary>
31-
/// Gets the minimum number of controls to keep in the recycle pool
31+
/// Gets or sets the minimum number of controls to keep in the recycle pool
3232
/// for each key. Default is 2.
3333
/// This is only used when warmup is enabled
3434
/// </summary>
35-
public int MinPoolSizePerKey { get; } = 2;
35+
public int MinPoolSizePerKey { get; set; } = 2;
3636

3737
public bool Match(object? data)
3838
{

tests/Avalonia.Controls.UnitTests/ContainerVirtualizationTests.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,43 @@ public void MaxPoolSizePerKey_Is_Respected_For_DataTemplate_With_EnableVirtualiz
7474
Assert.Equal(2, PooledCount(panel));
7575
}
7676

77+
/// <summary>
78+
/// <c>MinPoolSizePerKey</c> is the warmup-depth knob, and the markup it is meant to be
79+
/// configured from is a XAML <c>DataTemplate</c> - so it has to be settable there and the
80+
/// value has to reach <c>DiscoverTemplateKeys</c>, which is the only thing that reads it.
81+
/// </summary>
82+
[Fact]
83+
public void MinPoolSizePerKey_Set_On_A_DataTemplate_Reaches_Warmup()
84+
{
85+
using var app = App();
86+
87+
var template = CanvasTemplate(enableVirtualization: true, dataType: typeof(TypeA_Item));
88+
template.MinPoolSizePerKey = 7;
89+
var items = CreateItems<TypeA_Item>(200);
90+
91+
var (panel, _, _, _) = CreateTarget(items, template);
92+
93+
var keys = panel.DiscoverTemplateKeys();
94+
95+
Assert.True(keys.TryGetValue(typeof(TypeA_Item), out var depth),
96+
"The template's key was never encountered, so warmup depth was not resolved at all.");
97+
98+
// 3 is DefaultWarmupPoolSizePerKey - i.e. what this asserts is that the template was
99+
// asked, not that some default happened to match.
100+
Assert.Equal(7, depth);
101+
}
102+
77103
// ===== (c) IsEnabled = false is a kill switch back to stock behaviour =====
78104

79105
[Fact]
80106
public void IsEnabled_False_Forces_Default_Recycle_Key_And_Clears_Content()
81107
{
82108
using var app = App();
83-
var original = ContentVirtualizationDiagnostics.IsEnabled;
109+
var original = ContainerVirtualization.IsEnabled;
84110

85111
try
86112
{
87-
ContentVirtualizationDiagnostics.IsEnabled = false;
113+
ContainerVirtualization.IsEnabled = false;
88114

89115
// A template that opts in and keys per item type - with the kill switch off it must
90116
// be ignored entirely.
@@ -122,7 +148,7 @@ public void IsEnabled_False_Forces_Default_Recycle_Key_And_Clears_Content()
122148
}
123149
finally
124150
{
125-
ContentVirtualizationDiagnostics.IsEnabled = original;
151+
ContainerVirtualization.IsEnabled = original;
126152
}
127153
}
128154

0 commit comments

Comments
 (0)