65.9K
CodeProject 正在变化。 阅读更多。
Home

将服务与 Blazor 组件作用域匹配

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023 年 1 月 24 日

CPOL

3分钟阅读

viewsIcon

8937

一篇介绍如何创建与 Blazor 页面/表单具有相同范围的服务文章

引言

应用“单一职责原则”,您将拥有两个类

  1. WeatherForcastListForm - 一个显示 WeatherForecasts 列表的组件
  2. WeatherForecastListPresenter - 一个与数据管道交互以管理 WeatherForecasts 列表的对象

进一步采用这些相同的设计原则,您可以从 DI 将 WeatherForecastListPresenter 的一个实例注入到 WeatherForcastListForm 中,其生命周期范围与表单相同。

在 DotNetCore 框架中,这呈现了一个困境

  1. Scoped 范围太广:它适用于 SPA 会话的持续时间。如果仅在一个地方使用它,并且希望在 SPA 会话期间保持状态,则可以使用。

  2. Transient 范围太窄。

    2.1. 表单中的子组件无法使用 DI 访问 WeatherForecastListPresenter 的同一实例。

    2.2. 任何实现 IDisposableIAsyncDisposable 的类都不应被设置为 Transient。DI 服务容器维护对该实例的引用,以便在容器本身被 Disposed 时将其 Dispose。您在应用程序中创建了“内存泄漏”,因为每次访问该表单时都会构建 WeatherForecastListPresenter 的副本。它们仅在您关闭或刷新应用程序的会话时才被释放。

没有一个完美的解决方案。

前进 OwningComponentBase。它创建了自己的作用域服务容器,并在组件被释放时释放该容器。它具有与组件相同的范围。

不幸的是,有一个致命的缺陷:您的服务依赖的任何作用域服务也在同一容器中创建。毕竟,它只是一个 Scoped 容器。

考虑 AuthenticationService。SPA 范围容器中的实例是您的服务需要的实例,但它会获得一个没有用户信息的全新实例。对于任何通知服务、NavigationManager 和许多其他服务也是如此。

对于没有依赖项的服务来说是可以的,但是... 我们没有编写很多这样的服务!

解决难题

事实是,我们有一个 DotNetCore 服务容器配置,其设计围绕着旧的 MVC 服务器端模型。我们没有与组件的范围匹配的范围或容器。在 Microsoft 为我们提供一个之前,我们需要一个解决方法。

我的解决方案如下所述。

仓库

可以在这里找到本文的仓库和最新版本 Blazr.ComponentServiceProvider

演示计时器服务

由接口定义的简单 Timer 服务。

public interface ITimeService
{
    public string Message { get;}
    public event EventHandler? TimeChanged;
    public void UpdateTime();
}

具体的服务,带有调试代码以监视实例的正确创建和释放。

public class TimeService : ITimeService, IDisposable, IAsyncDisposable
{
    public readonly Guid InstanceId = Guid.NewGuid();
    private bool asyncdisposedValue;
    private bool disposedValue;

    public string Message { get; private set; } = DateTime.Now.ToLongTimeString();
    public event EventHandler? TimeChanged;

    public TimeService()
        => Debug.WriteLine($"TimeService - instance {InstanceId} created");

    public void UpdateTime()
    {
        Message = DateTime.Now.ToLongTimeString();
        TimeChanged?.Invoke(this, EventArgs.Empty);
    }

