Search code examples
event-handlingcomponentsblazor-server-side

Blazor Component value doesn't update first time but updates second time


I'm working on a component to search on the database, generate a dropdown and select an option. Currently it works, except the selected item , which is used for displaying, doesn't update (although the value does)

Search function works properly

Value works but selected item doesn't.

If you select the same result again, it works properly.

Code of the base component (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;
        }
    }
}

Component that uses the base component

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

}

Solution

  • There's a wall of code here, so difficult to put into context.

    However, I can see:

    protected override async void OnInitialized()
    

    with an await which is definitely wrong.

    Switch to using the async version.

    protected override async Task OnInitializedAsync()