3

First of all, I am very sorry for my English skills.

I am currently developing a web project through dotnet core 3.1 blazor.

As in the source below, I use IJSRuntime to call a Javascript function that takes a long time.

[Inject]
IJSRuntime JSRuntime { get; set; }
private async Task BtnDisplay()
    await JSRuntime.InvokeVoidAsync("RefreshWebJS", Data);
}

Javascript function takes a long time, so I added the source below to add a spinner.

private async Task BtnDisplay()
{
    showLoadingImg = true; // add
    await JSRuntime.InvokeVoidAsync("RefreshWebJS", Data);
    showLoadingImg = false;
}

A spinner is defined on the razor page as follows:

@if (showLoadingImg == true)
{
    <div style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; text-align: center;">
    <img src="/images/LoadingImg.gif" style="position: absolute; top: 50%; left: 50%;" />
    </div>
}

"StateHasChanged()" or "await InvokeAsync(() => StateHasChanged())" also doesn't work.

It works fine when I use Task instead of JSRuntime.

await Task.Delay(1000); 

Why doesn't it work when I use JSRuntime.InvokeVoidAsync?

Thank you and sorry for reading difficult English .

GT Kim
  • 31
  • 3
  • Have you tried putting the call to JSRuntime in its own task and using continueWith to showLoading = false? – Victor Procure Jun 16 '21 at 18:44
  • Yes, I tried 'continueWith' but it didn't work. The 'Task.delay(1)' method suggested by Mister Magoo works great – GT Kim Jun 16 '21 at 23:45

2 Answers2

5
private async Task BtnDisplay()
{
    showLoadingImg = true; // add
    await Task.Delay(1);
    await JSRuntime.InvokeVoidAsync("RefreshWebJS", Data);
    showLoadingImg = false;
}

Blazor automatically re-renders at the completion of the first Task that you await in an async task.

So, if that first Task is your long running process, it won't re-render until that finishes.

Adding await Task.Delay(1) is a simple way to allow the render before your long running process.

Further Reading : https://github.com/dotnet/aspnetcore/issues/22159

This is known feature of the way Blazor works and the creator of Blazor also recommends this approach in that thread (although he prefers await Task.Yield() - which I always forget to use!)

Mister Magoo
  • 7,452
  • 1
  • 20
  • 35
  • That works but sounds like a hacky solution. Alternatively, you can make the buttons event handler return immediately, causing the UI to rerender while starting the long-running js method in a new task that will resynchronize once finished. – loe Jun 16 '21 at 08:33
  • 1
    If you call understanding how Blazor works and making best use of it hacky, then sure. Do you have an example of your code as an answer? – Mister Magoo Jun 16 '21 at 08:36
  • no offense, your approach is perfectly valid, it was just my opinion that it seems like a workaround to use a dummy task in this way. And yes, I will supply an example later, when I have more time to do so. – loe Jun 16 '21 at 08:43
  • Now with the clarification in the GitHub issue and the mention of Task.Yield() I think I have to apologize to you for jumping to a fast conclusion. I did not know about this being a recommended way, thank you for sharing it! Something about this "fake" delay was just triggering me somehow because I saw such things in different contexts being used in weird ways. Task.Yield() seems very reasonable and will be more concise than the approach of spinning up a new Task to run the js invokation. – loe Jun 16 '21 at 09:12
  • The pity is that `Task.Yield()` does not always do the trick. – H H Jun 16 '21 at 11:33
1

As mentioned in a comment before, there is also an approach with a synchronous event handler, that starts the JS Interop in a new Task and returns immediately after that. You have to make sure to resynchronize this new task when it is finished, by using await InvokeAsync(StateHasChanged):

private bool ShowLoading = false;

private void HandleButtonClick()
{
    ShowLoading = true;
    Task.Run(async () => await CallLongRunningJs())
        .ContinueWith(t => { ShowLoading = false; InvokeAsync(StateHasChanged); });
}

private async Task CallLongRunningJs()
{
    await jsRuntime.InvokeVoidAsync("longRunningJsFunc", "data...");
}

It is more verbose than the approach with Task.Yield() presented by Mister Magoo, but I think it is good to mention this here for completeness.

loe
  • 83
  • 7
  • Task.Run() doesn't really do anything on WebAssembly, and is a disoptimization Server side. And ContinueWith() is outdated. The whole point of `async/await` here is to be able to write this in one clear method, like @MisterMagoo did. There is no benefit in "returns immediately". – H H Jun 16 '21 at 11:01
  • @HenkHolterman Thank you for this information. I was not considering Blazor WASM, I am working primarily with Blazor Server. I did not claim any additional benefit of returning immediately, other than this causes the UI to rerender before the task finishes. - On Blazor Server at least. Btw, where did you take this info from ContinueWith being outdated? Is there some official statement or did you mean it as some kind of general consensus? – loe Jun 16 '21 at 11:08
  • Wel "to rerender before the task finishes" is kind of a claim but I don't see the point. How do you rate the complexity (or readability) of this vs the other answer? I'll look for a link about ContinuWith but it's not official. Just common sense. – H H Jun 16 '21 at 11:32
  • @HenkHolterman This question is exactly about trying to get the loading indication to show up before the long-running task executes. That's the point. And yes, this solution might not have any other advantages compared to the other one. I'd also rate its readability lower, sure! It is however a solution, that works for blazor server and that I know and have used before in such cases like the one the question is stating. – loe Jun 16 '21 at 12:09