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





5.00/5 (1投票)
一篇介绍如何创建与 Blazor 页面/表单具有相同范围的服务文章
引言
应用“单一职责原则”,您将拥有两个类
WeatherForcastListForm
- 一个显示WeatherForecasts
列表的组件WeatherForecastListPresenter
- 一个与数据管道交互以管理WeatherForecasts
列表的对象
进一步采用这些相同的设计原则,您可以从 DI 将 WeatherForecastListPresenter
的一个实例注入到 WeatherForcastListForm
中,其生命周期范围与表单相同。
在 DotNetCore 框架中,这呈现了一个困境
-
Scoped
范围太广:它适用于 SPA 会话的持续时间。如果仅在一个地方使用它,并且希望在 SPA 会话期间保持状态,则可以使用。 -
Transient
范围太窄。2.1. 表单中的子组件无法使用 DI 访问
WeatherForecastListPresenter
的同一实例。2.2. 任何实现
IDisposable
或IAsyncDisposable
的类都不应被设置为 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
的实例,该实例
- 可能具有也可能没有 DI 依赖项
- 可能实现也可能不实现
IDisposable
和/或IAsyncDisposable
- 可能是服务容器中的接口或基类服务定义
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;
}
}
serviceType
是 TService
的具体注册对象,或者为 null
。如果 TService
是一个接口或基类,则 TService
和 serviceType
将是不同的类型。
如果 ServiceType
为 null
,则服务容器中没有 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 日:初始版本