asp.netblazorasp.net-identity

No buttons (or other interactive elements) work when I use a data-bound object


I’m trying to display user identities and fetch a list of users, but buttons stop functioning if I use ApplicationUser (inherited from IdentityUser) retrieved via UserManager.

@attribute [Authorize] 
@using Microsoft.EntityFrameworkCore
@using Radzen.Blazor
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@using jeremy.server.Data

@rendermode InteractiveServer
@inject UserManager<ApplicationUser> UserManager

<RadzenCard>
    <RadzenDataGrid @ref="grid" AllowSorting="true" 
                Data="@users"
                AllowPaging="true" PageSize=20 Density="Density.Compact" ShowPagingSummary="true"
                TItem="ApplicationUser" SelectionMode="DataGridSelectionMode.Single"
                AllowFiltering="true"
                AllowColumnResize="true"
                AllowRowSelectOnRowClick="true">
        <Columns>
            <RadzenDataGridColumn TItem="ApplicationUser" Property="UserName" Title="UserName" Width="300px" />
            <RadzenDataGridColumn TItem="ApplicationUser" Property="Email" Title="Email" Width="300px" />
            <RadzenDataGridColumn Title="Actions" Width="150px">
                <Template Context="user">
                    <RadzenButton Icon="edit" Click="@(() => EditUser(user.Id))" />
                    <RadzenButton Icon="delete" Click="@(() => DeleteUser(user.Id))" 
                                 ButtonStyle="ButtonStyle.Danger" />
                </Template>
            </RadzenDataGridColumn>
        </Columns>
    </RadzenDataGrid>

    <RadzenButton Text="Add User" Click="@AddUser" 
                 ButtonStyle="ButtonStyle.Primary" Icon="add_circle" />
</RadzenCard>

@code {
    private RadzenDataGrid<ApplicationUser> grid;
    private IEnumerable<ApplicationUser> users; 

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        users = UserManager.Users.ToListAsync().Result;
    }

    protected async Task AddUser() => this.EditUser(String.Empty);

    protected async Task EditUser(string userId)
    {
        var user = new ApplicationUser { UserName = "TestName", Email = "test@test.com" };
        var result = await UserManager.CreateAsync(user, "12345");

        this.grid.Reload();
    }

    protected async Task DeleteUser(string userId)
    {
        var user = users.FirstOrDefault(u => u.Id == userId);
        if (user != null)
        {
            var result = await UserManager.DeleteAsync(user);
        }
    }
}

And the browser console does not display errors.

However, if I cast UserManager.Users to my custom UserModel type, the issue disappears.


public class UserModel
{
    public string Id { get; set; }
    public string UserName { get; set; }
    public string? Email { get; set; }
}
   protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        users = UserManager.Users.Select(x => new UserModel()
        {
            Id = x.Id,
            UserName = x.UserName ?? string.Empty, 
            Email = x.Email
        }); 

    }

Why? Blazor is supposed to work directly with data.


Solution

  • The buttons are working but the modified data isn't reloaded. The real difference in the second snippet is that instead of loading the data once with ToListAsync, a query is used that will rerun every time.

    The original code (with the blocking bug fixed) only loads users once :

    private IEnumerable<ApplicationUser> users;
    
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        users = await UserManager.Users.ToListAsync();
    }
    

    The button handlers do not reload users. They tell the grid to reload using whatever is stored in the field :

    protected async Task EditUser(string userId)
    {
        var user = new ApplicationUser { UserName = "TestName", Email = "test@test.com" };
        var result = await UserManager.CreateAsync(user, "12345");
    
        this.grid.Reload();
    }
    

    Since the data is unchanged, no differences are shown. The code should change to

    protected async Task EditUser(string userId)
    {
        var user = new ApplicationUser { UserName = "TestName", Email = "test@test.com" };
        var result = await UserManager.CreateAsync(user, "12345");
        users = await UserManager.Users.ToListAsync();
    
        this.grid.Reload();
    }
    

    If not

    protected async Task EditUser(string userId)
    {
        var user = new ApplicationUser { UserName = "TestName", Email = "test@test.com" };
        var result = await UserManager.CreateAsync(user, "12345");
        users = await UserManager.Users.ToListAsync();
    }
    

    Redrawing happens automatically when Blazor detects changes. Blazor will re-render only the components whose data has changed. If that doesn't happen, StateHasChanged() should be called. In general, responses to UI actions trigger StateHasChanged automatically.

    Why the second snippet works

    In the second case, the code stores an IQueryable<UserModel> query to users, not data. The data won't be loaded until Blazor starts rendering. And... unless RadzenDataGrid loads asynchronously, it will block the Blazor thread until the data is loaded.

        users = UserManager.Users.Select(x => new UserModel()
        {
            Id = x.Id,
            UserName = x.UserName ?? string.Empty, 
            Email = x.Email
        }); 
    

    Every time an IQueryable gets iterated, the query executes again. Every time the grid gets re-rendered, the query will run again, incurring the same cost.

    It doesn't matter what type is used. This code would have the same effect, as UserManager.Users is already an IQueryable<>

    users = UserManager.Users;