From 8c53be1c2e8eb5fa88fb880664378d6cc9fc0df4 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 22:57:27 +1000 Subject: [PATCH 01/25] Fix virtualized list item selection state --- .../MudListItemExtended.SelectionState.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListItemExtended.SelectionState.cs diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListItemExtended.SelectionState.cs b/src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListItemExtended.SelectionState.cs new file mode 100644 index 00000000..0245d4f2 --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Components/ListExtended/MudListItemExtended.SelectionState.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; + +namespace MudExtensions +{ + public partial class MudListItemExtended + { + /// + /// Re-applies selection state whenever a list item receives parameters. + /// Virtualized lists can reuse an existing component instance for a different value, + /// so selection cannot rely only on registration/scroll timing. + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (MudListExtended == null) + { + return; + } + + var comparer = MudListExtended.Comparer ?? EqualityComparer.Default; + + _selected = MudListExtended.MultiSelection + ? MudListExtended.SelectedValues?.Contains(Value, comparer) == true + : comparer.Equals(MudListExtended.SelectedValue, Value); + } + } +} From 71ab450eec42f72ba8ef0f7416bb3fe9582e55ac Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 22:57:43 +1000 Subject: [PATCH 02/25] Limit select shadow items to selected values --- .../MudSelectExtended.ShadowItems.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs new file mode 100644 index 00000000..0cfd708a --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; + +namespace MudExtensions +{ + public partial class MudSelectExtended + { + /// + /// Returns only selected values that are present in the current item collection. + /// The hidden list exists to materialize selected item components for presentation; + /// it must not materialize the entire collection when the visible list is virtualized. + /// + protected ICollection? GetShadowItemCollection() + { + if (ItemCollection == null) + { + return null; + } + + var selectedValues = SelectedValues?.ToHashSet(_comparer) ?? new HashSet(_comparer); + return [.. ItemCollection.Where(selectedValues.Contains)]; + } + } +} From 4fbee31e277fbb8c3b2fafbbb28e80869e74dcf5 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 22:58:20 +1000 Subject: [PATCH 03/25] Keep select shadow list bounded to selection --- .../Components/SelectExtended/MudSelectExtended.razor | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor index 18d6f7a5..31a5ae1f 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor @@ -1,4 +1,4 @@ -@namespace MudExtensions +@namespace MudExtensions @typeparam T @inherits MudBaseInputExtended @@ -29,7 +29,7 @@ Variant="@Variant" HasAdornmentStart="@(AdornmentStart != null)" HasAdornmentEnd="true" Value="@(Strict && !IsValueInList ? null : ReadText)" Underline="@Underline" - Disabled="@GetDisabledState()" ReadOnly="true" Error="@ErrorState.Value" ErrorId="@ErrorIdState.Value" + Disabled="@GetDisabledState()" ReadOnly="true" Error="@ErrorState.Value" ErrorId="@ErrorState.Value" Clearable="@(Clearable && !GetReadOnlyState())" OnClearButtonClick="(async (e) => await SelectClearButtonClickHandlerAsync(e))" @attributes="GetUserAttributesForHiddenInput()" OnBlur="@OnLostFocus" ShrinkLabel="@(AdornmentStart != null || ShrinkLabel)" Typo="@Typo" ShowVisualiser="true" DataVisualiserStyle="min-height: 1.1876em; padding-right: 0px;"> @@ -159,12 +159,12 @@ - @*To Be Discussed as Second ShadowItems: Needed to render directly into select to has initial values*@ + @*The hidden list only materializes selected ItemCollection values. It keeps selected item components available for presenters without defeating virtualization.*@
Date: Tue, 15 Sep 2026 22:58:51 +1000 Subject: [PATCH 04/25] Restore select error identifier binding --- .../Components/SelectExtended/MudSelectExtended.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor index 31a5ae1f..1f480daf 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor @@ -29,7 +29,7 @@ Variant="@Variant" HasAdornmentStart="@(AdornmentStart != null)" HasAdornmentEnd="true" Value="@(Strict && !IsValueInList ? null : ReadText)" Underline="@Underline" - Disabled="@GetDisabledState()" ReadOnly="true" Error="@ErrorState.Value" ErrorId="@ErrorState.Value" + Disabled="@GetDisabledState()" ReadOnly="true" Error="@ErrorState.Value" ErrorId="@ErrorIdState.Value" Clearable="@(Clearable && !GetReadOnlyState())" OnClearButtonClick="(async (e) => await SelectClearButtonClickHandlerAsync(e))" @attributes="GetUserAttributesForHiddenInput()" OnBlur="@OnLostFocus" ShrinkLabel="@(AdornmentStart != null || ShrinkLabel)" Typo="@Typo" ShowVisualiser="true" DataVisualiserStyle="min-height: 1.1876em; padding-right: 0px;"> From fad99661e02f8e03d43836c48d16c24d6cb1090f Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:00:09 +1000 Subject: [PATCH 05/25] Add virtualized list selection regression tests --- .../ListExtendedVirtualizationTests.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ListExtendedVirtualizationTests.cs diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ListExtendedVirtualizationTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ListExtendedVirtualizationTests.cs new file mode 100644 index 00000000..227909c7 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/ListExtendedVirtualizationTests.cs @@ -0,0 +1,66 @@ +using AwesomeAssertions; +using Bunit; +using Microsoft.AspNetCore.Components; + +namespace MudExtensions.UnitTests.Components +{ + [TestFixture] + public class ListExtendedVirtualizationTests : BunitTest + { + [Test] + public void ReusedListItem_ReappliesSelectionFromSelectedValues() + { + var selectedValues = new int?[] { 42 }; + var cut = RenderList(selectedValues, value: 1); + + var initialItem = cut.FindComponent>(); + initialItem.Instance.IsSelected.Should().BeFalse(); + + cut.Render(parameters => parameters + .Add(x => x.Clickable, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, selectedValues) + .Add(x => x.ChildContent, RenderItem(42))); + + var reusedItem = cut.FindComponent>(); + ReferenceEquals(initialItem.Instance, reusedItem.Instance).Should().BeTrue(); + reusedItem.Instance.IsSelected.Should().BeTrue(); + } + + [Test] + public void MaterializedListItem_UsesConfiguredComparerForSelection() + { + var cut = Context.Render>(parameters => parameters + .Add(x => x.Clickable, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.Comparer, new AbsoluteValueComparer()) + .Add(x => x.SelectedValues, new int?[] { 42 }) + .Add(x => x.ChildContent, RenderItem(-42))); + + cut.FindComponent>().Instance.IsSelected.Should().BeTrue(); + } + + private IRenderedComponent> RenderList(IEnumerable selectedValues, int? value) + { + return Context.Render>(parameters => parameters + .Add(x => x.Clickable, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, selectedValues) + .Add(x => x.ChildContent, RenderItem(value))); + } + + private static RenderFragment RenderItem(int? value) => builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, nameof(MudListItemExtended.Value), value); + builder.CloseComponent(); + }; + + private sealed class AbsoluteValueComparer : IEqualityComparer + { + public bool Equals(int? x, int? y) => Math.Abs(x ?? 0) == Math.Abs(y ?? 0); + + public int GetHashCode(int? obj) => Math.Abs(obj ?? 0).GetHashCode(); + } + } +} From 39857e4c95651c6143529015c64181134cfe3b0b Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:00:24 +1000 Subject: [PATCH 06/25] Add select virtualization regression tests --- .../SelectExtendedVirtualizationTests.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs new file mode 100644 index 00000000..700eda08 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs @@ -0,0 +1,76 @@ +using AwesomeAssertions; +using Bunit; +using MudExtensions.UnitTests.Extensions; + +namespace MudExtensions.UnitTests.Components +{ + [TestFixture] + public class SelectExtendedVirtualizationTests : BunitTest + { + [Test] + public void VirtualizedItemCollection_ShadowListContainsOnlySelectedItems() + { + var items = Enumerable.Range(1, 4_000).Select(value => (int?)value).ToList(); + var selectedValues = new int?[] { 17, 3_999 }; + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, selectedValues)); + + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(2); + cut.Instance.SelectedValues.Should().BeEquivalentTo(selectedValues); + } + + [Test] + public void VirtualizedItemCollection_InitializedMultiSelectionStillProducesText() + { + var items = Enumerable.Range(1, 4_000).Select(value => (int?)value).ToList(); + var selectedValues = new int?[] { 17, 3_999 }; + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, selectedValues)); + + cut.WaitForAssertion(() => + cut.Instance.GetState(x => x.Text).Should().Be("17, 3999")); + } + + [Test] + public void VirtualizedItemCollection_ShadowListRespectsComparer() + { + var items = new List + { + new(1, "One"), + new(2, "Two"), + new(3, "Three") + }; + var selectedValues = new TestValue?[] { new(2, "Different instance") }; + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.Comparer, new TestValueComparer()) + .Add(x => x.ToStringFunc, value => value?.Name) + .Add(x => x.SelectedValues, selectedValues)); + + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); + shadowList.TextContent.Should().Contain("Two"); + } + + private sealed record TestValue(int Id, string Name); + + private sealed class TestValueComparer : IEqualityComparer + { + public bool Equals(TestValue? x, TestValue? y) => x?.Id == y?.Id; + + public int GetHashCode(TestValue? obj) => obj?.Id.GetHashCode() ?? 0; + } + } +} From 98f325eec25b603c6d95f5d0747a7d53e8f52cc6 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:00:51 +1000 Subject: [PATCH 07/25] Protect shadow list scaling behavior --- .../SelectExtendedVirtualizationScaleTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs new file mode 100644 index 00000000..dcd84b69 --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs @@ -0,0 +1,28 @@ +using AwesomeAssertions; +using Bunit; + +namespace MudExtensions.UnitTests.Components +{ + [TestFixture] + public class SelectExtendedVirtualizationScaleTests : BunitTest + { + [TestCase(10)] + [TestCase(100)] + [TestCase(1_000)] + [TestCase(4_000)] + public void ShadowItemCount_IsBoundedBySelection_NotCollectionSize(int itemCount) + { + var items = Enumerable.Range(1, itemCount).Select(value => (int?)value).ToList(); + var selectedValues = new int?[] { 1, itemCount }; + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, selectedValues)); + + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(2); + } + } +} From 89d94c67b0ad5a5fba242f6b4b1aff5647232236 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:01:13 +1000 Subject: [PATCH 08/25] Add strict virtualization regression coverage --- ...SelectExtendedVirtualizationStrictTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs new file mode 100644 index 00000000..338c693b --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs @@ -0,0 +1,43 @@ +using AwesomeAssertions; +using Bunit; +using MudExtensions.UnitTests.Extensions; + +namespace MudExtensions.UnitTests.Components +{ + [TestFixture] + public class SelectExtendedVirtualizationStrictTests : BunitTest + { + [Test] + public void StrictMode_ValidOffscreenInitialValue_RemainsRepresentable() + { + var items = Enumerable.Range(1, 4_000).Select(value => (int?)value).ToList(); + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.Strict, true) + .Add(x => x.Value, 3_999)); + + cut.WaitForAssertion(() => + cut.Instance.GetState(x => x.Text).Should().Be("3999")); + + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); + } + + [Test] + public void StrictMode_ValueOutsideItemCollection_IsNotAddedToShadowList() + { + var items = Enumerable.Range(1, 100).Select(value => (int?)value).ToList(); + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.Strict, true) + .Add(x => x.Value, 999)); + + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Should().BeEmpty(); + } + } +} From 3ee1d3e53de9685817fe5b737333dc0798d212b8 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:01:30 +1000 Subject: [PATCH 09/25] Cover initialized single selection virtualization --- ...endedVirtualizationSingleSelectionTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs new file mode 100644 index 00000000..72a9b8df --- /dev/null +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs @@ -0,0 +1,27 @@ +using AwesomeAssertions; +using Bunit; +using MudExtensions.UnitTests.Extensions; + +namespace MudExtensions.UnitTests.Components +{ + [TestFixture] + public class SelectExtendedVirtualizationSingleSelectionTests : BunitTest + { + [Test] + public void VirtualizedItemCollection_InitializedSingleSelectionUsesOneShadowItem() + { + var items = Enumerable.Range(1, 4_000).Select(value => (int?)value).ToList(); + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.Value, 3_999)); + + cut.WaitForAssertion(() => + cut.Instance.GetState(x => x.Text).Should().Be("3999")); + + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); + } + } +} From 9651a3b9b7636bcc7bfdcef25a77f0ffe1077315 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:02:43 +1000 Subject: [PATCH 10/25] Add temporary virtualization validation workflow --- .../workflows/virtualization-validation.yml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/virtualization-validation.yml diff --git a/.github/workflows/virtualization-validation.yml b/.github/workflows/virtualization-validation.yml new file mode 100644 index 00000000..59c99415 --- /dev/null +++ b/.github/workflows/virtualization-validation.yml @@ -0,0 +1,31 @@ +name: Virtualization validation + +on: + push: + branches: + - fix/virtualized-list-selection-state + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore + + - name: Test + run: dotnet test --configuration Release -f net10.0 --collect:"XPlat Code Coverage" From 3209ffff744b0f4595b7928ab680b8a5840f34b1 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:12:03 +1000 Subject: [PATCH 11/25] Capture virtualization validation build log --- .github/workflows/virtualization-validation.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/virtualization-validation.yml b/.github/workflows/virtualization-validation.yml index 59c99415..6718098b 100644 --- a/.github/workflows/virtualization-validation.yml +++ b/.github/workflows/virtualization-validation.yml @@ -25,7 +25,17 @@ jobs: run: dotnet restore - name: Build - run: dotnet build --no-restore + shell: bash + run: | + set -o pipefail + dotnet build --no-restore 2>&1 | tee build.log + + - name: Upload build log + if: always() + uses: actions/upload-artifact@v4 + with: + name: virtualization-build-log + path: build.log - name: Test run: dotnet test --configuration Release -f net10.0 --collect:"XPlat Code Coverage" From ebf0026755976ca08bda400d29ab0562f0c2220e Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:15:22 +1000 Subject: [PATCH 12/25] Fix select virtualization text assertions --- .../Components/SelectExtendedVirtualizationTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs index 700eda08..6df6c29e 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs @@ -1,6 +1,5 @@ using AwesomeAssertions; using Bunit; -using MudExtensions.UnitTests.Extensions; namespace MudExtensions.UnitTests.Components { @@ -37,7 +36,7 @@ public void VirtualizedItemCollection_InitializedMultiSelectionStillProducesText .Add(x => x.SelectedValues, selectedValues)); cut.WaitForAssertion(() => - cut.Instance.GetState(x => x.Text).Should().Be("17, 3999")); + cut.Find("input").Attributes["value"]?.Value.Should().Be("17, 3999")); } [Test] From 115971269c759ff7fc29a72989c59d69fc19caa5 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:15:32 +1000 Subject: [PATCH 13/25] Fix strict virtualization text assertion --- .../Components/SelectExtendedVirtualizationStrictTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs index 338c693b..83a959b3 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationStrictTests.cs @@ -1,6 +1,5 @@ using AwesomeAssertions; using Bunit; -using MudExtensions.UnitTests.Extensions; namespace MudExtensions.UnitTests.Components { @@ -19,7 +18,7 @@ public void StrictMode_ValidOffscreenInitialValue_RemainsRepresentable() .Add(x => x.Value, 3_999)); cut.WaitForAssertion(() => - cut.Instance.GetState(x => x.Text).Should().Be("3999")); + cut.Find("input").Attributes["value"]?.Value.Should().Be("3999")); var shadowList = cut.Find("div[style='display: none']"); shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); From 88de0055ac44c7621d7c8451f5f6868478ff90d3 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:15:42 +1000 Subject: [PATCH 14/25] Fix single selection virtualization text assertion --- .../SelectExtendedVirtualizationSingleSelectionTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs index 72a9b8df..912d16f2 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs @@ -1,6 +1,5 @@ using AwesomeAssertions; using Bunit; -using MudExtensions.UnitTests.Extensions; namespace MudExtensions.UnitTests.Components { @@ -18,7 +17,7 @@ public void VirtualizedItemCollection_InitializedSingleSelectionUsesOneShadowIte .Add(x => x.Value, 3_999)); cut.WaitForAssertion(() => - cut.Instance.GetState(x => x.Text).Should().Be("3999")); + cut.Find("input").Attributes["value"]?.Value.Should().Be("3999")); var shadowList = cut.Find("div[style='display: none']"); shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); From 635b8b08ccfe0eefecd8aec8d0f72ce7d3a6c8b0 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Tue, 15 Sep 2026 23:23:32 +1000 Subject: [PATCH 15/25] Fix comparer virtualization test setup order --- .../Components/SelectExtendedVirtualizationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs index 6df6c29e..720b9085 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs @@ -54,9 +54,9 @@ public void VirtualizedItemCollection_ShadowListRespectsComparer() .Add(x => x.ItemCollection, items) .Add(x => x.Virtualize, true) .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, selectedValues) .Add(x => x.Comparer, new TestValueComparer()) - .Add(x => x.ToStringFunc, value => value?.Name) - .Add(x => x.SelectedValues, selectedValues)); + .Add(x => x.ToStringFunc, value => value?.Name)); var shadowList = cut.Find("div[style='display: none']"); shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); From 5c4ca1c05387d7f686cc0b982f98aa24d24887ce Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Wed, 16 Sep 2026 00:07:39 +1000 Subject: [PATCH 16/25] Preserve non-virtualized select item registry --- .../SelectExtended/MudSelectExtended.ShadowItems.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs index 0cfd708a..078ee741 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.ShadowItems.cs @@ -6,15 +6,15 @@ namespace MudExtensions public partial class MudSelectExtended { /// - /// Returns only selected values that are present in the current item collection. - /// The hidden list exists to materialize selected item components for presentation; - /// it must not materialize the entire collection when the visible list is virtualized. + /// Returns the values that must be materialized by the hidden list. + /// Non-virtualized selects preserve the existing full item registry behavior; + /// virtualized selects only materialize selected values that exist in the current item collection. /// protected ICollection? GetShadowItemCollection() { - if (ItemCollection == null) + if (ItemCollection == null || !Virtualize) { - return null; + return ItemCollection; } var selectedValues = SelectedValues?.ToHashSet(_comparer) ?? new HashSet(_comparer); From 76b806ec54cdb43c9b5171bbbd10d4759014583f Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Wed, 16 Sep 2026 00:07:53 +1000 Subject: [PATCH 17/25] Cover non-virtualized select item registry --- .../SelectExtendedVirtualizationScaleTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs index dcd84b69..1d43e2ac 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationScaleTests.cs @@ -24,5 +24,20 @@ public void ShadowItemCount_IsBoundedBySelection_NotCollectionSize(int itemCount var shadowList = cut.Find("div[style='display: none']"); shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(2); } + + [Test] + public void NonVirtualizedItemCollection_PreservesFullRegisteredItemSet() + { + var items = Enumerable.Range(1, 100).Select(value => (int?)value).ToList(); + var selectedValues = new int?[] { 1, 100 }; + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, false) + .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, selectedValues)); + + cut.Instance.Items.Should().HaveCount(items.Count); + } } } From 17249c72db80c79bd8792ad9218adb5cb45a3473 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Wed, 16 Sep 2026 00:12:57 +1000 Subject: [PATCH 18/25] Cover virtualized item-content presentation --- ...endedVirtualizationSingleSelectionTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs index 912d16f2..ab6a51c8 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationSingleSelectionTests.cs @@ -1,5 +1,7 @@ using AwesomeAssertions; using Bunit; +using Microsoft.AspNetCore.Components; +using MudBlazor; namespace MudExtensions.UnitTests.Components { @@ -22,5 +24,26 @@ public void VirtualizedItemCollection_InitializedSingleSelectionUsesOneShadowIte var shadowList = cut.Find("div[style='display: none']"); shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); } + + [Test] + public void VirtualizedItemCollection_OffscreenValueSupportsItemContentPresenter() + { + var items = Enumerable.Range(1, 4_000).Select(value => (int?)value).ToList(); + RenderFragment> template = item => builder => + builder.AddContent(0, $"Selected item {item.Value}"); + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.ValuePresenter, ValuePresenter.ItemContent) + .Add(x => x.ItemTemplate, template) + .Add(x => x.Value, 3_999)); + + cut.WaitForAssertion(() => + cut.Markup.Should().Contain("Selected item 3999")); + + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); + } } } From 088fc6a8673dd8617458dbf20959e580cc814f12 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Wed, 16 Sep 2026 00:13:18 +1000 Subject: [PATCH 19/25] Cover virtualized selection changes --- .../SelectExtendedVirtualizationTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs index 720b9085..1884d078 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs @@ -39,6 +39,30 @@ public void VirtualizedItemCollection_InitializedMultiSelectionStillProducesText cut.Find("input").Attributes["value"]?.Value.Should().Be("17, 3999")); } + [Test] + public async Task VirtualizedItemCollection_ChangedSelectionReplacesShadowItems() + { + var items = Enumerable.Range(1, 4_000).Select(value => (int?)value).ToList(); + + var cut = Context.Render>(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.MultiSelection, true) + .Add(x => x.SelectedValues, new int?[] { 17 })); + + await cut.SetParametersAndRenderAsync(parameters => parameters + .Add(x => x.SelectedValues, new int?[] { 3_999 })); + + cut.WaitForAssertion(() => + { + cut.Find("input").Attributes["value"]?.Value.Should().Be("3999"); + var shadowList = cut.Find("div[style='display: none']"); + shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); + shadowList.TextContent.Should().Contain("3999"); + shadowList.TextContent.Should().NotContain("17"); + }); + } + [Test] public void VirtualizedItemCollection_ShadowListRespectsComparer() { From 4f25e5a35f67c94fa7e421bc6041665456a0f835 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Wed, 16 Sep 2026 00:18:48 +1000 Subject: [PATCH 20/25] Use bUnit v2 rerender API --- .../Components/SelectExtendedVirtualizationTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs index 1884d078..56787303 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs @@ -40,7 +40,7 @@ public void VirtualizedItemCollection_InitializedMultiSelectionStillProducesText } [Test] - public async Task VirtualizedItemCollection_ChangedSelectionReplacesShadowItems() + public void VirtualizedItemCollection_ChangedSelectionReplacesShadowItems() { var items = Enumerable.Range(1, 4_000).Select(value => (int?)value).ToList(); @@ -50,7 +50,10 @@ public async Task VirtualizedItemCollection_ChangedSelectionReplacesShadowItems( .Add(x => x.MultiSelection, true) .Add(x => x.SelectedValues, new int?[] { 17 })); - await cut.SetParametersAndRenderAsync(parameters => parameters + cut.Render(parameters => parameters + .Add(x => x.ItemCollection, items) + .Add(x => x.Virtualize, true) + .Add(x => x.MultiSelection, true) .Add(x => x.SelectedValues, new int?[] { 3_999 })); cut.WaitForAssertion(() => From 1fbc3ce6ae073a9ab7b2062b73139d6d80f5bd47 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Thu, 17 Sep 2026 02:40:51 +1000 Subject: [PATCH 21/25] test: isolate dynamic virtualized selection state from shadow rendering and text --- .../Components/SelectExtendedVirtualizationTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs index 56787303..1c0337c7 100644 --- a/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs +++ b/tests/CodeBeam.MudBlazor.Extensions.UnitTests/Components/SelectExtendedVirtualizationTests.cs @@ -50,20 +50,30 @@ public void VirtualizedItemCollection_ChangedSelectionReplacesShadowItems() .Add(x => x.MultiSelection, true) .Add(x => x.SelectedValues, new int?[] { 17 })); + cut.WaitForAssertion(() => + cut.Find("input").Attributes["value"]?.Value.Should().Be("17")); + cut.Render(parameters => parameters .Add(x => x.ItemCollection, items) .Add(x => x.Virtualize, true) .Add(x => x.MultiSelection, true) .Add(x => x.SelectedValues, new int?[] { 3_999 })); + // Assert each layer independently so failures identify whether parameters, hidden + // components or the input presenter stopped following the selection. + cut.WaitForAssertion(() => + cut.Instance.SelectedValues.Should().BeEquivalentTo(new int?[] { 3_999 })); + cut.WaitForAssertion(() => { - cut.Find("input").Attributes["value"]?.Value.Should().Be("3999"); var shadowList = cut.Find("div[style='display: none']"); shadowList.QuerySelectorAll("div.mud-list-item-extended").Count().Should().Be(1); shadowList.TextContent.Should().Contain("3999"); shadowList.TextContent.Should().NotContain("17"); }); + + cut.WaitForAssertion(() => + cut.Find("input").Attributes["value"]?.Value.Should().Be("3999")); } [Test] From deac1870d9d3bc72eb4e72db409965ca9f402932 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Fri, 18 Sep 2026 10:05:04 +1000 Subject: [PATCH 22/25] fix: synchronize virtualized multiselect text after parameter selection changes --- ...SelectExtended.VirtualizedSelectionText.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.VirtualizedSelectionText.cs diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.VirtualizedSelectionText.cs b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.VirtualizedSelectionText.cs new file mode 100644 index 00000000..3261f851 --- /dev/null +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.VirtualizedSelectionText.cs @@ -0,0 +1,19 @@ +namespace MudExtensions +{ + public partial class MudSelectExtended + { + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + + // SelectedValues is a synchronous parameter setter. Its existing async text + // updates cannot be awaited there, so reconcile the displayed text once + // the incoming parameter set has been applied. Do not depend on the newly + // selected shadow item having rendered yet. + if (Virtualize && MultiSelection && ItemCollection is not null) + { + await UpdateTextPropertyAsync(false); + } + } + } +} From c74d035b030013b819304ee8bff015843a582430 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Sat, 26 Sep 2026 23:31:50 +1000 Subject: [PATCH 23/25] Fix virtualized shadow list refresh on selection changes --- .../Components/SelectExtended/MudSelectExtended.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor index 1f480daf..9106bfb0 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor @@ -163,7 +163,7 @@
- Date: Sat, 26 Sep 2026 23:31:52 +1000 Subject: [PATCH 24/25] Unsubscribe disposed select items from selection updates --- .../SelectExtended/MudSelectItemExtended.razor.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectItemExtended.razor.cs b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectItemExtended.razor.cs index f42fc13c..a74395f3 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectItemExtended.razor.cs +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectItemExtended.razor.cs @@ -198,7 +198,12 @@ public void Dispose() { try { - MudSelectExtended?.Remove(this); + if (MudSelectExtended is { } select) + { + select.SelectionChangedFromOutside -= OnUpdateSelectionStateFromOutside; + select.Remove(this); + } + ((MudSelectExtended?)_shadowParent)?.UnregisterShadowItem(this); } catch (Exception) { } From c05db2b1c96eac96405e0551905e5ee795ec1720 Mon Sep 17 00:00:00 2001 From: Aaron Glover Date: Sat, 26 Sep 2026 23:35:01 +1000 Subject: [PATCH 25/25] Limit shadow list re-keying to virtualized selects --- .../Components/SelectExtended/MudSelectExtended.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor index 9106bfb0..482cc2ff 100644 --- a/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor +++ b/src/CodeBeam.MudBlazor.Extensions/Components/SelectExtended/MudSelectExtended.razor @@ -163,7 +163,7 @@
-