Repository: MudBlazor/MudBlazor (2026-04-21T17:10:10Z)

Issue:
MudSelect: Custom Comparer with Selected Values
### Before reporting

- [x] I searched existing issues
- [x] I tried in private/incognito browser

### What went wrong?

When `MudSelect` has set `Comparer` parameter and it has some initial items in `SelectedValues` bound collection, the input doesn't display anything. Additionally, to the reproduction link check this one out: https://try.mudblazor.com/snippet/QYwUOScFHNLyPXgw
It causes deadlock when you try to run it. 
Without `Comparer` parameter both snippets are working fine.

### Reproduction link

https://try.mudblazor.com/snippet/wYmUaScbdtghCGxw

### Reproduction steps

1. Run snippet
2. See that "test1" is not selected in the input


### Version (bug)

v9.3.0

### Version (working)

v8.15.0

### Environment

Chrome

### Blazor rendering mode

WASM


Discussion:

Patch:
diff --git a/src/MudBlazor/Components/Select/MudSelect.razor.cs b/src/MudBlazor/Components/Select/MudSelect.razor.cs
index 81deab0de77e..322217f61c1e 100644
--- a/src/MudBlazor/Components/Select/MudSelect.razor.cs
+++ b/src/MudBlazor/Components/Select/MudSelect.razor.cs
@@ -779,11 +779,15 @@ public override ValueTask SelectRangeAsync(int pos1, int pos2)
             return _elementReference.SelectRangeAsync(pos1, pos2);
         }
 
-        private async Task OnComparerChangedAsync(ParameterChangedEventArgs<IEqualityComparer<T?>?> arg)
+        private Task OnComparerChangedAsync(ParameterChangedEventArgs<IEqualityComparer<T?>?> arg)
         {
-            // Apply comparer and refresh selected values
+            // Rebuild the internal HashSet so future equality checks use the new comparer.
+            // Do NOT push to _selectedValuesState here: during initial parameter processing,
+            // this handler fires before OnSelectedValuesChangedAsync has populated _selectedValues,
+            // so SetValueAsync would publish an empty snapshot and invoke SelectedValuesChanged,
+            // silently overwriting the parent's @bind-SelectedValues binding to empty.
             _selectedValues = new HashSet<T?>(_selectedValues, arg.Value);
-            await UpdateSelectedValuesStateAsync(arg.Value);
+            return Task.CompletedTask;
         }
 
         private async Task OnSelectedValuesChangedAsync(ParameterChangedEventArgs<IReadOnlyCollection<T?>?> arg)


Test patch:
diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithCustomComparerInitialBindTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithCustomComparerInitialBindTest.razor
new file mode 100644
index 000000000000..e42196d31d95
--- /dev/null
+++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithCustomComparerInitialBindTest.razor
@@ -0,0 +1,32 @@
+<MudPopoverProvider />
+
+<MudSelect @bind-SelectedValues="SelectedItems"
+           Label="Test"
+           Variant="Variant.Outlined"
+           MultiSelection="true"
+           Comparer="TestStringComparer">
+    @foreach (var item in TestItems)
+    {
+        <MudSelectItem Value="item" />
+    }
+</MudSelect>
+
+@code {
+    public IReadOnlyCollection<string> TestItems { get; set; } = ["test1", "test2", "test3"];
+
+    public IReadOnlyCollection<string> SelectedItems { get; set; } = ["test1"];
+
+    public StringComparer TestStringComparer { get; set; } = new StringComparer();
+
+    public class StringComparer : IEqualityComparer<string?>
+    {
+        public bool Equals(string? x, string? y)
+        {
+            if (x is null && y is null) return true;
+            if (x is null || y is null) return false;
+            return x.Equals(y);
+        }
+
+        public int GetHashCode(string obj) => obj.GetHashCode();
+    }
+}
diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithCustomComparerInitialSelectionTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithCustomComparerInitialSelectionTest.razor
new file mode 100644
index 000000000000..4c8eec3cf88b
--- /dev/null
+++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/Select/MultiSelectWithCustomComparerInitialSelectionTest.razor
@@ -0,0 +1,48 @@
+<MudPopoverProvider />
+
+<MudGrid>
+    <MudItem xs="12">
+        <MudSelect T="Coffee" Label="Coffee"
+                   MultiSelection="true"
+                   SelectedValues="_selected" SelectedValuesChanged="SelectedChanged"
+                   Comparer="Comparer"
+                   ToStringFunc="@(x => x?.Name)">
+            <MudSelectItem Value="@(new Coffee("cap","Cappuccino"))" />
+            <MudSelectItem Value="@(new Coffee("lat","Cafe Latte"))" />
+            <MudSelectItem Value="@(new Coffee("esp","Espresso"))" />
+            <MudSelectItem Value="@(new Coffee("ire","Irish Coffee"))" />
+        </MudSelect>
+    </MudItem>
+</MudGrid>
+
+@code {
+    private static CoffeeComparer Comparer { get; } = new();
+
+    // Non-empty initial selection set at construction time: the regression case.
+    // Keys match "lat" and "esp" but Name differs to prove the custom comparer is honored.
+    public IReadOnlyCollection<Coffee?> _selected { get; set; } = new HashSet<Coffee?>(Comparer)
+    {
+        new Coffee("lat", "Preselected Latte"),
+        new Coffee("esp", "Preselected Espresso")
+    };
+
+    private void SelectedChanged(IReadOnlyCollection<Coffee?>? newSelected)
+    {
+        _selected = newSelected is null
+            ? new HashSet<Coffee?>(Comparer)
+            : new HashSet<Coffee?>(newSelected, Comparer);
+    }
+
+    public class Coffee(string key, string name)
+    {
+        public string Key { get; } = key;
+        public string Name { get; } = name;
+    }
+
+    private class CoffeeComparer : IEqualityComparer<Coffee?>
+    {
+        public bool Equals(Coffee? a, Coffee? b) => a?.Key == b?.Key;
+
+        public int GetHashCode(Coffee? x) => HashCode.Combine(x?.Key);
+    }
+}
diff --git a/src/MudBlazor.UnitTests/Components/SelectTests.cs b/src/MudBlazor.UnitTests/Components/SelectTests.cs
index 228641a2bd14..21583a1b75e6 100644
--- a/src/MudBlazor.UnitTests/Components/SelectTests.cs
+++ b/src/MudBlazor.UnitTests/Components/SelectTests.cs
@@ -1461,6 +1461,55 @@ public async Task MultiSelectWithCustomComparer()
             icons[7].Attributes["d"].Value.Should().Be(@unchecked);
         }
 
