1

I have a Blazor component that needs to pass a boolean to its child component to disable the form submit button when submitting.

<EditForm Model="Model" OnValidSubmit="SubmitSearch">
   <div class="panel-body">
      <ChildComponent IsSubmitting="@IsSubmitting"/>
   </div>
</EditForm>

My child component is just a series of inputs with a submit button

<div>

   // inputs etc.

   <span class="pull-left">
      <button class="btn btn-success" type="submit" disabled="@IsSubmitting">
          Submit Search
      </button>
   </span>

</div>

@code {
   [Parameter]
   public bool IsSubmitting { get; set; }
}

and my submit method sets IsSubmitting like so

public async void SubmitSearch()
{
   IsSubmitting = true;

   var result = await Service.GetStuff();

   // do stuff with result

   IsSubmitting = false;
}

I notice that the button never disables. Am I missing some sort of lifecycle hook to trigger a re-render when the parameter updates?

Any help appreciated.

mb1231
  • 308
  • 3
  • 16
  • 1
    Can you show us how `IsSubmitting` gets updated? – Mister Magoo Feb 07 '20 at 12:18
  • added as another comment - can't edit my own question - thanks! edit: never mind, managed to edit in the end – mb1231 Feb 07 '20 at 12:26
  • 1
    You shouldn't need to, but can you try adding a call to `StateHasChanged()` before the call to `await Service.GetStuff()`? The framework should be doing that for you, but let's rule that out first. – Mister Magoo Feb 07 '20 at 12:33
  • Thanks - tried that just now and didn't work :) – mb1231 Feb 07 '20 at 12:37
  • Try making SubmitSearch an async Task rather than void. – Mister Magoo Feb 07 '20 at 12:40
  • @MisterMagoo that didn't work either sadly! – mb1231 Feb 07 '20 at 12:44
  • Can you post the html code produce by ` – agua from mars Feb 07 '20 at 13:10
  • That's what I want, but the HTML rendered does not include the `disabled` attribute. It is simply ` – mb1231 Feb 07 '20 at 13:16
  • No, it's because disabled attribute does't accept bool, you can write, just `disabled` or `disabled="disabled"` – agua from mars Feb 07 '20 at 13:23
  • 1
    There are many other areas in my app where I set `disabled="@booleanCondition"` and it works fine. I very much suspect this is a problem with the parent not updating its child component. If I move the button back up to the parent component, the disabling works. – mb1231 Feb 07 '20 at 13:32
  • 1
    Yes, the issue is in the `SubmitSearch` method – agua from mars Feb 07 '20 at 13:35

4 Answers4

5

I found a solution in the last answer here: How to disable/hide a button as soon as clicked in Blazor?

Even though SubmitSearch was an async method, its call to the back-end service was largely synchronous.

As @HenkHolterman said in that answer, GUI is not updated unless the method calls are purely async.

So I created an async Task Dispatcher like so:

    async Task DispatchSubmit()
    {
        IsSubmitting = true;

        await Task.Delay(1);  // allow the GUI to catch up
        await SubmitSearch();

        IsSubmitting = false;
    }

that fixed everything!

mb1231
  • 308
  • 3
  • 16
  • 1
    @enet I will dig further into this tomorrow and update but this didn't work for me without the `Task.Delay(1)`, suggesting to me the issue is with the call not being truly async – mb1231 Feb 10 '20 at 13:43
  • What does it mean "not being truly async?" There are clear rules that make a method async or not , and they are not esoteric or religious . There is no reason to speak in terms of miracles. I've created a sample based on your question and it works perfectly well, without tricks and juggling. Let me ask you this: what does 'await Task.Delay(1)' do that solve the issue ? – enet Feb 10 '20 at 17:14
2

The only issue with your code is that you use void instead of Task in the SubmitSearch method, which should be like this:

public async Task SubmitSearch()
    {
        IsSubmitting = true;

        // await Task.Delay(3000);
       // var result = await Service.GetStuff();

        // do stuff with result

        IsSubmitting = false;
    }

The above code perfectly works and do the job...

enet
  • 41,195
  • 5
  • 76
  • 113
1

You just cannot do that :

public async void SubmitSearch() // async method MUST return a Task
{
   IsSubmitting = true;

   var result = await Service.GetStuff();

   // do stuff with result

   IsSubmitting = false;

    // The component is refreshed after the method call
}

But this should work

public void SubmitSearch()
{
   IsSubmitting = true;

   Service.GetStuff()
       .ContinueWith(t => 
       {
          IsSubmitting = false;
          InvokeAsync(StateHasChanged());

          if (t.Exception != null)
          {
               throw t.Exception;
          }

          var result = t.Result;
          // do stuff with result
       });

}
agua from mars
  • 16,428
  • 4
  • 61
  • 70
  • Thanks - this seems to do half the job. The button now becomes disabled after submitting. But now it never becomes enabled again! – mb1231 Feb 07 '20 at 14:59
  • Yeah, it looks like I'm missing an `await` further down in various methods following the query to the server. The error is an Entity Framework error about not being able to operate on two versions of the `DbContext` – mb1231 Feb 07 '20 at 16:09
  • I updated the answer to notify StateHasChanged before processing the service response. This should reset the disabled attribute in any case – agua from mars Feb 07 '20 at 16:11
1

Here's a code sample that disables the submit button as long as the the model is not valid.

using System.ComponentModel.DataAnnotations

<h1>My articles</h1>

<p>Leave me a comment</p>

<EditForm EditContext="@EditContext">
      <DataAnnotationsValidator />

<div class="form-group">
    <label for="name">Name: </label>
    <InputText Id="name" Class="form-control" @bind-Value="@Model.Name"> 
</InputText>
         <ValidationMessage For="@(() => Model.Name)" /> 

</div>
<div class="form-group">
    <label for="body">Text: </label>
    <InputTextArea Id="body" Class="form-control" @bind-Value="@Model.Text"> 
</InputTextArea>
    <ValidationMessage For="@(() => Model.Text)" />
</div>

</EditForm>
<p>
   <button>Action 1</button>
   <button>Action 2</button>
    <button disabled="@Disabled" @onclick="Save">Save</button>
</p>

@code
{
  private EditContext EditContext;
  private Comment Model = new Comment();

  protected string Disabled { get; set; } = "disabled";


private async Task Save ()
{
    await Task.Delay(3000);
    Console.WriteLine("Saving...");
    Console.WriteLine(Model.Name);
    Console.WriteLine(Model.Text);
}

protected override void OnInitialized()
{
    EditContext = new EditContext(Model);
    EditContext.OnFieldChanged += EditContext_OnFieldChanged;

    base.OnInitialized();
}

private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs 
   e)
{
    Console.WriteLine(e.FieldIdentifier.FieldName);

    SetSaveDisabledStatus(e);

}

private void SetSaveDisabledStatus(FieldChangedEventArgs e)
{
    if (EditContext.Validate())
    {
        Disabled = null;
    }
    else
    {
       Disabled = "disabled";
    }
}

public class Comment
{
    [Required]
    [MaxLength(10)]
    public string Name { get; set; }

    [Required]
    public string Text { get; set; }
} 

}

enet
  • 41,195
  • 5
  • 76
  • 113