Simon3
Simon3

Reputation: 117

StateHasChanged not execute on async call Blazor Server App

When the user press on a specific button i need to show an animation inside the button and disable it ( the method called need several seconds to accompish). This is my html:

 <div class="row">
            <div class="col-lg-4">
                @if (IsDownloading)
                {
                    <button class="btn btn-primary float-right search disabled" disabled><span class='fa-left fas fa-sync-alt spinning'></span>Downloading...</button>
                }
                else
                {
                    <button id="REC_PDF" class="btn btn-primary" @onclick="@(()=>getPDF())">Download Labels</button>
                }
                
            </div>
        </div>

and this is my code:

protected bool IsDownloading { get; set; }

public async Task getPDF()
{
    IsDownloading = true;
    StateHasChanged();

    await generate_pdf(labels);

    IsDownloading = false;
    StateHasChanged();
}

private async Task generate_pdf(List<Tuple<string, string, string, string>> filtered_label)
{
    loading = true;
    string PDF_PATH = "./PDF/" + CurrentValue;
    foreach (Tuple<string,string,string,string> label in filtered_label)
    {

        if (!Directory.Exists(PDF_PATH))
        {
            Directory.CreateDirectory(PDF_PATH);
        }
.......

The code is working but the rendering of the page called by StateHasChanged(); is executed 2 times when the method getPDF ( and generate_PDF consequentially) is fully executed.

I need page rendering on the frist call to show css loading animation.

( as suggested in other post i have used InvokeAsync(() => StateHasChanged()); but is not working)

Thanks.

Upvotes: 5

Views: 1924

Answers (3)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30450

The click event is handled like this in Blazor:

var task = InvokeAsync(EventMethod);
StateHasChanged();
if (!task.IsCompleted)
{
    await task;
    StateHasChanged();
}

Note StateHasChanged doesn't actually re-render the component, it just stacks a RenderFragment delegate on the Render queue.

All the code runs on the UI Synchronisation context, so the render event queued by calling StateHasChanged only gets executed either:

  • at the point the Event Method yields control
  • if no yield takes place (because there's only sync code inside the Task block) at the end.

I can't see any code that yields in your generate_pdf.

Try the following:

public async Task getPDF()
{
    IsDownloading = true;
    await Task.Yield();

    await generate_pdf(labels);

    IsDownloading = false;
}

Note: Directory.CreateDirectory is not an async method. Only FileStream has async methods.

Upvotes: 8

Henk Holterman
Henk Holterman

Reputation: 273824

The problem is that generate_pdf() is not asynchronous. Adding async is not enough. You need something asynchronous to enable a Render.

Since this is a UI problem I suggest to solve it in the top layer:

public async Task getPDF()
{
    IsDownloading = true;
    //StateHasChanged();  // not needed at start
    await task.Delay(1);  // allow the UI to update    

    try
    {
      await generate_pdf(labels);     
    }
    finally
    {
      IsDownloading = false;         
    }
    //StateHasChanged();  // not needed at the end
}

When generate_pdf() does not do any await itself you might as well make it a normal void method.

Upvotes: 1

Nicola Biada
Nicola Biada

Reputation: 2800

Try this modifications:

public async Task getPDF()
{
    if (IsDownloading) return;

    IsDownloading = true;
    StateHasChanged();

    await generate_pdf(labels);
}

then at the end of generate_pdf (better inside a try/finally block) add:

    IsDownloading = false;
    StateHasChanged();

Upvotes: 0

Related Questions