【问题标题】:Scoped Service not being shared as expectedScoped Service 未按预期共享
【发布时间】: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> &nbsp; @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\">&nbsp;</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\">&nbsp;</div>
    
            <div class=\"row user-header\">
    
                <div class=\"col-1\">&nbsp;</div>
    
                <div class=\"col-2\"
                     @onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.Name))\">
                    <SortIndicator Column=\"@(ApplicationUserFilterColumns.Name)\" /> &nbsp;???? Name
                </div>
    
                <div class=\"col-4\"
                     @onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.Email))\">
                    <SortIndicator Column=\"@(ApplicationUserFilterColumns.Email)\" /> &nbsp;???? Email
                </div>
    
                <div class=\"col-2\"
                     @onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.Phone))\">
                    <SortIndicator Column=\"@(ApplicationUserFilterColumns.Phone)\" /> &nbsp;???? Phone
                </div>
    
                <div class=\"col-3\"
                     @onclick=\"@(async () => await ToggleAsync(ApplicationUserFilterColumns.HighSchool))\">
                    <SortIndicator Column=\"@(ApplicationUserFilterColumns.HighSchool)\" /> &nbsp;???? 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>
    &nbsp;

名称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\">
                    &nbsp;????&nbsp;
                </a>
    
                @if (CanDelete)
                {
                    <span @onclick=\"DeleteRequestAsync\"
                          title=\"Delete\"
                          class=\"clickable red\">
                        ❌
                    </span>
                }
                else
                {
                    <span>&nbsp;</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


【解决方案1】:

拼凑起来有点困难,但第一步是确定您正在处理注册为IApplicationUserFiltersApplicationUserFilterControls 的同一实例。

将以下内容添加到ApplicationUserFilterControls

public class ApplicationUserFilterControls : IApplicationUserFilters 
{
  public Guid Uid = Guid.NewGuid();

  public ApplicationUserFilterControls(....)
  {
     Debug.Writeline($"New Instance of ApplicationUserFilterControls created with Uid: {Uid.ToString()}");
     ....
  }
}

观察输出窗口并插入断点以检查服务的 Uid。我很确定他们都会是一样的。

我认为您的问题在于误解了组件生命周期和渲染发生的时间。

当它被调用时:

protected async Task ReloadAsync()
{
...    
    Filters.Loading = true;
    
....    
    await using ApplicationDbContext context = await DbContextFactory.CreateDbContextAsync();
   // This yields so the UI gets rendered at this point
.....
    Filters.Loading = false;
}

下面的组件将在第一次渲染时渲染,即当为 true 时,但不会在最终渲染事件中重新渲染,因为没有更改任何参数。组件没有内在的机制来检测服务对象内的变化。

<button class="btn btn-primary"
        disabled="@Filters.Loading"
        @onclick="ToggleAsync">
    @Label - @Filters.Loading
</button>

您要么需要一个事件来加载服务中的状态(组件订阅),要么将状态作为参数传递,而不是直接从服务中读取。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-25
    • 1970-01-01
    • 2017-05-24
    • 1970-01-01
    • 1970-01-01
    • 2020-12-12
    • 2019-07-12
    相关资源
    最近更新 更多