Blazor组件首次选择值更新但选中项显示异常问题求助
Blazor搜索选择组件首次选中后显示异常问题
我正在开发一款Blazor组件,用于数据库搜索、生成下拉列表并选择选项。当前功能整体可用,但首次选择选项时,用于展示的选中项未更新(尽管底层值已成功更新);再次选择相同结果时,显示恢复正常。
现象说明
- 搜索功能正常工作
- 首次选择后,底层值已更新但选中项展示区域无变化
- 再次选择同一结果,选中项展示恢复正常
基础组件(BrickSearchSelect)原代码
@using System.Diagnostics.CodeAnalysis; @using System.Globalization; @using System.Timers; @typeparam T @typeparam TBase <div class="search-container d-flex position-relative flex-column gap-2"> @if (IsAltering || SelectedItem == null) { <div class="d-flex align-items-end gap-2"> <div class="form-group col"> @if (!string.IsNullOrWhiteSpace(Label)) { <label class="form-label">@Label</label> } <input type="text" class="form-control" @onblur="FocusOut" @onfocus="@(() => ShowResult = Search?.Length >= MinLength)" @onsubmit:preventDefault="true" @onsubmit:stopPropagation="true" @oninput="@(async e => await SearchChanged(e))" @bind="@Search" /> </div> @if (!CurrentValue.Equals(default(T)) && CurrentValue is not null) { <button type="button" class="btn btn-outline-warning" @onclick="@(() => IsAltering = false)">Cancelar</button> } </div> @if (ShowResult) { <div class="mt-1 z-3 position-absolute rounded-3 shadow-sm overflow-y-auto top-100 start-0 w-100" style="max-height:25vh"> <div class="result-list list-group"> @foreach (var result in Result) { <button type="button" class="list-group-item list-group-item-action" @onclick="@(() => SelectItem(result))"> @ListItem.Invoke(result) </button> } </div> </div> } } else { <div class="d-flex gap-2 align-items-end"> <div class="col"> @Selected.Invoke(CurrentItem) </div> <button type="button" class="btn btn-outline-primary" @onclick="@(() => IsAltering = true)">Alterar</button> </div> } </div> @code { [Parameter] public T Value { get; set; } [Parameter] public EventCallback<T> ValueChanged { get; set; } [Parameter] public Func<string, Task<ICollection<TBase>>> ListMethod { get; set; } private T CurrentValue { get => Value; set { if (value.Equals(Value)) return; ValueChanged.InvokeAsync(value); } } [Parameter] public EventCallback reload { get; set; } private string Search { get; set; } [Parameter] public string Label { get; set; } = ""; [Parameter] public RenderFragment<TBase> ListItem { get; set; } [Parameter] public RenderFragment<TBase> Selected { get; set; } [Parameter] public int MinLength { get; set; } = 3; [Parameter] public TBase SelectedItem { get; set; } [Parameter] public EventCallback<TBase> SelectedItemChanged { get; set; } private TBase CurrentItem { get => SelectedItem; set { if (value.Equals(CurrentItem)) return; SelectedItemChanged.InvokeAsync(value); } } [Parameter] public Func<TBase, T> OnClick { get; set; } private void SelectItem(TBase item) { CurrentItem = item; ShowResult = false; IsAltering = false; Search = ""; CurrentValue = OnClick.Invoke(item); } private bool IsAltering { get; set; } = true; private bool ShowResult { get; set; } = false; private ICollection<TBase> Result { get; set; } private Timer timer { get; set; } = new(); private bool Loading { get; set; } = false; private void FocusOut() { timer.Dispose(); timer = new Timer(300); timer.Elapsed += ((e, f) => ShowResult = false); timer.Enabled = true; timer.Start(); } private async Task SearchChanged(ChangeEventArgs value) { if (value.Value.ToString() != Search) { Search = value.Value.ToString(); if (Search.Length >= MinLength) { timer.Dispose(); timer = new Timer(300); timer.Elapsed += GetResults; timer.Enabled = true; timer.Start(); } else { ShowResult = false; } } } private async void GetResults(object sender, EventArgs e) { timer.Dispose(); Loading = true; Result = await ListMethod.Invoke(Search); Loading = false; ShowResult = true; await InvokeAsync(StateHasChanged); } protected override void OnInitialized() { base.OnInitialized(); if (!Value.Equals(default(T)) && Value is not null) { IsAltering = false; } } }
使用基础组件的子组件原代码
@inherits OwningComponentBase<IContractService> <div class="d-flex gap-2 align-items-end"> <div class="col"> <BrickSearchSelect @bind-Value="CurrentValue" @bind-SelectedItem="Company" reload="@(() => InvokeAsync(StateHasChanged))" ListMethod="SearchCompany" Label="Selecione uma Companhia" OnClick="@(x => x.Id)"> <ListItem> <div class="d-flex gap-2"> <div class="col"> <span class="d-block fw-bold">@context.Name</span> <span class="d-block text-muted small">@(context.TradeName ?? "")</span> </div> <div class="col"> <div class="span d-block small text-muted">RUC</div> <span class="d-block fw-bold">@context.RUC</span> </div> </div> </ListItem> <Selected Context="ctx"> <div class="d-flex gap-2 align-items-center"> <div class="col"> <span class="d-block small text-muted">NOME:</span> <span class="fw-bold d-block">@(ctx.Name ?? "Erro")</span> </div> <div class="col"> <span class="d-block small text-muted">RUC:</span> <span class="fw-bold d-block">@(ctx.RUC ?? "Erro")</span> </div> </div> </Selected> </BrickSearchSelect> </div> <button type="button" class="btn btn-outline-primary" @onclick="@(() => AddNewCompany = true)">Nova Companhia</button> </div> <Modal @bind-Show="AddNewCompany" Title="Adicionar nova Companhia" Size="Modal.ModalSize.Large" OnConfirm="@(() => AddCompany())"> <CompanyForm EditContext="NewCompanyEditContext" IsEdit="false" @bind-Value="NewCompany" ShowButtons="false" /> </Modal> @code { [Parameter] public Guid CompanyId { get; set; } [Parameter] public EventCallback<Guid> CompanyIdChanged { get; set; } private Guid CurrentValue { get => CompanyId; set { if (value.Equals(CompanyId)) return; CompanyIdChanged.InvokeAsync(value); } } private Company Company { get; set; } = new(); private bool AddNewCompany { get; set; } = false; private Company NewCompany { get; set; } = new(); private EditContext NewCompanyEditContext { get; set; } private async Task AddCompany() { @if (NewCompanyEditContext.Validate()) { await Service.AddCompany(NewCompany); var addedCompany = NewCompany; CompanyId = addedCompany.Id; Company = addedCompany; AddNewCompany = false; NewCompany = new(); } } private async Task<Guid> OnSelect(Company company) { return company.Id; } private async Task<ICollection<Company>> SearchCompany(string search) { return await Service.SearchCompanies(search, 0, 0, x => x.Name, false); } protected override async void OnInitialized() { base.OnInitialized(); NewCompanyEditContext = new EditContext(NewCompany); if (CompanyId != Guid.Empty) { Company = await Service.GetCompanyById(CompanyId); } } }
问题原因
SelectItem方法为同步方法,触发SelectedItemChanged和ValueChanged后立即切换IsAltering状态,但EventCallback执行是异步的,父组件更新参数需要时间,此时组件渲染时SelectedItem仍未更新,导致UI未切换到选中展示区域。CurrentItem和CurrentValue的封装逻辑错误:子组件不能直接修改父组件传递的参数,只能通过EventCallback通知父组件更新,而组件内部状态判断依赖于父组件回传的参数,存在异步延迟。
修复方案
1. 修改基础组件核心逻辑
- 将
SelectItem改为异步方法,等待父组件完成参数更新后再切换UI状态 - 移除错误的参数封装逻辑,直接调用EventCallback与父组件交互
- 手动触发
StateHasChanged确保组件及时同步最新状态
修复后的基础组件代码
@using System.Diagnostics.CodeAnalysis; @using System.Globalization; @using System.Timers; @typeparam T @typeparam TBase <div class="search-container d-flex position-relative flex-column gap-2"> @if (IsAltering || SelectedItem == null) { <div class="d-flex align-items-end gap-2"> <div class="form-group col"> @if (!string.IsNullOrWhiteSpace(Label)) { <label class="form-label">@Label</label> } <input type="text" class="form-control" @onblur="FocusOut" @onfocus="@(() => ShowResult = Search?.Length >= MinLength)" @onsubmit:preventDefault="true" @onsubmit:stopPropagation="true" @oninput="@(async e => await SearchChanged(e))" @bind="@Search" /> </div> @if (!Value.Equals(default(T)) && Value is not null) { <button type="button" class="btn btn-outline-warning" @onclick="@(() => IsAltering = false)">Cancelar</button> } </div> @if (ShowResult) { <div class="mt-1 z-3 position-absolute rounded-3 shadow-sm overflow-y-auto top-100 start-0 w-100" style="max-height:25vh"> <div class="result-list list-group"> @foreach (var result in Result) { <button type="button" class="list-group-item list-group-item-action" @onclick="@(() => SelectItem(result))"> @ListItem.Invoke(result) </button> } </div> </div> } } else { <div class="d-flex gap-2 align-items-end"> <div class="col"> @Selected.Invoke(SelectedItem) </div> <button type="button" class="btn btn-outline-primary" @onclick="@(() => IsAltering = true)">Alterar</button> </div> } </div> @code { [Parameter] public T Value { get; set; } [Parameter] public EventCallback<T> ValueChanged { get; set; } [Parameter] public Func<string, Task<ICollection<TBase>>> ListMethod { get; set; } [Parameter] public EventCallback reload { get; set; } private string Search { get; set; } [Parameter] public string Label { get; set; } = ""; [Parameter] public RenderFragment<TBase> ListItem { get; set; } [Parameter] public RenderFragment<TBase> Selected { get; set; } [Parameter] public int MinLength { get; set; } = 3; [Parameter] public TBase SelectedItem { get; set; } [Parameter] public EventCallback<TBase> SelectedItemChanged { get; set; } [Parameter] public Func<TBase, T> OnClick { get; set; } private async Task SelectItem(TBase item) { // 等待父组件完成参数更新 await SelectedItemChanged.InvokeAsync(item); await ValueChanged.InvokeAsync(OnClick.Invoke(item)); // 更新组件内部状态 ShowResult = false; IsAltering = false; Search = ""; // 强制触发组件重新渲染 await InvokeAsync(StateHasChanged); } private bool IsAltering { get; set; } = true; private bool ShowResult { get; set; } = false; private ICollection<TBase> Result { get; set; } private Timer timer { get; set; } = new(); private bool Loading { get; set; } = false; private void FocusOut() { timer.Dispose(); timer = new Timer(300); timer.Elapsed += ((e, f) => ShowResult = false); timer.Enabled = true; timer.Start(); } private async Task SearchChanged(ChangeEventArgs value) { if (value.Value.ToString() != Search) { Search = value.Value.ToString(); if (Search.Length >= MinLength) { timer.Dispose(); timer = new Timer(300); timer.Elapsed += GetResults; timer.Enabled = true; timer.Start(); } else { ShowResult = false; } } } private async void GetResults(object sender, EventArgs e) { timer.Dispose(); Loading = true; Result = await ListMethod.Invoke(Search); Loading = false; ShowResult = true; await InvokeAsync(StateHasChanged); } protected override void OnInitialized() { base.OnInitialized(); // 同时校验Value和SelectedItem的有效性 if (!Value.Equals(default(T)) && Value is not null && SelectedItem != null) { IsAltering = false; } } }
内容的提问来源于stack exchange,提问作者Adriano Curvo
相关产品推荐
相关产品推荐

