4

Many SO answers use await Task.Delay(1) to solve various async rendering issues in Blazor (wasm). I've even found a number of places in my own code where doing that "makes it work".

However it's always stated as matter of fact, without a thorough explanation, and I can't find this technique in the docs either.

Some questions:

  • Why use await Task.Delay(1) - when would I use this technique, what is the use case?
  • The docs do not discuss this (that I could find); is it because it's a hack, or is it a legitimate way to deal with the use case?
  • Any difference between Task.Delay(1) and Task.Yield()?
lonix
  • 14,255
  • 23
  • 85
  • 176
  • https://twitter.com/nick_craver/status/1021003343777452032 – aybe Nov 18 '22 at 02:00
  • Could you include a small list of questions with answers that suggest the `await Task.Delay(1)` as a solution to a problem? – Theodor Zoulias Nov 18 '22 at 02:01
  • @TheodorZoulias Ok that's a decent approach, I'll try compile a list of interesting places where this is used, maybe that will shed light on the technique. – lonix Nov 18 '22 at 02:16
  • Thanks @aybe that's an interesting thread. I think the issue for blazor(wasm) specifically is something to do with how the render queue works. – lonix Nov 18 '22 at 02:19
  • Search blazor source code for occurrences, maybe you'll find some hints: https://github.com/dotnet/aspnetcore. Press period key to open VS Code within web browser. – aybe Nov 18 '22 at 03:04
  • @TheodorZoulias, here's a link for you: https://stackoverflow.com/a/74489072/6152891 – enet Nov 18 '22 at 11:53
  • @enet do you mean [this](https://stackoverflow.com/questions/74424648/blazor-jsinterop-div-not-available-when-js-invoked) link? You could consider editing the question and adding this link inside, along with a couple of similar links. – Theodor Zoulias Nov 18 '22 at 22:23

3 Answers3

6

Why use await Task.Delay(1) - when would I use this technique, what is the use case?

It gives the UI a chance to update and redraw in the middle of your code.

The docs do not discuss this (that I could find); is it because it's a hack, or is it a legitimate way to deal with the use case?

It's a hack. But in the specific case of Blazor/WASM (no other Blazor or .NET runtime), there's not a lot of other options. If possible, I'd suggest splitting up your logic so your app isn't doing so much all at once; but sometimes that's not possible (or easy).

Any difference between Task.Delay(1) and Task.Yield()?

Depending on the browser details, yes.

On Windows UI apps, Task.Yield won't work for this because the UI message loop is a priority queue and "run this code" is highest priority. So (again, for Windows UI apps), this would queue the rest of the method and then return to the message loop, which would then continue executing the code instead of refreshing the UI (which are lower-priority messages).

For Blazor/WASM, whether Task.Yield would work or not depends on the browser implementation of its (implicit) message loop. If it has a similar priority queue, then you'd end up with the same problem as a Windows UI where Task.Yield does yield to the message loop but doesn't drain it.

On all platforms, Task.Delay(1) actually queues a timer callback, which is generally enough time to get some UI updates handled before the code continues running.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
2
  • Why use await Task.Delay(1)

To show intermediate results in an eventhandler.

  • The docs do not discuss this

It is usually not needed. But there's no argument against using it either. I figured out how to use it when solving a problem like this. And I got some negative feedback, see the comments under those posts.

  • Any difference between Task.Delay(1) and Task.Yield()?

Yes, Task.Yield() looks more sensible but I found it does not always work. See Stephen Cleary's answer here.

H H
  • 263,252
  • 30
  • 330
  • 514
  • 1
    *"..don't rely on an await call being really async and guarantee it with a `Task.Delay(1)`"* -- I don't think that the `Task.Delay(1)` guarantees anything. Theoretically it can have zero effect. The current thread might get suspended by the OS just after the `Task.Delay(1)` task has been created, and when it is resumed again a few milliseconds later it may find that the `Task` has already been completed. In that case the `await` will complete synchronously, and the code will behave like it encountered an `await Task.CompletedTask`. I don't know if this scenario is practically possible though. – Theodor Zoulias Nov 18 '22 at 08:24
  • 1
    So you are the one behind this!? It deserves a name... "Henk's Hack" maybe? It works (is necessary) for me in a number of places, but until now I didn't understand why. Thanks Henk! – lonix Nov 18 '22 at 09:03
  • 1
    Just found that the [docs](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/rendering?view=aspnetcore-6.0#an-asynchronous-handler-involves-multiple-asynchronous-phases-1) show something similar to this in *"An asynchronous handler involves multiple asynchronous phases"*. – lonix Nov 18 '22 at 12:39
0

Below is a link to a question answered by me and by Henk Holterman, in whose answer he uses await Task.Delay(1);

Run the code, and see the difference, as for instance, using await Task.Delay(1); results in re-rendering the component twice, etc.

Is the use of await Task.Delay(1); necessary? Absolutely not. This is a bad practice that not only results in a second re-rendering of a component, but it may lead to subtle issues with complex code. Blazor offers a list of life-cycle methods which you can capture and use to provide the wanted solutions. No hacking, please. This may prove very expensive in the long run. Create elegant code, not hacking...

UPDATE

The code snippet below describes a use case for the use of Task.Delay, demonstrating a page with a button element with the caption "Save," The requirement is to alter the caption's text to "Saving...," immediately after the user clicks the button, for the duration of saving an employee record in a data store. If you know of other use cases for Task.Delay, please let me know.

Index.razor

@page "/"

<div>
    <button class="btn btn-primary" 
                              @onclick="Save">@caption</button>
</div>


@code 
{
    
    private string caption = "Save";

    private async Task SaveEmployee()
    {
        // Simulate saving an employee's record in database...
        // I use System.Threading.Thread.Sleep instead of a loop 
        // with millions of iterations.
        System.Threading.Thread.Sleep(3000);
        // Retruns completed task
        await Task.CompletedTask;
    }

    private async Task Save()
    {
        caption = "Saving...";
        // Renders here automatically after calling Task.Delay()
            
        await Task.Delay(1000);

        await SaveEmployee();

        caption = "Save";
        // Renders here automatically when SaveEmployee() 
        //complete
    }
}

On the other hand, the following code snipppet demonstrates how to not use Task.Delay, and provides an elegant solution, which is certainly not an hack, and whose additional advantage is that it results in a single rendering of the component... Task.Delay involve a second rendering, mind you...

Note: The code below is an answer to this question

Component.razor

<div @ref="ReferenceToDiv" id="select-@Id" style="background-color: red; width:300px; height: 300px">

 </div>

@code
{
    ElementReference ReferenceToDiv;
    // As you can see, you should call the "adjustPosition" method from the 
    // `OnAfterRenderAsync` method to ensure that the div element has been 
    // rendered. DO Not Re-render In Vain. That is, do not use
    // await Task.Delay(1); to re-render your component

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (opened)
        {
          await jsModule.InvokeVoidAsync("adjustPosition", ReferenceToDiv);                
        } 
    }

    public void OnClick()
    {
        opened = !opened;
       
    }
}

test.js

export function adjustPosition(element) {
    // Should return 300px
    console.log($(element.style.width);   
}
enet
  • 41,195
  • 5
  • 76
  • 113
  • Thanks enet for your point of view. I've tried my best to avoid this technique, but on more than one occasion it was the only thing that worked for me - to let the renderer work through the jobs on the render queue. The next time I encounter this situation, I'll ping you here with a link to a separate question and maybe you can show me another way to do it. – lonix Nov 18 '22 at 12:04
  • 1
    If using `await Task.Delay(1);` is bad practice like you say, then show your `index.razor` using correct practice. – mxmissile Jan 13 '23 at 15:30