dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.36k stars 9.99k forks source link

Razor StreamRendering bug with server side project #53936

Closed jayveedee closed 7 months ago

jayveedee commented 8 months ago

Is there an existing issue for this?

Describe the bug

Hello, first of all I want to say that I could just not be understanding how StreamRendering works as I'm really new to Blazor!

I have two components called "Players" and "PlayersTable". Players is very simple, has only two divs, one with a header and a button and the other div with the component PlayersTable. There's also an empty ViewModel here, which does nothing right now PlayersTable on the other hand is a table with some information on the player. This component follows pretty much what the Weather sample component does with an if and else block to handle loading. The viewmodel here actually fetches some players from a remote database and updates the data within. (I've provided code examples in the "Steps to reproduce" block)

My issue is that this does not work as expected with the stream rendering while following the Weather component example. There is a small delay when opening the page which is what should happen if there were no stream rendering. I did however find a workaround to this weirdly enough and that was to just add a tiny delay to the initialization of the viewmodel by using Task.Delay(1);

Is this a bug, or am I doing something wrong? All the viewmodels/services being used here are injected like normal. Also tested with Edge and Firefox browsers.

Expected Behavior

When opening the page, the placeholder view should be seen while loading data.

Steps To Reproduce

@using Web.Components.Pages.TronFC.Players;

@page "/TronFC/Players"
@attribute [StreamRendering]

<PageTitle>Players</PageTitle>

<div>
    <h1>Players</h1>
    <button class="btn btn-primary" @onclick="() => ViewModel.AddPlayer()">Add</button>
</div>
<div>
    <PlayersTable />
</div>

@code {
    [Inject]
    public required PlayersViewModel ViewModel { get; set; }
}
@page "/TronFC/Players/Table"
@attribute [StreamRendering]

@if (ViewModel.IsLoading == true)
{
    <p><em>Loading...</em></p>
}
else if (ViewModel.DataItems.Count == 0)
{
    <p><em>No players found.</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Name</th>
                <th>Enrollment</th>
                <th>Win rate</th>
                <th>Attendance</th>
                <th>Injured</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in ViewModel.DataItems)
            {
                <tr>
                    <td>@item.Name</td>
                    <td></td> <!-- TODO -->
                    <td></td> <!-- TODO -->
                    <td></td> <!-- TODO -->
                    <td></td> <!-- TODO -->
                </tr>
            }
        </tbody>
    </table>
}

@code {
    [Inject]
    public required PlayersTableViewModel ViewModel { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(1); // No idea why this fixes the issue of stream rendering
        await ViewModel.Initialize();
    }
}
using Web.Entity;
using Web.Services;

namespace Web.Components.Pages.TronFC.Players;

public class PlayersTableViewModel
{
    //------------------------------------------------------------------------------//
    //                                  Properties                                  //
    //------------------------------------------------------------------------------//
    public bool IsLoading { get; set; }
    public List<PlayerTableDataItem> DataItems { get; set; }

    private readonly IPlayerService _playerService;

    //------------------------------------------------------------------------------//
    //                                 Constructors                                 //
    //------------------------------------------------------------------------------//
    public PlayersTableViewModel(IPlayerService playerService)
    {
        _playerService = playerService ?? throw new ArgumentNullException(nameof(playerService));

        DataItems = [];
        IsLoading = true;
    }

    //------------------------------------------------------------------------------//
    //                                Public Methods                                //
    //------------------------------------------------------------------------------//
    public async Task Initialize()
    {
        await LoadData();
    }

    public async Task EditPlayer(PlayerTableDataItem item)
    {
        await _playerService.UpdatePlayer(item.Data);
        await LoadData();
    }

    public async Task DeletePlayer(PlayerTableDataItem item)
    {
        await _playerService.DeletePlayer(item.Data.Id);
        await LoadData();
    }

    //------------------------------------------------------------------------------//
    //                               Private Methods                                //
    //------------------------------------------------------------------------------//
    private async Task LoadData()
    {
        IsLoading = true;

        List<Player> players = await _playerService.GetPlayers();
        List<PlayerTableDataItem> dataItems = players.Select(p => new PlayerTableDataItem(p)).ToList();

        DataItems = dataItems;
        IsLoading = false;
    }

