【发布时间】:2022-08-18 21:51:53
【问题描述】:
语境
对于上下文,我的代码和问题是Blazor Server with EF Core Demo 的几乎相同的副本。主要区别在于我所有的 UI C# 都是使用代码隐藏模式在基类中编写的。
如演示应用程序所示,我的代码有一个过滤器服务,用于帮助保持组件之间的状态。在我的 ManageUsers.razor 组件上,我有一个子组件来切换联系人列表中姓名的显示方式 (NameToggle.razor)。如果Filters.Loading 为TRUE,则禁用该按钮以免尝试修改正在积极构建的列表。
我的问题
我的问题是,每当我使用NavigationManager.NavigateTo() 在各个组件页面(例如,ViewContact、EditContact)之间导航然后返回主组件页面时,主组件页面上的Filters.Loading 状态和其Filter.Loading 的状态子组件是不同的。
为了更清楚,我在下面提供了我的三个组件的代码 sn-ps。 ManageUsers.razor 组件是 NameToggle.razor 和 UserRow.razor 的父级。 UserRow 组件使用NavigationManager 服务导航到另一个可路由组件,然后该组件使用相同的服务导航回ManageUsers.razor。但是,当您以这种方式离开和返回时,NameToggle.razor 呈现的按钮将被禁用。将值打印到屏幕后,我可以看到即使 Filters.Loading 在 ManageUsers.razor 中为 FALSE,但在 NameToggle.razor 等子组件中为 TRUE。
到目前为止我所做的事情
我花了几个小时将我的代码与演示进行比较并阅读文档。对于我的一生,我无法弄清楚为什么会发生这种情况。该服务确实注册了一个 SCOPED 生命周期。我已经想到了一些关于为什么它可能不起作用的想法,但是在比较我的代码和阅读文档数小时之后,我又想出了一个短板。其中一些想法是,可能在注入的属性上使用null!; 会导致问题。根据文档,我将其更改为 default!;,但没有任何改变。
我认为可能 UserTableWrapper.razor 组件的级联参数可能为空,但我也测试了它,它永远不会为空。
进一步的背景
不确定这是否真的很重要,但我的解决方案是使用干净的架构。我的过滤器接口写在我的 Application.dll 中,它的实现写在 Infrastructure.dll 中。此外,我在同一层中有基础设施层的服务注册。
代码
Program.cs 片段
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();
基础设施服务.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;
}
}
管理用户.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 class=\"container-fluid users-table\">
<div class=\"row\">
<div class=\"col-2\">
<NameToggle />
<span> @Filters.Loading</span>
</div>
<div class=\"col-8\">
<TextFilter />
</div>
<div class=\"col-2\">
<button class=\"btn btn-primary\"
@onclick=\"NewUser\">➕ New User</button>
</div>
</div>
<div class=\"row\"> </div>
<div class=\"row\">
<div class=\"col-6\">
Page @Filters.PageHelper.Page of @Filters.PageHelper.PageCount: displaying @Filters.PageHelper.PageItems of @Filters.PageHelper.TotalItemCount users.
<a disabled=\"@(Filters.Loading || ! Filters.PageHelper.HasPrev)\"
class=\"btn btn-primary @IsDisabled(Filters.PageHelper.HasPrev)\"
href=\"@($\"ManageUsers/{Filters.PageHelper.PrevPage}\")\">
Previous
</a>
<a disabled=\"@(Filters.Loading || ! Filters.PageHelper.HasNext)\"
class=\"btn btn-primary @IsDisabled(Filters.PageHelper.HasNext)\"
href=\"@($\"ManageUsers/{Filters.PageHelper.NextPage}\")\">
Next
</a>
</div>
</div>
<div class=\"row\"> </div>
<div class=\"row user-header\">
<div class=\"col-1\"> </div>
<div class=\"col-2\"
@onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.Name))\">
<SortIndicator Column=\"@(ApplicationUserFilterColumns.Name)\" /> ???? Name
</div>
<div class=\"col-4\"
@onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.Email))\">
<SortIndicator Column=\"@(ApplicationUserFilterColumns.Email)\" /> ???? Email
</div>
<div class=\"col-2\"
@onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.Phone))\">
<SortIndicator Column=\"@(ApplicationUserFilterColumns.Phone)\" /> ???? Phone
</div>
<div class=\"col-3\"
@onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.HighSchool))\">
<SortIndicator Column=\"@(ApplicationUserFilterColumns.HighSchool)\" /> ???? High School
</div>
</div>
@if (Filters.Loading || Users is null)
{
<div class=\"row\">
<div class=\"col-12 alert alert-info\">
Loading...
</div>
</div>
}
@if (Users is not null && Users.Count == 0)
{
<div class=\"row\">
<div class=\"col-12 alert alert-warning\">
No users found.
</div>
</div>
}
@if (Users is not null)
{
@foreach (ApplicationUser user in Users)
{
<UserRow @key=user
CurrentUser=\"user\"
DeleteUser=\"DeleteApplicationUserAsync\" />
}
}
</div>
</UserTableWrapper>
ManageUsersBase.cs
using FLA.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace FLA.WebUI.Models.UserManagement;
/// <summary> Base for <see cref=\"ManageUsers\" />. </summary>
public class ManageUsersBase : ComponentBase
{
/// <summary> Keeps track of the last page loaded. </summary>
private int _lastPage = -1;
/// <summary> The <see cref=\"IApplicationUserFilters\" /> injected through dependency injection. </summary>
[ Inject ]
protected IApplicationUserFilters Filters { get; set; } = default!;
/// <summary> The <see cref=\"IDbContextFactory{ApplicationDbContext}\" /> injected through dependency injection. </summary>
[ Inject ]
protected IDbContextFactory<ApplicationDbContext> DbContextFactory { get; set; } = default!;
/// <summary> The <see cref=\"NavigationManager\" /> injected through dependency injection. </summary>
[ Inject ]
protected NavigationManager NavigationManager { get; set; } = default!;
/// <summary> The <see cref=\"ApplicationUserQueryAdapter\" /> injected through dependency injection. </summary>
[ Inject ]
protected ApplicationUserQueryAdapter QueryAdapter { get; set; } = default!;
/// <summary> The current page. </summary>
[ Parameter ]
public int Page { get => Filters.PageHelper.Page; set => Filters.PageHelper.Page = value; }
/// <summary> A wrapper for user table related activity (like delete). </summary>
protected UserTableWrapper Wrapper { get; set; } = new ();
/// <summary> Current page of <see cref=\"ApplicationUser\" />. </summary>
protected ICollection<ApplicationUser>? Users { get; private set; }
/// <summary> Helper method to set disabled on class for paging. </summary>
/// <param name=\"condition\"> <c> TRUE </c> when the element is active (and therefore should not be disabled). </param>
/// <returns> The string literal <b> \"disabled\" </b> or an empty string. </returns>
protected string IsDisabled(bool condition) => ! Filters.Loading && condition ? string.Empty : \"disabled\";
/// <summary> Main logic when getting started. </summary>
/// <param name=\"firstRender\"> <c> TRUE </c> for first-time render. </param>
protected override void OnAfterRender(bool firstRender)
{
// Ensure we\'re on the same, er, right page.
if (_lastPage < 1)
{
NavigationManager.NavigateTo(\"/ManageUsers/1\");
return;
}
// Normalize the page values.
if (Filters.PageHelper.PageCount > 0)
{
if (Page < 1)
{
NavigationManager.NavigateTo(\"/ManageUsers/1\");
return;
}
if (Page > Filters.PageHelper.PageCount)
{
NavigationManager.NavigateTo($\"/ManageUsers/{Filters.PageHelper.PageCount}\");
return;
}
}
base.OnAfterRender(firstRender);
}
/// <summary> Triggered for any paging update. </summary>
/// <returns> A <see cref=\"Task\" />. </returns>
protected override async Task OnParametersSetAsync()
{
// Make sure the page really changed.
if (Page != _lastPage)
{
_lastPage = Page;
await ReloadAsync();
}
await base.OnParametersSetAsync();
}
/// <summary>
/// Used to toggle the table sort. Will either switch to \"ascending\" on a new column, or toggle between
/// \"ascending\" and \"descending\" on a column with the sort already set.
/// </summary>
/// <param name=\"column\"> The <see cref=\"ApplicationUserFilterColumns\" /> to sort. </param>
/// <returns> A <see cref=\"Task\" />. </returns>
protected Task ToggleAsync(ApplicationUserFilterColumns column)
{
if (Filters.SortColumn == column)
{
Filters.SortAscending = ! Filters.SortAscending;
}
else
{
Filters.SortColumn = column;
}
return ReloadAsync();
}
/// <summary> Deletes a <see cref=\"ApplicationUser\" />. </summary>
/// <returns> A <see cref=\"Task\" />. </returns>
protected async Task DeleteApplicationUserAsync()
{
await using ApplicationDbContext context = await DbContextFactory.CreateDbContextAsync();
Filters.Loading = true;
if (context.Users is not null)
{
ApplicationUser? user = await context.Users.FirstOrDefaultAsync(u => u.Id == Wrapper.DeleteRequestId);
if (user is not null)
{
context.Users.Remove(user);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
/// <summary> Reloads the page on filters and paging controls. </summary>
/// <returns> A <see cref=\"Task\" />. </returns>
protected async Task ReloadAsync()
{
if (Filters.Loading || Page < 1)
{
return;
}
Filters.Loading = true;
Wrapper.DeleteRequestId = Guid.Empty;
Users = new List<ApplicationUser>();
await using ApplicationDbContext context = await DbContextFactory.CreateDbContextAsync();
IQueryable<ApplicationUser>? query = context.Users?.AsQueryable();
if (query is not null)
{
// Run the query to load the current page.
Users = await QueryAdapter.FetchAsync(query);
}
// Now we\'re done.
Filters.Loading = false;
}
/// <summary> Navigates to <see cref=\"AddUser\" />. </summary>
protected void NewUser()
{
NavigationManager.NavigateTo(\"/UserNew\");
}
}
名称Toggle.razor
@inherits NameToggleBase
<button class=\"btn btn-primary\"
disabled=\"@Filters.Loading\"
@onclick=\"ToggleAsync\">
@Label - @Filters.Loading
</button>
名称ToggleBase.cs
namespace FLA.WebUI.Models.UserManagement;
/// <summary> Base mode for <see cref=\"NameToggle\" />. </summary>
public class NameToggleBase : ComponentBase
{
/// <summary> The <see cref=\"IApplicationUserFilters\" /> injected through dependency injection. </summary>
[ Inject ]
protected IApplicationUserFilters Filters { get; set; } = default!;
/// <summary> Button text based on current state. </summary>
protected string Label => Filters.ShowFirstNameFirst ? \"Display LAST, FIRST\" : \"Display FIRST LAST\";
/// <summary> Reference to the <see cref=\"UserTableWrapper\" />. </summary>
[ CascadingParameter ]
public UserTableWrapper? Wrapper { get; set; }
/// <summary> Toggle name preference. </summary>
/// <returns> A <see cref=\"Task\" />. </returns>
protected Task ToggleAsync()
{
Filters.ShowFirstNameFirst = ! Filters.ShowFirstNameFirst;
return Wrapper is not null ? Wrapper.FilterChanged.InvokeAsync(this) : Task.CompletedTask;
}
}
用户行.razor
@inherits UserRowBase
@if (CurrentUser is not null)
{
<div class=\"row user-detail\">
<div class=\"col-1 text-right\">
<a title=\"Edit\"
href=\"UserEdit/@CurrentUser.Id\">
????
</a>
@if (CanDelete)
{
<span @onclick=\"DeleteRequestAsync\"
title=\"Delete\"
class=\"clickable red\">
❌
</span>
}
else
{
<span> </span>
}
</div>
<div class=\"col-2\">
<a href=\"@ViewLink\"
alt=\"View User Details\"
title=\"Details\">
@Name
</a>
</div>
@if (! DeleteConfirmation)
{
<div class=\"col-4\">@CurrentUser.Email</div>
<div class=\"col-2\">@CurrentUser.PhoneNumber</div>
<div class=\"col-3\">@CurrentUser.HighSchool</div>
}
else
{
<div class=\"col-9\">
<DeletePrompt Confirmation=\"ConfirmAsync\" />
</div>
}
</div>
}
UserRowBase.cs
namespace FLA.WebUI.Models.UserManagement;
/// <summary> Base model for <see cref=\"UserRow\" />. </summary>
public class UserRowBase : ComponentBase
{
/// <summary> The <see cref=\"ApplicationUser\" /> being rendered. </summary>
private ApplicationUser _currentUser = new ();
/// <summary> The <see cref=\"IApplicationUserFilters\" /> injected through dependency injection. </summary>
[ Inject ]
public IApplicationUserFilters Filters { get; set; } = default!;
/// <summary> The <see cref=\"ApplicationUser\" /> being rendered. </summary>
[ Parameter ]
public ApplicationUser? CurrentUser
{
get => _currentUser;
set
{
if (value is null || value.Equals(_currentUser))
{
return;
}
_currentUser = value;
DeleteConfirmation = false;
}
}
/// <summary> Event to raise when a user delete is requested. </summary>
[ Parameter ]
public EventCallback DeleteUser { get; set; }
/// <summary> Overall wrapper of functionality. </summary>
[ CascadingParameter ]
public UserTableWrapper? TableWrapper { get; set; }
/// <summary> Returns <c> TRUE </c> if conditions for delete are met. </summary>
protected bool CanDelete => ! DeleteConfirmation && (TableWrapper?.DeleteRequestId == Guid.Empty || TableWrapper?.DeleteRequestId == CurrentUser?.Id);
/// <summary> Navigate to the details page. </summary>
protected string ViewLink => $\"UserDetails/{CurrentUser?.Id}\";
/// <summary> Confirm the delete. </summary>
protected bool DeleteConfirmation { get; set; }
/// <summary> The user\'s correctly formatted name to be displayed. </summary>
protected string Name => Filters.ShowFirstNameFirst ? $\"{CurrentUser?.FirstName} {CurrentUser?.MiddleName} {CurrentUser?.LastName}\"
: $\"{CurrentUser?.LastName}, {CurrentUser?.FirstName} {CurrentUser?.MiddleName}\";
/// <summary> Called based on confirmation. </summary>
/// <param name=\"confirmed\"> <c> TRUE </c> when confirmed. </param>
/// <returns> A <see cref=\"Task\" />. </returns>
protected async Task ConfirmAsync(bool confirmed)
{
if (confirmed)
{
await DeleteAsync();
}
else
{
DeleteConfirmation = false;
if (TableWrapper is not null)
{
await TableWrapper.DeleteRequested.InvokeAsync(Guid.Empty);
}
}
}
/// <summary> Set delete to true. </summary>
protected async Task DeleteRequestAsync()
{
if (TableWrapper?.DeleteRequestId == Guid.Empty && CurrentUser is not null)
{
DeleteConfirmation = true;
await TableWrapper.DeleteRequested.InvokeAsync(CurrentUser.Id);
}
}
/// <summary> Deletes the <see cref=\"ApplicationUser\" />. </summary>
/// <returns> A <see cref=\"Task\" />. </returns>
private Task DeleteAsync() => DeleteUser.InvokeAsync(this);
}
-
\'code behind\' 模式不需要基类。反正最近两年没有。
-
也许当你导航和返回时,它们在不同的范围内,你可以尝试使用 AddSingleton 来注册。
-
@HenkHolterman 代码隐藏模式可以使用部分类或实现
ComponentBase的基类来实现。两者都同样可以接受。 -
@XinranShen 问题是状态应该只保留每个电路的跨组件。 Filters 服务旨在根据是否正在生成用户列表来动态呈现 UI。例如,如果它正在生成,则应该禁用切换按钮,因为我们不想修改仍在填充的集合。
-
@XinranShen 如果我将服务设为单例,那么任何访问/重新加载页面的用户都会导致交互性锁定所有用户。我没有包含过滤器服务的 sn-p,因为我的实现几乎是来自链接演示的复制粘贴。我刚刚更改了类名/命名空间。
标签: c# asp.net-core blazor blazor-server-side