我需要相当于依赖注入的来解决 Blazor/Razor 组件。
也就是说,我想以与 DI / IoC 允许我们解耦后端服务完全相同的方式解耦 Razor 类库中的组件。 有谁知道我怎样才能实现这个目标?
我不是问如何使用 DI 来解析 blazor/razor 组件中的服务。
想象一个包含三个区域的大型 blazor 项目,每个区域都有自己的 Razor 类库:
我可以将这三个领域很好地分解为三个独立的项目。依赖注入使我能够在最终项目中很好地整合service依赖项。 例如,razor 页面
Page_Student_View
可以很好地使用 IRoomService
,其实现来自使用依赖注入的 rooms 项目。
但是,我无法找到任何方法来实现与 Razor 组件的等效解耦。 我想要的是这样的:
@* Inside the student project *@
@page "/student/view
<div>
My tearcher is <ITeacherInlineDisplay TeacherId="@Student.TeacherId"/>
<br/>
My room is <IRoomInlineDisplay RoomId="@Student.RoomId"/>
</div>
接口定义在公共类库中:
/* Inside the Common project */
public interface ITeacherInlineDisplay : IComponent
{
[Parameter] int? TeacherId { get; set;}
}
在教师项目内实现房间显示控制的实现:
@* Inside the Teacher project *@
@implements ITeacherInlineDisplay
<div>
@Teacher.Name
@* Complex stuff in here: context menu, drag + drop etc. *@
</div>
@code {
[Parameter] int? TeacherId { get; set;}
}
我可以很快实现 2 - 如果没有人有更好的解决方案,我可能会这样做。 我考虑修补 Blazor/Razor 源,使其在编译 razor 页面时解析 IComponent。这是一项我认为不值得我做的大工作。
我简直不敢相信我是唯一有这种需求的人 - 萨利对这种架构有更广泛的需求吗?
我有一个相当大的 ASP.NET Blazor 服务器端应用程序。我在管理 razor/Blazor 组件之间的依赖关系时遇到问题。
这个项目发展得非常迅速,但没有一个连贯的计划——更多的是一个原型项目,它已经与客户一起上线,并且不断发展壮大。 Blazor / EF Core 很好地处理了这个问题。我们经常重构,依靠 Visual Studio 重构和 EF Core 迁移来更新所有内容。
我们有效地进行了垂直切片(按业务领域)和水平切片(数据后期/中间层/UI 组件)。
我正在运行(并且始终会)最新版本的堆栈:当前为 .NET 8 / ASP.NET Core 8 / EF Core 8。
如果我们有 razor 组件的 DI,我会考虑在 UI 级别实施测试。如果没有这个(并且能够在控件中模拟/存根)我就不会打扰。
免责声明:这只是一个概念证明 - 并不是好或坏的建议 - 我将其留给您来决定。
Blazor 能够通过
IComponentActivator
接口的实例控制组件激活。
DefaultComponentActivator
。
您可以通过 DI 提供您自己的:
builder.Services.AddSingleton<IComponentActivator,MyComponentActivator>();
渲染器现在使用MyComponentActivator
来根据需要创建组件实例。
快速复制粘贴
DefaultComponentActivator
并进行一些调整以使用 DI 会得到如下所示的结果:
MyComponentActivator
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
public class MyComponentActivator(IServiceProvider serviceProvider) : IComponentActivator
{
public IComponent CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
{
var resolvedType = serviceProvider.GetService(componentType);
if (resolvedType is IComponent component)
{
return component;
}
return DefaultCreateInstance(componentType);
}
/* Code below here is (c) Microsoft - taken from DefaultComponentActivator */
private static readonly ConcurrentDictionary<Type, ObjectFactory> _cachedComponentTypeInfo = new();
public static void ClearCache() => _cachedComponentTypeInfo.Clear();
/// <inheritdoc />
public IComponent DefaultCreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
{
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
}
var factory = GetObjectFactory(componentType);
return (IComponent)factory(serviceProvider, []);
}
private static ObjectFactory GetObjectFactory([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
{
// Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the
// callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary
// and it doesn't matter if we build a cache entry more than once.
if (!_cachedComponentTypeInfo.TryGetValue(componentType, out var factory))
{
factory = ActivatorUtilities.CreateFactory(componentType, Type.EmptyTypes);
_cachedComponentTypeInfo.TryAdd(componentType, factory);
}
return factory;
}
}
CreateInstance
方法已编写为使用 DI 来定位和激活组件实例 - 当 DI 无法解决任何问题时,会回退到默认组件激活。
读者任务:我们是否可以将默认的 IComponentActivator 注入到 MyComponentActivator 中,这样我们就不需要复制代码???
当您编写 Blazor 代码时,编译器需要组件的设计时知识,并且接口将无法工作。
你不能简单地写
<ITeacherInlineDisplay/>
因为它不会被识别为组件。
创建一个简单的基本组件
TeacherInlineDisplay
(在共享库中,就像在接口中一样),它具有您想要使用的“接口”(例如虚拟参数)
TeacherInlineDisplay
public class TeacherInlineDisplay : ComponentBase
{
[Parameter] public virtual int? TeacherID { get; set; }
}
然后在您的
Teacher
项目/组件中继承/扩展该基类。
例如
TeacherInlineLocal
@* Inside the Teacher project *@
@inherits TeacherInlineDisplay
<div>
@Teacher.Name
@* Complex stuff in here: context menu, drag + drop etc. *@
</div>
@code {
[Parameter] public override int? TeacherId { get; set;}
}
您现在可以像这样在 DI 中注册组件实现:
builder.Services.AddTransient<TeacherInlineDisplay, TeacherInlineLocal>();
并将基本组件放置在页面上,如下所示:
@* Inside the student project *@
@page "/student/view
<div>
My tearcher is <TeacherInlineDisplay TeacherId="@Student.TeacherId"/>
<br/>
</div>
并且组件
TeacherInlineDisplay
将从 DI 解析。