    //------------------------------------------------------------------------------//
    //                                 Inner Classes                                //
    //------------------------------------------------------------------------------//
    public class PlayerTableDataItem
    {
        public string Name { get; set; }
        public Player Data { get; set; }

        public PlayerTableDataItem(Player player)
        {
            Name = player.GetFullName();
            Data = player;
        }
    }
}

Exceptions (if any)

No response

.NET Version

8.0

Anything else?

No response

hederson commented 8 months ago

I'm getting this error even in the Weather component example. I updated the Visual Studio to the latest version and also checked my dotnet sdk. I'm getting this issue when I compile in release mode and run the Weather example without any change.

im-ashar commented 8 months ago

I am also facing the similar kind of issue. Here is my Code

@page "/management/manage-amenities"
@attribute [StreamRendering]
@attribute [Authorize(Roles = $"{nameof(RolesTypes.Admin)},{nameof(RolesTypes.Staff)}")]
@inject IAmenityService AmenityService
<div class="container">
    <div class="row mt-3">
        <div class="col-sm-3">
            <EditForm Model="Model" FormName="AmenityForm" Enhance OnValidSubmit="SaveAmenityAsync">
                 <DataAnnotationsValidator />
                 @if (!string.IsNullOrWhiteSpace(_errorMessage))
                {
                    <Alert Color="AlertColor.Danger" Dismissable="true">
                        <Icon Name="IconName.ExclamationTriangleFill" class="me-2"></Icon>
                        @_errorMessage
                    </Alert>
                }
                <h3>Add Amenity</h3>
                <div class="form-floating mb-3">
                    <InputText @bind-Value="Model.Name" class="form-control" aria-required="true" placeholder="Amenity Name" />
                    <label for="@Model.Name">Amenity Name</label>
                    <ValidationMessage For="() => Model.Name" class="text-danger" />
                </div>
                <div class="form-floating mb-3">
                    <InputText @bind-Value="Model.Icon" class="form-control" aria-required="true" placeholder="Amenity Icon" />
                    <label for="@Model.Icon">Amenity Icon</label>
                    <ValidationMessage For="() => Model.Icon" class="text-danger" />
                </div>
                <Button Class="btn btn-success" Type="ButtonType.Submit" Disabled="@_isSaving">
                    @if (_isSaving)
                    {
                        <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
                        <span role="status">Saving...</span>
                    }
                    else
                    {
                        <span>Save Amenity</span>
                    }
                </Button>
            </EditForm>
        </div>
        <div class="col-sm-9">
            <h3 class="text-center">Available Amenities</h3>
            <div class="row">
                <div class="col-sm-12">
                    <table class="table table-bordered">
                        <thead>
                            <tr>
                                <th>Id</th>
                                <th>Icon</th>
                                <th>Name</th>
                                <th>Icon CSS Class</th>
                            </tr>
                        </thead>
                        <tbody>
                            @if (_amenities is not null)
                            {
                                @foreach (var amenity in _amenities)
                                {
                                    <tr>
                                        <td>@amenity.Id</td>
                                        <td class="text-primary"><i class="@amenity.Icon"></i></td>
                                        <td>@amenity.Name</td>
                                        <td>@amenity.Icon</td>
                                    </tr>
                                }
                            }
                            else if (_amenities == null || _amenities.Length == 0)
                            {
                                <tr>
                                    <td colspan="4">
                                        <h6 class="text-center">No Amenities Available.</h6>
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

@code {
    [SupplyParameterFromForm]
    private Amenity Model { get; set; } = new();
    private Amenity[]? _amenities;
    bool _isSaving = false;
    string? _errorMessage = string.Empty;

    protected override async Task OnInitializedAsync()
    {
        _amenities = [.. await AmenityService.GetAmenitiesAsync()];
    }
    private async Task SaveAmenityAsync()
    {
        _isSaving = true;
        var result = await AmenityService.SaveAmenityAsync(Model);
        if (result.IsSuccess)
        {
            _amenities = [.. _amenities, result.Data];
            Model = new();
        }
        else
        {
            _errorMessage = result.ErrorMessage;
        }
        _isSaving = false;
    }
}

If I go to the page from my navbar which is using Enhance navigation I get "No Amenities found" but if I refresh the page from browser or if go to the page by entering the url instead of using navbar I get my data inside the table perfectly.

Second thing which I try is that I removed the StreamRendering attribute and in my case that also works but i want to use StreamRendering.

javiercn commented 8 months ago

@jayveedee thanks for contacting us.

This seems to point out to an issue in your code, but we can't tell from the snippets because we can't run the code.

You don't need @attribute [StreamRendering] in @page "/TronFC/Players" because it's not doing any async work.

Within @page "/TronFC/Players/Table" it's likely that the code below is not actually asynchronous (hence why your Task.Delay trick works as it forces true asynchronous behavior, also Task.Yield is your friend :)).

That likely means for some reason your data is loading synchronously, you can validate this by modifying the code to do var Task = ViewModel.Initialize() and check for Task.IsCompleted, and you should see that it returns true.

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(1); // No idea why this fixes the issue of stream rendering
        await ViewModel.Initialize();
    }

