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:
steps to reproduce issue case 2:
working case 1:
working case 2:
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);
}
}
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);
}
}