Context
For context, my code and problem in question is an almost identical copy of the Blazor Server with EF Core Demo. The primary difference is that all my UI C# is written in base classes using the code-behind pattern.
As is seen in the demo app, my code has a filters service used to help keep state between components. On my ManageUsers.razor component, I have a child component to toggle how the names in the contacts list are displayed (NameToggle.razor). If Filters.Loading
is TRUE, then the button is disabled so as not to attempt to modify a list that's actively being built.
My Issue
My issue is that whenever I use NavigationManager.NavigateTo()
to navigate between the various component pages (e.g., ViewContact, EditContact) and then back to the main component page, the state of Filters.Loading
on the main component page and Filter.Loading
of its child components are different.
To be clearer, I've provided code snippets of three of my components below. The ManageUsers.razor components is the parent of NameToggle.razor and UserRow.razor. The UserRow component uses the NavigationManager
service to navigate to another routable component, which then uses that same service to navigate back to ManageUsers.razor. However, when you navigate away and back in this manner, the button rendered by NameToggle.razor is disabled. After printing the values to the screen, I can see that even though Filters.Loading
is FALSE in the ManageUsers.razor, it is TRUE in the child components like NameToggle.razor.
Things I've done so far
I have spent hours comparing my code to the demo and reading through documentation. For the life of me, I cannot figure out why this is happening. The service is indeed registered with a SCOPED lifetime. I've thought of some ideas as to why it might not be working, but again, after hours of comparing my code and reading the docs, I've come up short. Some of those ideas were that maybe using null!;
on the injected properties was causing an issue. Per the documentation, I changed it to being default!;
, but nothing changed.
I thought that maybe the cascading parameter of the UserTableWrapper.razor component might have been null, but I also tested that and it's never null.
Further context
Not sure if this really matters, but my solution is using clean architecture. I have my Filters interface written in my Application.dll and its implementation written in Infrastructure.dll. Additionally, I have the service registrations for the infrastructure layer in the same layer.
Code
Program.cs Snippet
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Configure and add application logger as it should be for run-time.
builder.Host.AddSerilog();
// Add services to the container.
builder.Services.AddApplicationServices();
builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddWebUIServices();
WebApplication app = builder.Build();
InfrastructureServices.cs
using FLA.Application.Common.Interfaces;
using FLA.Domain.Entities.Identity;
using FLA.Infrastructure.Common.Filtering;
using FLA.Infrastructure.Persistence;
using FLA.Infrastructure.Persistence.Seeding;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace FLA.Infrastructure.DependencyInjection;
/// <summary> Extensions for adding and configuring services from the Infrastructure project. </summary>
public static class InfrastructureServices
{
/// <summary> Adds and configures services from the infrastructure layer to the application's services container. </summary>
/// <param name="services"> <see cref="IServiceCollection" />: the application's services container. </param>
/// <param name="configuration"> <see cref="IConfiguration" />: the application's configuration. </param>
/// <returns> The <see cref="IServiceCollection" /> with the various services added and configured. </returns>
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
// Register db context factory and configure options.
string connectionString = configuration.GetConnectionString(ApplicationDbContext.ConnectionStringKey);
MySqlServerVersion serverVersion = new (ServerVersion.AutoDetect(connectionString));
services.AddDbContextFactory<ApplicationDbContext>(options => options.UseMySql(connectionString, serverVersion,
mySqlOptions => mySqlOptions.MigrationsAssembly("FLA.Infrastructure")));
services.AddScoped<ApplicationDbContextInitializer>();
// Pager.
services.AddScoped<IPageHelper, PageHelper>();
// Filters.
services.AddScoped<IApplicationUserFilters, ApplicationUserFilterControls>();
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication();
services.AddAuthorization();
services.Configure<IdentityOptions>(options =>
{
// Sign In settings.
options.SignIn.RequireConfirmedAccount = true;
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 8;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(20);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._@ ";
options.User.RequireUniqueEmail = true;
});
return services;
}
}
ManageUsers.razor
@page "/ManageUsers"
@page "/ManageUsers/{Page:int}"
@inherits ManageUsersBase
@attribute [ Authorize(Roles = "Administrator") ]
<PageTitle>Manage Users</PageTitle>
<h1>Manage Users</h1>
<UserTableWrapper @ref="Wrapper"
FilterChanged="ReloadAsync"
DeleteRequested="id => Wrapper.DeleteRequestId = id">
<div >
<div >
<div >
<NameToggle />
<span> @Filters.Loading</span>
</div>
<div >
<TextFilter />
</div>
<div >
<button
@onclick="NewUser">➕ New User</button>
</div>
</div>
<div > </div>
<div >
<div >
Page @Filters.PageHelper.Page of @Filters.PageHelper.PageCount: displaying @Filters.PageHelper.PageItems of @Filters.PageHelper.TotalItemCount users.
<a disabled="@(Filters.Loading || ! Filters.PageHelper.HasPrev)"
href="@($"ManageUsers/{Filters.PageHelper.PrevPage}")">
Previous
</a>
<a disabled="@(Filters.Loading || ! Filters.PageHelper.HasNext)"
href="@($"ManageUsers/{Filters.PageHelper.NextPage}")">
Next
</a>
</div>
</div>
<div > </div>
<div >
<div > </div>
<div
@onclick="@(async () => await ToggleAsync(ApplicationUserFilterColumns.Name))">
<SortIndicator Column="@(ApplicationUserFilterColumns.Name)" />