Save button doesn’t trigger event in the first click Blazor Web Assembly Edit form

So, I have an EditForm Component which has InputText Component which triggers the onFieldChanged event. Also, I have a button that is using the OnValidSubmit EventCallback<EditContext>, which then submits the form. The problem is that if the InputText is focused and I try to click the Save button, it doesn’t trigger on the first click because it calls first the FieldChanged event, and does not call the OnValidSubmit event, on the second click it works. It also works if you click somewhere else then in the button, but clicking after the InputText doesn’t work.

How can I make it so it calls the onValidSubmit after the fieldchangedEvent?

Thanks in advance.

Edit

The component EditFormBody:

@inherits ComponentBase 
@typeparam TItem
<div class="panel panel-primary">
<div class="panel-heading">
    <h4 class="panel-title">@Title</h4>
    <div class="panel-heading-btn">
        <a href="javascript:;" class="btn btn-xs btn-icon btn-circle" data-click="panel-expand"><i class="fa fa-expand"></i></a>
    </div>
</div>
<div class="panel-body">
    <div class="@BodyCss">

        @if (ShowWait)
        {
            <PleaseWait />
        }
        else
        {
            <EditForm  EditContext="@FormEditContext" OnValidSubmit="@OnSave">
                <DataAnnotationsValidator />
                <div class="form-group row">
                    <div class="col-12">
                        <Microsoft.AspNetCore.Components.Forms.ValidationSummary />
                    </div>
                </div>
                <div class="form-group row">
                    <div class="col-12">
                        @ChildContent

                    </div>
                </div>
               
                                <button type="submit" class="btn btn-primary mr-1" tabindex="51" disabled="@IsReadOnly">Save</button>
                           
                        </div>
                    
                </div>
            </EditForm>
        }
    </div>
