Search code examples
asp.net-coreblazorblazor-server-side

blazor unable to change select option after manually choosing an item more than once


I have a razor page with a server side blazor component where I'm allowing the user to select an option using a more detailed lookup feature that launches in a separate tab using JSInvoke and localStorage. Works well until the user selects the empty option it stops functioning. no errors reported. asp.net 6.0 lts. Im testing with the most current versions of Chrome and Edge as of the 2024 Q3.

What is apparent to me is that if you use the dropdown to select an option more than one time, the look up stops functioning.

steps to reproduce issue case 1:

  • use the drop down to select option 200 without using the lookup feature
  • use the drop down to select the empty first option in the list [select]
  • use the look up button to select option 100 (nothing happens)

steps to reproduce issue case 2:

  • use the drop down to select option 100 without using the lookup feature
  • use the drop down to select option 200 without using the lookup feature
  • use the look up button to select option 100 (nothing happens)

working case 1:

  • use the drop down to select option 200 without using the lookup feature
  • use the look up button to select option 100 (works)

working case 2:

  • use the look up button to select option 100 (works)

Sample project with the error on GIT https://github.com/tet-web-dev/dropdownOptionLookup

Main Razor Page:

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <component type="typeof(lookup.Components.AppWorkspace)" render-mode="Server"  />
</div>
@section Scripts{
    <script>
        let _dotNetObject;
        let lookupWindow;
        var FormScripts = FormScripts || {};
        FormScripts.openLookup = function (o) {
            _dotNetObject = o.objRef
            if (lookupWindow)
                lookupWindow.close();
            lookupWindow = window.open('@Url.Page("LookUp")' )
        }

        window.addEventListener('storage', (event) => {
        if (event.storageArea != localStorage) return;

        if (event.key === 'selected_item') {
            var selected_value = localStorage.getItem('selected_item');
            _dotNetObject.invokeMethodAsync('LookupItemSelect', selected_value);

            localStorage.setItem('selected_item', '0');
            lookupWindow.close();
        }
    });
</script>

Parent Blazor Component:

@using Microsoft.JSInterop
@inject NavigationManager NavManager
@inject IJSRuntime JSRuntime
@implements IDisposable;

<h3>App Workspace</h3>

<button class="btn btn-link" onclick="@(() => LookupClick())">[Lookup]</button>
<SelectWithLookup @ref="ActiveLookup" Options="Options"  />

@code {
DotNetObjectReference<AppWorkspace> ObjectReference;
[Parameter]
public List<SelectOption> Options { get; set; } = new() { new SelectOption() { Id = 100, Text = "Option 100" }, new SelectOption() { Id = 200, Text = "Option 200" } };

private SelectWithLookup ActiveLookup { get; set; } = new();
public List<SelectWithLookup> selects = new List<SelectWithLookup>();

SelectWithLookup SelectRef
{
    set { selects.Add(value); }
}

[JSInvokable("LookupItemSelect")]
public async Task LookupItemSelect(string Id)
{
    await ActiveLookup.LookupSelect(int.Parse(Id));
    StateHasChanged();
}

private async Task LookupClick()
{
    ObjectReference = DotNetObjectReference.Create(this);
    await JSRuntime.InvokeVoidAsync("FormScripts.openLookup", new { objRef = 
    ObjectReference });
}

public void Dispose()
{
        GC.SuppressFinalize(this);
        if (ObjectReference != null)
        {
            ObjectReference.Dispose();
        }
    }
}

Child Blazor Component:

<select class="form-control mb-3" >
    <option value="0">[Select]</option>
    @if (Options is not null)
    {
        @foreach (var item in Options.OrderBy(z => z.Text))
        {
            <option value="@item.Id" selected="@item.IsSelected">@item.Text</option>
        }
    }
</select>

@code {

[Parameter]
public List<SelectOption> Options { get; set; }

public async Task LookupSelect(int id)
{
    Options.ForEach(x =>
    {
        if (id == x.Id)
            {
                x.IsSelected = true;
            }
            else
            {
                x.IsSelected = false;
            }
        });
        StateHasChanged();
    }

    [Parameter]
    public EventCallback<SelectWithLookup> LookupClickEvent { get; set; }

    private async Task LookupClick(SelectWithLookup s)
    {
        await LookupClickEvent.InvokeAsync(s);
    }

}

Solution

  • A whole working demo you could follow:

    AppWorkspace.razor

    @using Microsoft.JSInterop
    @inject NavigationManager NavManager
    @inject IJSRuntime JSRuntime
    @implements IDisposable
    
    <h3>App Workspace</h3>
    
    <button class="btn btn-link" onclick="@(() => LookupClick())">[Lookup]</button>
    <SelectWithLookup @ref="ActiveLookup" Options="Options" @bind-SelectedOption="selectedOption" />
    
    @code {
        DotNetObjectReference<AppWorkspace> ObjectReference;
        private SelectWithLookup ActiveLookup { get; set; } = new();
    
        public List<SelectOption> Options { get; set; } = new()
        {
            new SelectOption() { Id = 100, Text = "Option 100" },
            new SelectOption() { Id = 200, Text = "Option 200" }
        };
    
        private SelectOption selectedOption;
    
        [JSInvokable("LookupItemSelect")]
        public async Task LookupItemSelect(string id)
        {
            selectedOption = Options.FirstOrDefault(option => option.Id == int.Parse(id));
            await InvokeAsync(StateHasChanged);
        }
    
        private async Task LookupClick()
        {
            ObjectReference = DotNetObjectReference.Create(this);
            await JSRuntime.InvokeVoidAsync("FormScripts.openLookup", new { objRef = ObjectReference });
        }
    
        public void Dispose()
        {
            GC.SuppressFinalize(this);
            ObjectReference?.Dispose();
        }
    }
    

    SelectWithLookup.razor

    <select class="form-control mb-3" @bind-value="SelectedOptionId" @bind-value:event="onchange">
        <option value="0">[Select]</option>
        @if (Options is not null)
        {
            @foreach (var item in Options.OrderBy(z => z.Text))
            {
                <option value="@item.Id">@item.Text</option>
            }
        }
    </select>
    
    @code {
        [Parameter]
        public List<SelectOption> Options { get; set; }
    
        private int SelectedOptionId
        {
            get => SelectedOption?.Id ?? 0;
            set
            {
                if (SelectedOption?.Id != value)
                {
                    SelectedOption = Options.FirstOrDefault(option => option.Id == value);
                    SelectedOptionChanged.InvokeAsync(SelectedOption);
                }
            }
        }
    
        private SelectOption _selectedOption;
        [Parameter]
        public SelectOption SelectedOption
        {
            get => _selectedOption;
            set
            {
                if (_selectedOption != value)
                {
                    _selectedOption = value;
                    SelectedOptionChanged.InvokeAsync(value);
                    SelectedOptionId = value?.Id ?? 0;
                }
            }
        }
    
        [Parameter]
        public EventCallback<SelectOption> SelectedOptionChanged { get; set; }
    
        public async Task LookupSelect(int id)
        {
            SelectedOption = Options.FirstOrDefault(option => option.Id == id);
            await InvokeAsync(StateHasChanged);
        }
    }