StateHasChanged не работает при вызове конечной точки API, которая передает ответ в потоковом режиме

Эта проблема, вероятно, возникает из-за того, что Blazor WASM является однопоточным, но я ищу хороший обходной путь, который не использует javascript.

Я вызываю API, который передает ответ, и я хочу показать ответ пользователю по мере его передачи.

Вот мой код (упрощенный):

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

Код работает правильно, но пользовательский интерфейс обновляется только после того, как все элементы будут возвращены из API, что занимает от 5 до 30 секунд.

В течение этого времени пользователь не видит никаких изменений, и когда вызов API завершен, он сразу показывает всю правильную информацию.

Я пробовал разные мелкие вещи, подобные этому, чтобы увидеть, есть ли простое решение. Однако это не имеет значения.

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

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

Я также делал подобные вещи внутри вызова «GetFromApi», но без разницы.

Я думаю, что если я смогу каким-то образом дать вызову API передышку, то у одного потока будет время обновить пользовательский интерфейс.

Я знаю, что могу звонить, используя простой Javascript, и я уже сделал это решение и могу подтвердить, что оно работает нормально. Этот вопрос о том, есть ли способ заставить это работать, не прибегая к javascript в моем приложении Blazor.

С нетерпением жду возможности узнать, есть ли у кого-нибудь хорошая идея, как заставить это работать. Спасибо за внимание!

ОБНОВЛЕНИЕ: Как и просили в комментариях, вот код из "GetFromApi" (упрощенный):

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;
    }
}

Новая информация доступна постепенно, но при вызове StateHasChanged пользовательский интерфейс не обновляется — только после завершения вызова API.

Результат список? если да, то какой список?

Daniel W. 11.04.2023 16:40

Что возвращает GetFromApi? Я сделал то же самое с ChatGPT, я преобразовал поток в IAsyncEnumerable<string> ...

Brian Parker 11.04.2023 16:49

Спасибо, Брайан. Я включил свой код из GetFromApi. Когда вы сделали это, это было из приложения Blazor WebAssembly?

Niels Brinch 12.04.2023 09:13
Для развертывания Сайтов с использованием Blazor, Angular и React с репозиторием на GitHub на Cloudflare
Для развертывания Сайтов с использованием Blazor, Angular и React с репозиторием на GitHub на Cloudflare
Как развернуть сайты с помощью Blazor, Angular и React с репозиторием на GitHub на Cloudflare.
1
3
54
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Как предполагает @BrianParker в комментариях, ваша проблема почти наверняка связана с GetFromApi. Если этот метод не дает результатов, то поток блокируется до его завершения, и отображение обновляется только в конце, когда визуализатор получает некоторое время потока.

Поскольку информации о GetFromApi нет, вот некоторый код, демонстрирующий добавочное обновление:

@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" };
}

Спасибо. Я обновил свой вопрос кодом из GetFromApi. Разница между моим кодом и вашим образцом заключается в том, что в моем коде есть открытое и работающее соединение API, дающее результаты.

Niels Brinch 12.04.2023 09:14

Это выглядит нормально. Добавьте запись в цикл while, чтобы увидеть, выполняется ли он более одного раза.

MrC aka Shaun Curtis 12.04.2023 09:43

Спасибо. Да, я добавил ведение журнала с помощью Console.WriteLine и мог видеть из консоли, что данные поступают постепенно, но пользовательский интерфейс не обновлялся. Добавив Task.Delay, он начал обновляться, а за счет уменьшения буфера сократил время первоначальной необъяснимой «икоты», которая, по-видимому, возникает, когда вызов только начинается.

Niels Brinch 12.04.2023 10:53
Ответ принят как подходящий

Task.Yield() это правильная идея, но обычно она не работает (недостаточно "передышки"). Вместо этого используйте Task.Delay(1).

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

Вы можете использовать счетчик и вызывать StateHasChanged+Delay только для каждого n-го элемента. Рендеринг их всех может занять много времени.

Да! Это помогло. После некоторого тестирования я обнаружил, что await Task.Delay(10) дает лучший результат. Кроме того, также необъяснимым образом помогает. В ходе моего расследования я обнаружил, что код, который был раньше, действительно работал, если бы он работал дольше, чем небольшой тестовый вызов продолжительностью 5 секунд. У него есть сбой в начале вызова, и он не восстанавливается до завершения вызова, поэтому, уменьшив буфер, он преодолевает этот сбой и в сочетании с await Task.Delay(10) теперь работает гладко.

Niels Brinch 12.04.2023 10:24

Хорошо, потребность в Delay(10) означает, что у вас в фоновом режиме выполняется тяжелая (ЦП) работа.

H H 12.04.2023 11:29

Другие вопросы по теме