You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
        }
    }
}

问题原因

  1. SelectItem方法为同步方法,触发SelectedItemChanged和ValueChanged后立即切换IsAltering状态,但EventCallback执行是异步的,父组件更新参数需要时间,此时组件渲染时SelectedItem仍未更新,导致UI未切换到选中展示区域。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 18:01:57