'Blazor Generic SelectList not showing checked items on load

I am using a generic SelectList to render a list of checkboxes. I can check items and save them to the users record without problems. But on retrieving the users record the checked items are not being displayed as checked, even though they are being returned to the page correctly.

Generic SelectList code

@typeparam TItem

@foreach (var item in Items)
{

    var id = Guid.NewGuid();
    <div>
        @if (SelectedItems.Contains(item))
        {
            <input id="@id" type="checkbox" checked="checked" @onchange="_ => HandleChange(item)" />
        }
        else
        {
            <input id="@id" type="checkbox" @onchange="_ => HandleChange(item)" />
        }
        <label for="@id">@ItemTemplate(item)</label>
    </div>
}

@code {
    [Parameter]
    public List<TItem> Items { get; set; }

    [Parameter]
    public RenderFragment<TItem> ItemTemplate { get; set; }

    [Parameter]
    public List<TItem> SelectedItems { get; set; }

    [Parameter]
    public EventCallback<List<TItem>> SelectedItemsChanged { get; set; }

    void HandleChange(TItem item)
    {
        if (SelectedItems.Contains(item))
        {
            SelectedItems.Remove(item);
        }
        else
        {
            SelectedItems.Add(item);
        }

        SelectedItemsChanged.InvokeAsync(SelectedItems);
    }
}

Usage

<SelectList Items="SpecialityService.Specialities" @bind-SelectedItems="SelectedSpecialities" Context="spec">
    <ItemTemplate>
        @spec.Description
    </ItemTemplate>
</SelectList>

Data

@code {
    [Parameter]
    public Guid? id { get; set; }

    List<Speciality> SelectedSpecialities { get; set; } = new List<Speciality>();


    AppUser appUser = new AppUser { };

    protected override async Task OnInitializedAsync()
    {

        await SpecialityService.GetSpecialities();

    }

    protected override async Task OnParametersSetAsync()
    {
        appUser = await UserService.GetSingleAppUser((Guid)id);
        foreach (Speciality speciality in appUser.Specialities)
        {
            SelectedSpecialities.Add(speciality);
        }
    

    }

Results

Resulting page after load



Solution 1:[1]

Your principle problem is that you are trying to do object based equality checking which won't work. You either need to use records instead of classes or override the Equals and GetHashCode methods for your classes.

Here's a slightly modified version of you code that demonstrates both in a demo page.

@typeparam TItem

@foreach (var item in Items)
{
    var id = Guid.NewGuid();
    <div>
        @if (SelectedItems.Contains(item))
        {
            <input id="@id" type="checkbox" checked="checked" @onchange="_ => HandleChange(item)" />
        }
        else
        {
            <input id="@id" type="checkbox" @onchange="_ => HandleChange(item)" />
        }
        @if (ItemTemplate is not null)
        {
            @ItemTemplate(item)
        }
    </div>
}

@code {
    [Parameter]
    public List<TItem> Items { get; set; } = new List<TItem>();

    [Parameter]
    public RenderFragment<TItem>? ItemTemplate { get; set; }

    [Parameter]
    public List<TItem> SelectedItems { get; set; } = new List<TItem>();

    [Parameter]
    public EventCallback<List<TItem>> SelectedItemsChanged { get; set; }

    void HandleChange(TItem item)
    {
        if (SelectedItems.Contains(item))
            SelectedItems.Remove(item);
        else
            SelectedItems.Add(item);

        SelectedItemsChanged.InvokeAsync(SelectedItems);
    }
}
@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

<div>
    This list demonstrates using a record for the list rather than a class. 
</div>
<div class="my-2">First List</div>
<SelectList TItem=Country Items=this.ListOfCountries SelectedItems=this.SelectedCountries SelectedItemsChanged=this.ListChanged Context="spec">
    <ItemTemplate>
        @spec.Name
    </ItemTemplate>
</SelectList>
<div class="p-2">
    Selected = 
    @foreach(var item in SelectedCountries){
        <span class="px-2">@item.Name</span> 
    }
</div>

<div>
    This list demonstrates using a class with custom equality checking and HashCoding similar to a record. 
</div>
<div class="my-2">Second List</div>
<SelectList TItem=CountryClass Items=this.CountryList SelectedItems=this.SelectedCountryList SelectedItemsChanged=this.List2Changed Context="spec">
    <ItemTemplate>
        @spec.Name
    </ItemTemplate>
</SelectList>
<div class="p-2">
    Selected = 
    @foreach(var item in SelectedCountryList){
        <span class="px-2">@item.Name</span> ; 
    }
</div>

@code {
    public List<Country> ListOfCountries => new List<Country>
    {
        new Country { Name = "Holland"},
        new Country { Name = "France"},
        new Country { Name = "Spain"},
        new Country { Name = "Portugal"}
    };

    public List<CountryClass> CountryList => new List<CountryClass>
    {
        new CountryClass { Name = "Holland"},
        new CountryClass { Name = "France"},
        new CountryClass { Name = "Spain"},
        new CountryClass { Name = "Portugal"}
    };

    public List<Country> SelectedCountries = new List<Country>();

    public List<CountryClass> SelectedCountryList = new List<CountryClass>();


    protected async override Task OnInitializedAsync()
    {
        // emulate data get
        await Task.Delay(100);

        SelectedCountries = new List<Country>
        {
        new Country { Name = "France"},
        new Country { Name = "Spain"}
        };

        SelectedCountryList = new List<CountryClass>
        {
        new CountryClass { Name = "Holland"},
        new CountryClass { Name = "Portugal"}
        };
    }

    private void ListChanged(List<Country> countries)
      => SelectedCountries = countries;

    private void List2Changed(List<CountryClass> countries)
       => SelectedCountryList = countries;

    /// Local Data classes

    public record Country
    {
        public string Name { get; set; } = string.Empty;
    }

    public class CountryClass
    {
        public string Name { get; set; } = string.Empty;

        public override bool Equals(object? obj)
        {
            if (obj is not null && obj is CountryClass)
                return this.Name == ((CountryClass)obj).Name;

            return false;
        }

        public override int GetHashCode()
           => HashCode.Combine(this.Name);

    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 MrC aka Shaun Curtis