1

This issue is probably happening because Blazor WASM is single-threaded but I am looking for a good workaround that does not involve javascript.

I am calling an API which streams a response and I want to show the response to the user as it streams.

Here is my code (simplified):

await foreach (var item in GetFromApi(path))
{
    result += item;
    StateHasChanged();
}

The code works correctly but the UI is only updated once all the items are returned from the API which takes 5 to 30 seconds.

During this time, the user sees no changes and when the API call is completed, it shows all the correct information all at once.

I have tried different small things like this to see if there was an easy fix. However, it makes no difference.

await foreach (var item in GetFromApi(path))
{
    result += item;

    await Task.Yield();
    await InvokeAsync(StateHasChanged);
    StateHasChanged();
    await Task.Yield();
}

I have also done similar things inside the "GetFromApi" call but with no difference.

I am thinking if I can somehow give the API call a breather, then the single thread has time to update the UI.

I am aware that I can call using plain Javascript and I have already made that solution and can confirm it works fine. This question is about whether there is a way to make this work without resorting to javascript in my Blazor app.

Looking forward to see if anyone has a good idea how to get this to work. Thank you for your attention!

UPDATE: As requested in comments, here is the code from "GetFromApi" (simplified):

public async IAsyncEnumerable<string> GetFromApi(string path, int bufferSize = 5000)
{
    using var request = new HttpRequestMessage(HttpMethod.Get, path);
    request.SetBrowserResponseStreamingEnabled(true);
    using var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
    using var stream = await response.Content.ReadAsStreamAsync(); // get stream
    using var reader = new StreamReader(stream);

    var buffer = new char[bufferSize];
    int bytesRead;

    while ((bytesRead = await reader.ReadBlockAsync(buffer, 0, buffer.Length)) > 0)
    {
        var text = new string(buffer, 0, bytesRead);
        yield return text;
    }
}

It has the new information available gradually, but when StateHasChanged is called, the UI is not updated - only after the API call is finished.

H H
  • 263,252
  • 30
  • 330
  • 514
Niels Brinch
  • 3,033
  • 9
  • 48
  • 75

2 Answers2

2

As @BrianParker suggests in the comments, your problem almost certainly resides in GetFromApi. If that method doesn't yield, then the thread is blocked until it completes, and the display only updates at the end when the Renderer gets some thread time.

As there's no information on GetFromApi, here's some code that demonstrates incremental updating:

@page "/"
@using System.Text

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.
<div class="m-2">
    <button class="btn btn-primary" @onclick=GetData>Get Data</button>
</div>

<div class="bg-dark text-white m-2 p-2">
    <pre>
        @_displayData
    </pre>
</div>

@code {
    private StringBuilder _displayData = new();
    private async Task GetData()
    {
        _displayData = new();
        await foreach (var city in GetFromApi())
        {
            _displayData.AppendLine(city);
            this.StateHasChanged();
        }
    }

    private async IAsyncEnumerable<string> GetFromApi()
    {
        foreach (var city in _cities)
        {
            await Task.Delay(2000);
            yield return city;
        }
    }

    private List<string> _cities = new() { "London", "Paris", "Lisbon" };
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thank you. I have updated my question with the code from GetFromApi. The difference between my code and your sample is that in my code there is an open and running API connection while yielding results. – Niels Brinch Apr 12 '23 at 07:14
  • 1
    It looks ok. Add some logging to the `while` loop to see if it runs more than once. – MrC aka Shaun Curtis Apr 12 '23 at 07:43
  • Thank you. Yes, I added logging with Console.WriteLine and could see from the console it was getting data gradually but the UI was not being updated. By adding Task.Delay it started updating and by lowering the buffer, it shortened the time of the initial inexplicable 'hiccup' that it apparently has when the call is just starting. – Niels Brinch Apr 12 '23 at 08:53
1

Task.Yield() is the right idea but it usually doesn't work (not enough of a 'breather'). Use Task.Delay(1) instead.

await foreach (var item in GetFromApi(path))
{
    result += item;
    StateHasChanged();
    await Task.Delay(1);
}

You may want to use a counter and only call StateHasChanged+Delay every n-th item. Rendering them all could take a lot of time.

H H
  • 263,252
  • 30
  • 330
  • 514
  • 1
    Yes! This did the trick. After some testing I found that `await Task.Delay(10)` gave a better result. Additionally, also inexplicably helps. During my investigation I found that the code before actually worked if only I had it running for longer than the small test call of 5 seconds. It sort of has a hiccup in the beginning of the call and doesn't recover from that before the call is over, so by reducing the buffer, it gets over that hiccup and combined with `await Task.Delay(10)` it runs smoothly now. – Niels Brinch Apr 12 '23 at 08:24
  • OK, the need for Delay(10) means you have some heavy (CPU) work going on in the background. – H H Apr 12 '23 at 09:29