    public ValueTask DisposeAsync()
    {
        if (!asyncdisposedValue)
            Debug.WriteLine($"TimeService - instance {InstanceId} async disposed");

        asyncdisposedValue = true;
        return ValueTask.CompletedTask;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
                Debug.WriteLine($"TimeService - instance {InstanceId} disposed");

            disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

时间戳

一个用于显示和更新 TimeService 的组件。请注意捕获级联 ITimeService。它具有调试代码,用于显示其参数何时由父级呈现更新。

@namespace Blazr.UI
@implements IHandleAfterRender
@implements IHandleEvent

<div class="bg-light p-2 m-2">

    <h3>TimeStamp Component</h3>

    <div class="m-2">
        <button class="btn btn-primary" @onclick=Clicked>Update Timestamp</button>
    </div>

    <div>
        @(this.TimeService?.Message ?? "No message set.")
    </div>

    <div class="mt-2 bg-dark text-white">
        Parameters Set at at @this.ParametersChangedTimeStamp
    </div>

</div>

@code {
    [CascadingParameter] private ITimeService? TimeService { get; set; } = default!;

    private string ParametersChangedTimeStamp = "Not Set";

    protected override void OnInitialized()
    {
        if (this.TimeService is null)
            throw new NullReferenceException($"The {this.GetType().FullName} 
                  required a cascaded ITimeService");
    }

    protected override void OnParametersSet()
    {
        Debug.WriteLine("TimeStamp - Parameter Change");
        this.ParametersChangedTimeStamp = DateTime.Now.ToLongTimeString();
        base.OnParametersSet();
    }

    private void Clicked()
      => TimeService?.UpdateTime();

    // Saving CPU Cycles - No AfterRender Handling
    Task IHandleAfterRender.OnAfterRenderAsync()
        => Task.CompletedTask;

    // Saving CPU Cycles - shortcut the UI event handling code. One render per UI event
    async Task HandleEventAsync(EventCallbackWorkItem callback, object? arg)
    {
        await callback.InvokeAsync(arg);
        StateHasChanged();
    }
}

我们需要代码来创建我们的 服务。它需要创建一个 TService 的实例,该实例

  1. 可能具有也可能没有 DI 依赖项
  2. 可能实现也可能不实现 IDisposable 和/或 IAsyncDisposable
  3. 可能是服务容器中的接口或基类服务定义

ActivatorUtilities 是一个鲜为人知的实用程序,我们可以使用它来满足这些要求。

该解决方案将此功能实现为 IServiceContainer 的扩展方法。代码看似简单

public static class ServiceUtilities
{
    public static TService? GetComponentService<TService>
    (this IServiceProvider serviceProvider) where TService : class
    {
        var serviceType = serviceProvider.GetService<TService>()?.GetType();

        if (serviceType is null)
            return ActivatorUtilities.CreateInstance<TService>(serviceProvider);

        return ActivatorUtilities.CreateInstance
               (serviceProvider, serviceType) as TService;
    }
}

serviceTypeTService 的具体注册对象,或者为 null。如果 TService 是一个接口或基类,则 TServiceserviceType 将是不同的类型。

如果 ServiceTypenull,则服务容器中没有 TService 的定义:我们需要直接激活它。

如果服务类型是一种类型,我们将其激活为提供的具体类型。

在任何一种情况下,如果 CreateInstance 无法创建实例,我们可能会返回 null。我们让请求者处理 null 返回值。

有一个第二个 Try 包装方法。

public static bool TryGetComponentService<TService>
       (this IServiceProvider serviceProvider,[NotNullWhen(true)] 
        out TService? service) where TService : class
{
    service = serviceProvider.GetComponentService<TService>();
    return service != null;
}

CascadingComponentService

我们还需要一个组件包装器来封装服务的创建和释放。

主要代码在 SetParametersAsync 中实现。一切都需要在任何呈现事件发生之前发生。AsyncDisposable 被实现为正确释放 TService

@namespace Blazr.UI
@typeparam TService where TService: class
@implements IAsyncDisposable
@implements IHandleAfterRender
@implements IHandleEvent

<CascadingValue Value="this.ComponentService" IsFixed>
    @this.ChildContent
</CascadingValue>

@code {
    [Parameter] public RenderFragment? ChildContent { get; set; }

    [Inject] private IServiceProvider serviceProvider { get; set; } = default!;

    public TService? ComponentService { get; set; } = default!;

    private bool _firstRender = true;
    private IDisposable? _disposable;
    private IAsyncDisposable? _asyncDisposable;

    public override Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);

        if (_firstRender)
        {
            this.ComponentService = serviceProvider.GetComponentService<TService>();

            if (this.ComponentService is null)
                throw new NullReferenceException
                ($"No {typeof(TService).FullName} cound be created.");

            _disposable = this.ComponentService as IDisposable;
            _firstRender = false;
        }
        // Saving CPU Cycles - No Initialized/OnParametersSet run
        this.StateHasChanged();
        return Task.CompletedTask;
    }

    public async ValueTask DisposeAsync()
    {
        _disposable?.Dispose();

        if (this.ComponentService is IAsyncDisposable asyncDisposable)
            await asyncDisposable.DisposeAsync();
    }

    // Saving CPU Cycles - No AfterRender Handling
    Task IHandleAfterRender.OnAfterRenderAsync()
        => Task.CompletedTask;

    // Saving CPU Cycles - No automatic rendering
    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        => callback.InvokeAsync(arg);
}

演示页面

显示页面演示了使用 CascadingComponentService 以及如何捕获其服务以在其自己的 UI 中使用。

@page "/"
@implements IDisposable
@implements IHandleAfterRender
@implements IHandleEvent

<PageTitle>Index</PageTitle>

<CascadingComponentService TService="ITimeService" @ref=_service >
    <TimeStamp />
</CascadingComponentService>

<div class="alert alert-info">
    @(_service?.ComponentService?.Message ?? "No message set.")
</div>

@code {
    private CascadingComponentService<ITimeService>? _service;

    protected async override Task OnInitializedAsync()
    {
        await Task.Yield();
        // Yields to let the UI do a first render and ensure _service is assigned
        if (_service is not null && _service.ComponentService is not null)
            _service.ComponentService.TimeChanged += this.OnTimeChanged;
    }

    private void OnTimeChanged(object? sender, EventArgs e)
        => StateHasChanged();

    public void Dispose()
    {
        if (_service is not null && _service.ComponentService is not null)
            _service.ComponentService.TimeChanged -= this.OnTimeChanged;
    }

    // Saving CPU Cycles - No AfterRender Handling
    Task IHandleAfterRender.OnAfterRenderAsync()
        => Task.CompletedTask;
}

您可以解包级联并在根组件内自己完成。

总结

就这样,不是火箭科学,也不是很原创。 欢迎提供关于改进/我犯错的意见。

历史

  • 2023 年 1 月 24 日:初始版本
© . All rights reserved.