+        [Test(Description = "https://github.com/MudBlazor/MudBlazor/issues/13106")]
+        public async Task MultiSelectWithCustomComparer_InitialSelectionPreservedOnFirstRender()
+        {
+            var comp = Context.Render<MultiSelectWithCustomComparerInitialSelectionTest>();
+
+            // The parent's bound collection must still contain both preselected items.
+            comp.Instance._selected.Should().HaveCount(2);
+            comp.Instance._selected.Select(c => c!.Key).Should().BeEquivalentTo("lat", "esp");
+
+            // The input text reflects the preselected values' names (proves the comparer matched on Key).
+            comp.Find("input").GetAttribute("value").Should().Be("Preselected Latte, Preselected Espresso");
+
+            // Open the menu and assert checkbox icons.
+            await comp.Find("div.mud-input-control").MouseDownAsync();
+
+            const string @unchecked =
+                "M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z";
+            const string @checked =
+                "M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z";
+            var icons = comp.FindAll("div.mud-list-item path").ToArray();
+            icons[1].Attributes["d"].Value.Should().Be(@unchecked); // Cappuccino
+            icons[3].Attributes["d"].Value.Should().Be(@checked);   // Cafe Latte (Key="lat")
+            icons[5].Attributes["d"].Value.Should().Be(@checked);   // Espresso   (Key="esp")
+            icons[7].Attributes["d"].Value.Should().Be(@unchecked); // Irish Coffee
+        }
+
+        [Test(Description = "https://github.com/MudBlazor/MudBlazor/issues/13106")]
+        public async Task MultiSelectWithCustomComparer_InitialBindPreservedOnFirstRender()
+        {
+            var comp = Context.Render<MultiSelectWithCustomComparerInitialBindTest>();
+
+            // @bind target must still hold ["test1"].
+            comp.Instance.SelectedItems.Should().BeEquivalentTo("test1");
+
+            // Rendered input reflects the preselected value.
+            comp.Find("input").GetAttribute("value").Should().Be("test1");
+
+            await comp.Find("div.mud-input-control").MouseDownAsync();
+
+            const string @unchecked =
+                "M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z";
+            const string @checked =
+                "M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z";
+            var icons = comp.FindAll("div.mud-list-item path").ToArray();
+            icons[1].Attributes["d"].Value.Should().Be(@checked);   // test1
+            icons[3].Attributes["d"].Value.Should().Be(@unchecked); // test2
+            icons[5].Attributes["d"].Value.Should().Be(@unchecked); // test3
+        }
+
         [Test]
         public async Task Select_Item_Collection_Should_Match_Number_Of_Select_Options()
         {