目录
介绍
OwningComponentBase是一个具有自己的依赖项注入容器的Blazor组件。其目的是提供比SPA级别容器更精细的对作用域服务和瞬态服务的控制。
在本文中,我将深入探讨如何使用它。它有其缺点,其设计中存在一些固有的问题。
引用微软文档:
使用OwningComponentBase类作为基类来创建控制服务提供者作用域的生命周期的组件。这在使用需要处理(如存储库或数据库抽象)的瞬态或作用域服务时非常有用。使用OwningComponentBase作为基类可确保服务提供者作用域与组件一起被处理。
听起来不错,但很少有人使用它。我不知道这是因为他们不知道它,认为它太难使用还是已经尝试过并被它的问题所困扰。
OwningComponentBase有两种形式:
- OwningComponentBase在组件DI服务容器中手动设置和使用所需的任何服务的位置。
- OwningComponentBase<TService>其中TService添加到容器中并作为Service提供。您可以手动添加其他服务。
我不会反刍有关基本用法的相同旧信息。您可以从Microsoft文档以及几篇文章和视频中获取。
存储库
本文的存储库在这里——Blazr.OwningComponentBase。
命名法
在Blazor中很容易混淆命名法。
- DI 是依赖注入的缩写
- 组件DI容器是与OwningComponentBase实现组件关联的DI容器——通常是页面(另一个令人困惑的术语)。
- SPA DI容器是与浏览器选项卡中运行的当前单页应用程序实例关联的DI容器。注意 F5 将关闭并重新初始化 SPA DI容器。每个浏览器选项卡都有一个单独的SPA DI容器。
- 应用程序DI容器是运行所有单一实例的顶级DI实例。其中只有一个,它在应用程序实例的生存期内存在。
典型场景
我们有一个UI表单,显示我们俱乐部的成员列表。它使用连接到数据管道的视图服务来管理列表,并使用通知服务在基础数据发生更改时通知实时表单:可能是模式对话框中的编辑或删除。UI表单用于站点上的多个上下文:预订会话的成员列表、无偿成员,...因此,我们不想要单个作用域视图服务。
有两种方法:
- 设置要从OwningComponentBase中继承的窗体并使用内置的DI服务容器
- 使视图服务成为临时服务,每次使用表单时创建一个实例
一些测试服务和一个测试页面来查看这两个选项。
测试服务
暂时性服务类。
public class TransientService : IDisposable
{
public Guid Uid = Guid.NewGuid();
public TransientService() =>
Debug.WriteLine($"{this.GetType().Name} - created instance: {Uid}");
public virtual void Dispose() =>
Debug.WriteLine($"{this.GetType().Name} - Disposed instance: {Uid}");
}
实现基本通知模式的通知服务。
public class NotificationService : IDisposable
{
public Guid Uid = Guid.NewGuid();
public event EventHandler? Updated;
public string Message { get; private set; } = string.Empty;
public NotificationService() =>
Debug.WriteLine($"{this.GetType().Name} - created instance: {Uid}");
public void Dispose() =>
Debug.WriteLine($"{this.GetType().Name} - Disposed instance: {Uid}");
public void NotifyChanged()
{
this.Message = $"Updated at {DateTime.Now.ToLongTimeString()}";
this.Updated?.Invoke(this, EventArgs.Empty);
}
}
以及使用这两种服务的视图服务。
using System.Diagnostics;
namespace Blazr.OwningComponentBase.Data;
public class ViewService : IDisposable
{
public Guid Uid = Guid.NewGuid();
public NotificationService NotificationService;
public TransientService TransientService;
public ViewService(NotificationService notificationService,
TransientService transientService)
{
Debug.WriteLine($"{this.GetType().Name} - created instance: {Uid}");
NotificationService = notificationService;
TransientService = transientService;
}
public void UpdateView()
=> NotificationService.NotifyChanged();
public void Dispose()
=> Debug.WriteLine($"{this.GetType().Name} - Disposed instance: {Uid}");
}
它们在应用程序服务容器中注册,如下所示:
builder.Services.AddScoped<ViewService>();
builder.Services.AddScoped<NotificationService>();
builder.Services.AddTransient<TransientService>();
实例问题
演示这一点的测试页。它继承自OwningComponentBase<TService>。
@page "/"
@inherits OwningComponentBase<ViewService>
@implements IDisposable
@inject NotificationService NotificationService
<h1>OwningComponentBase Test 1</h1>
<div class="alert alert-primary">
<h5>Service Info</h5>
<div>
Service Id: @Service.Uid
</div>
<div>
Service => Notification Service Id: @Service.NotificationService?.Uid
</div>
<div>
Service => Notification Service Message: @Service.NotificationService?.Message
</div>
<div class="text-end">
<button class="btn btn-primary"
@onclick=this.UpdateView>Update View Notification Service Message</button>
</div>
</div>
<div class="alert alert-info">
<h5>Component Info</h5>
<div>
Local Notification Service Id: @NotificationService.Uid
</div>
<div>
Local Component Message: @NotificationService.Message
</div>
<div class="text-end">
<button class="btn btn-primary"
@onclick=this.UpdateLocal>Update Component Notification Service Message
</button>
</div>
</div>
@code {
protected override void OnInitialized()
=> NotificationService.Updated += this.OnUpdate;
private void OnUpdate(object? sender, EventArgs e)
=> this.InvokeAsync(StateHasChanged);
private void UpdateView()
=> Service.UpdateView();
private void UpdateLocal()
=> NotificationService.NotifyChanged();
protected override void Dispose(bool disposing)
{
NotificationService.Updated -= this.OnUpdate;
base.Dispose(disposing);
}
}
运行解决方案时,页面如下所示:
检查NotificationService实例上的Guid:服务有两个活动实例,因此有两个活动实例。
ViewService实例在组件DI容器中创建。它在其构造函数中定义NotificationService。组件DI容器从应用程序级服务工厂获取服务定义。看到它是一个作用域服务,它会检查自己的当前实例,没有找到任何实例,因此创建一个。
测试页注入NotificationService。此请求在页面创建期间由提供其实例的SPA容器处理。它与(即将创建的)组件DI服务容器中的不同。
如果在NotificationService上ViewService引发通知,则会在组件DI实例(而不是SPA DI实例)上引发通知。反之亦然,它在组件实例上注册处理程序时不会收到来自SPA实例的任何通知。
我相信许多程序员在这个障碍上绊倒并放弃了。
修复实例问题
修复它的关键是:
- 对DI基础知识有很好的了解
- 了解需要注入哪些服务实例
查看服务更改
通过一些异常检查更NotificationService改为属性,以确保我们在设置服务之前不使用该服务。
private NotificationService? _notificationService;
public NotificationService NotificationService
{
get
{
if (_notificationService is null)
throw new InvalidOperationException("No service is registered.
You must run SetParentServices before using the service.");
return _notificationService!;
}
}
更新构造函数以删除NotificationService注入。
public ViewService(TransientService transientService)
{
Debug.WriteLine($"{this.GetType().Name} - created instance: {Uid}");
TransientService = transientService;
}
添加SetServices方法。这将从提供的IServiceProvider实例获取NotificationService。
我们从组件调用此方法,并传入SPA DI服务提供程序。如果我们不这样做,我们会在第一次尝试使用它时得到我们刚刚编码的异常!
public void SetServices(IServiceProvider serviceProvider)
=> _notificationService = serviceProvider.GetService<NotificationService>();
对Test.razor的更改
注入SPA服务提供程序。
@inject IServiceProvider SpaServiceProvider
并调用.SetServicesServiceOnInitialized
protected override void OnInitialized()
{
Service.SetServices(SpaServiceProvider);
NotificationService.Updated += this.OnUpdate;
}
现在我们看到每个人都在使用NotificationService相同的DI实例。Guid是相同的,通知工作正常。
Disposal问题
DI容器维护对它创建的所有实现IDisposable或IAsyncDisposable。这样做是为了在释放容器本身时在此类对象上运行Dispose。垃圾回收器不会销毁此类对象,因为它们仍被引用(由容器引用)。释放 暂时性对象开始收集,导致内存泄漏,直到SPA会话结束。
您可以在我们的测试中看到问题。下面是服务创建和处置的日志。查看TransientService条目。当我们退出页面时,将释放瞬态服务4984252f,因为它是组件DI容器中的服务。瞬态服务333012f6由页面在SPA DI容器中创建,不会释放。下次访问该页面时,将创建另一个实例。
// First Load
Page => TransientService - created instance: 333012f6-452b-4105-9372-a67fb45c5b16
Page => NotificationService - created instance: 03817ba1-4dc1-4f62-b111-d1a8ceb28aac
OCB => TransientService - created instance: 4984252f-2308-4feb-b64c-4df34bc4688d
Page => ViewService - created instance: dfb929fc-3d54-4d42-a501-77be735d61c0
// Exit Page
Page => ViewService - Disposed instance: dfb929fc-3d54-4d42-a501-77be735d61c0
OCB => TransientService - Disposed instance: 4984252f-2308-4feb-b64c-4df34bc4688d
// Back to Page
Page => TransientService - created instance: 07bf1685-24b3-4714-8ecc-f9c82d6cc4ca
OCB => TransientService - created instance: 11f35c6f-7cf1-420a-9bf3-90f29108d8e3
Page => ViewService - created instance: f1451c57-9c87-4924-b171-f0fcbd7b5741
// Exit Page
Page => ViewService - Disposed instance: f1451c57-9c87-4924-b171-f0fcbd7b5741
OCB => TransientService - Disposed instance: 11f35c6f-7cf1-420a-9bf3-90f29108d8e3
因此,一般规则是永远不要在瞬态服务中实现IDisposable或IAsyncDisposable或任何需要释放的功能。由于几乎所有数据库活动都需要某种形式的托管处置,因此谨慎的做法是永远不要执行与瞬态服务中的数据库关联的任何操作。
正如您在上面看到的,有一种方法可以使用OwningComponentBase来解决这个问题,它是短暂的DI容器。在上面的代码中,每次我们离开页面时都会释放临时服务。
但是,我要告诫不要以这种方式实施服务。在主SPA容器中意外开始使用此类服务太容易了。最好将服务设置为Scoped,然后仅在OwningComponentBase中使用它。
Dispose问题
在测试页中,我们挂接了一个事件处理程序,我们需要在处置时分离该处理程序。
public void Dispose()
=> NotificationService.Updated -= this.OnUpdate;
这将隐藏因此永远不会运行OwningComponentBase的实现。它包含一些相当重要的代码来释放DI容器。
void IDisposable.Dispose()
{
if (!IsDisposed)
{
_scope?.Dispose();
_scope = null;
Dispose(disposing: true);
IsDisposed = true;
}
}
正确的方法是重写Dispose(bool disposing)。
protected override void Dispose(bool disposing)
{
NotificationService.Updated -= this.OnUpdate;
base.Dispose(disposing);
}
总结
OwningComponentBase是工具箱中的好工具。您只需要知道如何部署它。
一些规则/准则:
- 确保您了解要使用的DI服务实例。
- 使用Guid和日志记录在开发过程中跟踪实例创建/处置。它可以非常有启发性!
- 瞬态服务不应实现或需要实现处置。
- 继承Dispose(bool disposing)时重写OwningComponentBase,绝不重写Dispose()。
https://www.codeproject.com/Articles/5341118/Using-Blazors-OwningComponentBase