Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 966
VirtualValentin Avatar asked Sep 10 '19 12:09

VirtualValentin


People also ask

What is the benefit of using Blazor app over using razor pages?

The biggest difference between Razor and Blazor is that Razor is a markup language with C#, while Blazor is the framework that lets you run C# code and use the Razor view engine in the browser.

What are the differences between razor pages and Blazor app?

Razor is a templating engine that combines C# with HTML to build dynamic web content. Blazor is a component-based, single-page app framework for building client-side web apps using . NET that works well with all modern browsers via WebAssembly for client-side Blazor.

Can you mix Blazor and razor?

Razor components can be integrated into Razor Pages and MVC apps in a hosted Blazor WebAssembly solution.

Is razor pages server side rendering?

ASP.NET Razor Pages (Server-side) Razor Pages (not to be confused with Razor Views, which are used in MVC) also does all its User Interface (UI) rendering on the server and returns HTML to the browser. At a high level, this looks just like MVC.


2 Answers

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>

OnInitializedAsync 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();            
        }
    }
}
like image 148
Panagiotis Kanavos Avatar answered Nov 08 '22 06:11

Panagiotis Kanavos


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:

enter image description here

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;
            }
        }
like image 5
dani herrera Avatar answered Nov 08 '22 04:11

dani herrera