Streaming rendering only makes sense if the component does actual async work (as in something returns a non-fulfilled task to the component) as otherwise, the component will only render once with the final results.

jayveedee commented 8 months ago

@javiercn hey I will try what you suggested later today and I'll get back to you. Thanks for the reply!

jayveedee commented 8 months ago

@javiercn Alright I played around with it a bit again and I couldn't see that the Task.IsCompleted would return true. The data loading is done via the PlayerService which uses a datacontext from EF Core to load the data. I have included that file's code as well now in this comment.

I don't see how it is synchronous but maybe you see something I do not :P ViewModel.Initialize() is called from the code behind and awaited LoadData() is called within the ViewModel and also awaited LoadData awaits the data from _playerService and then does some changes until returning PlayerService uses the built in function from the DataContext (DbContext) ToListAsync() and is also awaited

using Microsoft.EntityFrameworkCore;
using Web.Data;
using Web.Entity;

namespace Web.Services;

public class PlayerService : IPlayerService
{
    private readonly DataContext _dataContext;

    public PlayerService(DataContext dataContext)
    {
        _dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext));
    }

    public Task CreatePlayer(Player player)
    {
        throw new NotImplementedException();
    }

    public Task DeletePlayer(int id)
    {
        throw new NotImplementedException();
    }

    public Task<Player?> GetPlayer(int id)
    {
        throw new NotImplementedException();
    }

    public async Task<List<Player>> GetPlayers()
    {
        return await _dataContext.Players.ToListAsync();
    }

    public Task UpdatePlayer(Player player)
    {
        throw new NotImplementedException();
    }
}
using Microsoft.EntityFrameworkCore;
using Web.Entity;

namespace Web.Data;

public class DataContext : DbContext
{
    public DataContext(DbContextOptions<DataContext> options) : base(options)
    {

    }

    public DbSet<Player> Players { get; set; }

    public DbSet<Practice> Practices { get; set; }

    public DbSet<PlayerPractices> PracticesJunctions { get; set; }
}
Mizuwokiru commented 8 months ago

I guess I figured out what the problem is. I could reproduce it on IIS Express, but on Kestrel it works as expected.

Just create a Blazor WebApp project with such configuration:

Project creation configuration

Then try to run it on https and IIS Express configurations.

davidfowl commented 8 months ago

https://github.com/dotnet/aspnetcore/issues/52323

cmuller29 commented 7 months ago

Hello, I am having the same issue. I have narrowed down to the async EF Core functions. When used, it seems the stream rendering does not work...

When using this syntax testModels = await Task.Run(() => dbContext.OkModels.ToList()); stream rendering works but not with this syntax : testModels = await dbContext.KoModels.ToListAsync();

Here is a repo with a basic web app with two pages (one with the stream rendering working and a second where it does not work) to reproduce the issue : https://github.com/cmuller29/TestStreamRender

Can you help with this issue ?

Thank you