使用Blazor的OwningComponentBase

文章讨论了Blazor中OwningComponentBase的使用,包括它如何允许更细粒度的服务控制,以及与服务实例、disposal和作用域相关的问题。作者通过示例展示了如何处理服务实例的一致性,特别是对于瞬态服务,强调了不应该在这些服务中实现IDisposable,以及正确处理Dispose方法的重要性。
摘要由CSDN通过智能技术生成

目录

介绍

存储库

命名法

典型场景

测试服务

实例问题

修复实例问题

查看服务更改

对Test.razor的更改

Disposal问题

Dispose问题

总结


介绍

OwningComponentBase是一个具有自己的依赖项注入容器的Blazor组件。其目的是提供比SPA级别容器更精细的对作用域服务和瞬态服务的控制。

在本文中,我将深入探讨如何使用它。它有其缺点,其设计中存在一些固有的问题。

引用微软文档:

使用OwningComponentBase类作为基类来创建控制服务提供者作用域的生命周期的组件。这在使用需要处理(如存储库或数据库抽象)的瞬态或作用域服务时非常有用。使用OwningComponentBase作为基类可确保服务提供者作用域与组件一起被处理。

听起来不错,但很少有人使用它。我不知道这是因为他们不知道它,认为它太难使用还是已经尝试过并被它的问题所困扰。

OwningComponentBase有两种形式:

  1. OwningComponentBase在组件DI服务容器中手动设置和使用所需的任何服务的位置。
  2. OwningComponentBase<TService>其中TService添加到容器中并作为Service提供。您可以手动添加其他服务。

我不会反刍有关基本用法的相同旧信息。您可以从Microsoft文档以及几篇文章和视频中获取。

存储库

本文的存储库在这里——Blazr.OwningComponentBase

命名法

Blazor中很容易混淆命名法。

  • DI 是依赖注入的缩写
  • 组件DI容器是与OwningComponentBase实现组件关联的DI容器——通常是页面(另一个令人困惑的术语)。
  • SPA DI容器是与浏览器选项卡中运行的当前单页应用程序实例关联的DI容器。注意 F5 将关闭并重新初始化 SPA DI容器。每个浏览器选项卡都有一个单独的SPA DI容器。
  • 应用程序DI容器是运行所有单一实例的顶级DI实例。其中只有一个,它在应用程序实例的生存期内存在。

典型场景

我们有一个UI表单,显示我们俱乐部的成员列表。它使用连接到数据管道的视图服务来管理列表,并使用通知服务在基础数据发生更改时通知实时表单:可能是模式对话框中的编辑或删除。UI表单用于站点上的多个上下文:预订会话的成员列表、无偿成员,...因此,我们不想要单个作用域视图服务。

有两种方法:

  1. 设置要从OwningComponentBase中继承的窗体并使用内置的DI服务容器
  2. 使视图服务成为临时服务,每次使用表单时创建一个实例

一些测试服务和一个测试页面来查看这两个选项。

测试服务

暂时性服务类。

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服务容器中的不同。

如果在NotificationServiceViewService引发通知,则会在组件DI实例(而不是SPA DI实例)上引发通知。反之亦然,它在组件实例上注册处理程序时不会收到来自SPA实例的任何通知。

我相信许多程序员在这个障碍上绊倒并放弃了。

修复实例问题

修复它的关键是:

  1. 对DI基础知识有很好的了解
  2. 了解需要注入哪些服务实例

查看服务更改

通过一些异常检查更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容器维护对它创建的所有实现IDisposableIAsyncDisposable。这样做是为了在释放容器本身时在此类对象上运行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

因此,一般规则是永远不要在瞬态服务中实现IDisposableIAsyncDisposable或任何需要释放的功能。由于几乎所有数据库活动都需要某种形式的托管处置,因此谨慎的做法是永远不要执行与瞬态服务中的数据库关联的任何操作。

正如您在上面看到的,有一种方法可以使用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是工具箱中的好工具。您只需要知道如何部署它。

一些规则/准则:

  1. 确保您了解要使用的DI服务实例。
  2. 使用Guid和日志记录在开发过程中跟踪实例创建/处置。它可以非常有启发性!
  3. 瞬态服务不应实现或需要实现处置。
  4. 继承Dispose(bool disposing)时重写OwningComponentBase,绝不重写Dispose()

https://www.codeproject.com/Articles/5341118/Using-Blazors-OwningComponentBase

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Blazor使用 iframe 主要是通过使用 `iframe` 元素和 JavaScript 的 `postMessage` API 进行通信来实现的。下面是一个简单的示例,展示了如何在 Blazor 组件中嵌入一个 iframe。 1. 添加 iframe 元素 首先,在 Blazor 组件的渲染逻辑中添加一个 iframe 元素: ```html <iframe id="myIframe"></iframe> ``` 2. 通过 JavaScript 与 iframe 通信 然后,在组件的代码文件中,使用 JavaScript 进行与 iframe 的通信: ```csharp @inject IJSRuntime JSRuntime; @code { protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JSRuntime.InvokeVoidAsync("addIframeListener"); } } } ``` 在上面的代码中,我们使用 `IJSRuntime` 服务注入了 JavaScript 运行时,并在 `OnAfterRenderAsync` 方法中调用了一个名为 `addIframeListener` 的 JavaScript 函数。这个函数会监听来自 iframe 的消息,并将其打印到控制台。 3. 编写 JavaScript 函数 最后,编写一个名为 `addIframeListener` 的 JavaScript 函数,用于监听来自 iframe 的消息: ```javascript function addIframeListener() { window.addEventListener('message', function (event) { console.log('Received message from iframe:', event.data); }); } ``` 这个函数会监听 `message` 事件,当接收到来自 iframe 的消息时,将其打印到控制台。 以上就是在 Blazor使用 iframe 的基本方法,需要注意的是,由于涉及到与跨域 iframe 的通信,需要特别小心处理安全问题。如果不小心处理不当,可能会导致安全漏洞。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值