Эта проблема, вероятно, возникает из-за того, что 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.
Что возвращает GetFromApi
? Я сделал то же самое с ChatGPT, я преобразовал поток в IAsyncEnumerable<string>
...
Спасибо, Брайан. Я включил свой код из GetFromApi. Когда вы сделали это, это было из приложения Blazor WebAssembly?
Как предполагает @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, дающее результаты.
Это выглядит нормально. Добавьте запись в цикл while
, чтобы увидеть, выполняется ли он более одного раза.
Спасибо. Да, я добавил ведение журнала с помощью Console.WriteLine и мог видеть из консоли, что данные поступают постепенно, но пользовательский интерфейс не обновлялся. Добавив Task.Delay, он начал обновляться, а за счет уменьшения буфера сократил время первоначальной необъяснимой «икоты», которая, по-видимому, возникает, когда вызов только начинается.
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)
теперь работает гладко.
Хорошо, потребность в Delay(10) означает, что у вас в фоновом режиме выполняется тяжелая (ЦП) работа.
Результат список? если да, то какой список?