问题
I've been playing around with Blazor and the IAsyncEnumerable feature in C# 8.0. Is it possible to use IAsyncEnumerable and await within Razor Pages to progressively display markup with data?
Example service:
private static readonly string[] games = new[] { "Call of Duty", "Legend of Zelda", "Super Mario 64" };
public async IAsyncEnumerable<string> GetGames()
{
foreach (var game in games)
{
await Task.Delay(1000);
yield return game;
}
}
Example in razor page:
@await foreach(var game in GameService.GetGames())
{
<p>@game</p>
}
This gives error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
Any ideas if this is possible?
回答1:
Server-side Razor allows what you describe. This video describes the code in this Github repo that shows how to use IAsyncEnumerable by modifying the ForecastService
example in server-side Blazor template.
Modifying the service itself is easy, and actually results in cleaner code :
public async IAsyncEnumerable<WeatherForecast> GetForecastAsync(DateTime startDate)
{
var rng = new Random();
for(int i=0;i<5;i++)
{
await Task.Delay(200);
yield return new WeatherForecast
{
Date = startDate.AddDays(i),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
};
}
}
The Blazor page on the other hand is more complicated. It's not just that the loop would have to finish before the HTML was displayed, you can't use await foreach
in the page itself because it's not asynchronous. You can only define asynchronous methods in the code block.
What you can do, is enumerate the IAsyncEnumerable and notify the page to refresh itself after each change.
The rendering code itself doesn't need to change :
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
OnInitializeAsync
needs to call StateHasChanged()
after receiving each item :
List<WeatherForecast> forecasts;
protected override async Task OnInitializedAsync()
{
forecasts =new List<WeatherForecast>();
await foreach(var forecast in ForecastService.GetForecastAsync(DateTime.Now))
{
forecasts.Add(forecast);
this.StateHasChanged();
}
}
In the question's example, incoming games could be stored in a List, leaving the rendering code unchanged :
@foreach(var game in games)
{
<p>@game</p>
}
@code {
List<string> games;
protected override async Task OnInitializedAsync()
{
games =new List<games>();
await foreach(var game in GameService.GetGamesAsync())
{
games.Add(game);
this.StateHasChanged();
}
}
}
回答2:
You can't write await foreach
on .razor
template code. But, as workaround, you can write it at @code
section:
@if (@gamesUI == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Game</th>
</tr>
</thead>
<tbody>
@foreach (var game in gamesUI) // <--- workaround
{
<tr>
<td>@game</td>
</tr>
}
</tbody>
</table>
}
@code {
List<string> gamesUI; // <--- workaround
protected override async Task OnInitializedAsync()
{
gamesUI = new List<string>();
await foreach(var game in GService.GetgamesAsync() )
{
gamesUI.Add(game);
this.StateHasChanged();
}
}
}
Effect:
Yielding data:
private static readonly string[] games = new[] { "Call of Duty", "Legend of Zelda", "Super Mario 64", "Bag man" };
public async IAsyncEnumerable<string> GetgamesAsync()
{
var rng = new Random();
foreach (var game in games)
{
await Task.Delay(1000);
yield return game;
}
}
来源:https://stackoverflow.com/questions/57870709/can-you-use-iasyncenumerable-in-razor-pages-to-progressively-display-markup