</div>
@code {


[Parameter] public TItem Model { get; set; }

[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public RenderFragment LeftButtons { get; set; }


[Parameter] public bool IsReadOnly { get; set; }
[Parameter] public bool ShowCancel { get; set; } = true;

[Parameter] public EventCallback<EditContext> OnValidSubmit { get; set; }
[Parameter] public EventCallback Cancel { get; set; }
[Parameter] public EventCallback<string> PropertyChanged { get; set; }


private EditContext FormEditContext { get; set; }
private bool ShowWait { get; set; } = true;

protected override async Task OnParametersSetAsync ()
{
    if (Model != null)
    {
        FormEditContext = new EditContext(Model);
        FormEditContext.OnFieldChanged += OnChange;
        ShowWait = false;
    }
    await base.OnParametersSetAsync();
}
private void OnChange (object sender, FieldChangedEventArgs e)
{
    if (e.FieldIdentifier.FieldName != "CurrentValue")
        PropertyChanged.InvokeAsync(e.FieldIdentifier.FieldName);

}
private async Task OnSave (EditContext context)
{
    try
    {
     
        ShowWait = true;
        await OnValidSubmit.InvokeAsync(context);
        ShowWait = false;
    }
    catch (Exception ex)
    {
        ShowWait = false;
        await this.ShowErrorMessage(JS, Title, ex);
    }
   
}
private async Task Delete ()
{
    try
    {
        ShowWait = true;
        await OnValidSubmit.InvokeAsync(null);
        ShowWait = false;

        //await this.NavigateTo(ReturnUrl);
    }
    catch (Exception ex)
    {
        ShowWait = false;
        await this.ShowErrorMessage(JS, Title, ex);
    }
}}

The form where i call the component:

<EditFormBody Model="@CurrentObject" Mode="@Mode" Title="@Title" ReturnUrl="/administration/users" OnValidSubmit="@Save" Cancel="Cancel">
        <ChildContent>
            <InputText  @bind-Value="@CurrentObject.Email" />
        </ChildContent>
    </EditFormBody>

Answer

Here’s a bare bones version of your code. This works in my test environment – Blazor Server project built from the template. I’ve looked at your code and can’t see the issue at the moment. I suggest you take this barebones version, check it works, then build it up unitl you break it. Good luck.

// BasicEditor.razor
@page "/basiceditor"
<BasicEditorCard EditContext="_EditContext" OnValidSubmit="ValidatedSubmit">
    <InputText @bind-Value="model.Email"></InputText>
</BasicEditorCard>
<div>@message</div>

@code {

    public class Model
    {
        public string Email { get; set; }
    }

    private EditContext _EditContext;

    private Model model { get; set; } = new Model() { Email = "[email protected]" };

    private string message = "Not yet clicked";

    protected override Task OnInitializedAsync()
    {
        _EditContext = new EditContext(model);
        return base.OnInitializedAsync();
    }

    private Task ValidatedSubmit(EditContext editContext)
    {
        message = $"clicked at {DateTime.Now.ToLongTimeString()}";
        return Task.CompletedTask;
    }
}
// BasicEditorCard.razor
<EditForm EditContext="EditContext" OnValidSubmit="ValidatedSubmit">
    <DataAnnotationsValidator />
    @ChildContent
    <button type="submit">Submit</button>
</EditForm>
<div>@message</div>

@code {

    [Parameter] public RenderFragment ChildContent { get; set; }

    [Parameter] public EditContext EditContext { get; set; }

    [Parameter] public EventCallback<EditContext> OnValidSubmit { get; set; }

    private string message = "No";

    private Task ValidatedSubmit()
    {
        OnValidSubmit.InvokeAsync(EditContext);
        message = $"clicked at {DateTime.Now.ToLongTimeString()}";
        return Task.CompletedTask;
    }
}

Update

You have a two problems:

Reversed Logic
protected override async Task OnParametersSetAsync ()
{
    // should be if (Model is null)
    if (Model != null)
    {
        FormEditContext = new EditContext(Model);
        FormEditContext.OnFieldChanged += OnChange;
        ShowWait = false;
    }
    await base.OnParametersSetAsync();
}

Your logic is the wrong way round! Every time a parameter changes you are creating a new EditContext.

Generics

Using generics is also causing a problem. EditContext takes an object, so you can do away with the generics in the Component and simply declare Model as follows, losing @typeparam TItem in the process:

[Parameter] public object Model { get; set; }

My final prototype version of your component looks like this:

@implements IDisposable
<EditForm EditContext="EditContext" OnValidSubmit="ValidatedSubmit">
    <DataAnnotationsValidator />
    @ChildContent
    <button type="submit">Submit</button>
</EditForm>
<div>@message</div>
@code {

    [Parameter] public RenderFragment ChildContent { get; set; }
    [Parameter] public object Model { get; set; }
    [Parameter] public EventCallback<EditContext> OnValidSubmit { get; set; }
    private string message = "No";
    protected EditContext EditContext;

    protected override Task OnParametersSetAsync()
    {
        if (this.EditContext is null)
        {
            EditContext = new EditContext(Model);
            EditContext.OnFieldChanged += this.OnFieldChanged;
        }
        return base.OnParametersSetAsync();
    }

    private void OnFieldChanged(object sender, EventArgs e)
    {
        var x = true;
    }

    private Task ValidatedSubmit()
    {
        OnValidSubmit.InvokeAsync(EditContext);
        message = $"clicked at {DateTime.Now.ToLongTimeString()}";
        return Task.CompletedTask;
    }

    public void Dispose()
      => EditContext.OnFieldChanged -= this.OnFieldChanged;
}

On a different subject, you should be able to lose the Javascript in toggling the card content by using similar techniques to what you’ve already used in the wait. Here’s my UIShow component that should point you in the right direction. A button/anchor to toggle a boolean property?

@if (this.Show)
{
    @this.ChildContent
}

@code {
    [Parameter] public bool Show { get; set; }
    [Parameter] public RenderFragment ChildContent { get; set; }
}

One of my Blazor mantra’s/commandments is “Thou shalt not write Javascript”!