您可以在 Razor 页面中使用 IAsyncEnumerable 来逐步显示标记吗?

Can you use IAsyncEnumerable in Razor pages to progressively display markup?

我一直在研究 Blazor 和 C# 8.0 中的 IAsyncEnumerable 功能。是否可以在 Razor Pages 中使用 IAsyncEnumerable 和 await 来逐步显示带有数据的标记?

示例服务:

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

razor 页面中的示例:

@await foreach(var game in GameService.GetGames())
{
  <p>@game</p>
}

这会产生错误 CS4033:'await' 运算符只能在异步方法中使用。考虑使用 'async' 修饰符标记此方法并将其 return 类型更改为 'Task'。

有什么想法可以吗?

服务器端 Razor 允许您描述的内容。 This video describes the code in this Github repo 展示了如何通过修改服务器端 Blazor 模板中的 ForecastService 示例来使用 IAsyncEnumerable。

修改服务本身很容易,实际上代码更简洁:

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

另一方面,Blazor 页面更复杂。不仅循环必须在显示 HTML 之前完成,您 不能 在页面本身中使用 await foreach 因为它不是异步的。您只能在代码块中定义异步方法。

您可以做的是枚举 IAsyncEnumerable 并通知页面在每次更改后自行刷新。

渲染代码本身不需要改变:

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

OnInitializedAsync 需要在收到每个项目后调用 StateHasChanged() :

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

在问题的例子中,传入的游戏可以存储在一个列表中,保持渲染代码不变:

@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();            
        }
    }
}

您不能在 .razor 模板代码上编写 await foreach。但是,作为解决方法,您可以将其写在 @code 部分:

@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();
        }
    }
}

效果:

屈服数据:

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