探索 Blazor 组件渲染





5.00/5 (1投票)
探索 Blazor 组件更新过程
引言
本文最初是我为了探索我以为自己已经理解的 Blazor 组件渲染机制而编写的一些代码。
代码库和站点
代码可在 GitHub 上的 AllinOne 项目中找到。原始项目演示了如何在同一项目中结合 Server 和 WASM。您将在 Shared Project 下的 AllinOne.Shared > Components > Controls 中找到代码,以及计数器页面 AllinOne.Shared > Components > Views > Counter.razor。
该应用程序托管在 Azure 上,地址为 https://allinoneserver.azurewebsites.net/。
代码
我们将构建一组计数器组件,可以在 Counter
页面中进行级联。
计数器
首先,我们需要一个基本的 Counter
类,可以将其作为服务运行。注意当 Counter
递增时触发的 event
。
public class CounterService
{
// The counter.
public int Counter { get; private set; } = 0;
// An event triggered when we increment the counter
public event EventHandler CounterChanged;
// The incrementer method. Triggers CounterChanged when called
public void IncrementCounter()
{
this.Counter++;
this.CounterChanged?.Invoke(this, EventArgs.Empty);
}
}
将其添加到 StartUp
(Server) 或 Program
(WASM) 中。
// Server
public class Startup
{
......
public void ConfigureServices(IServiceCollection services)
{
.....
services.AddSingleton<CounterService>();
}
.....
}
// WASM
public class Program
{
public static async Task Main(string[] args)
{
....
builder.Services.AddSingleton<CounterService>();
....
}
}
BaseCounter
BaseCounter
实现基本的计数器组件功能。
一个按钮,带有两个徽章,中间有一个标签。左侧徽章显示本地 counter
变量的值,右侧徽章显示 CounterService.Counter
的值。OnParametersSetAsync
将本地 counter
设置为 CounterService.Counter
。因此,Counter
仅在组件通过 Render
被调用 SetParametersAsync
时更新。
@namespace AllinOne.Shared.Components
<table class="border border-primary">
<tr>
<td class="text-nowrap">
<button type="button" class="btn @this.buttoncolor"
@onclick="() => this.Service.IncrementCounter()">
<span class="badge bg-light text-dark mx-1">@this.counter</span>
@this.Name <span class="badge bg-dark text-white ml-1">
@this.Service.Counter</span>
</button>
</td>
@if (ChildContent != null)
{
<td width="90%">
@ChildContent
</td>
}
@if (Body != null)
{
<td width="90%">
@Body
</td>
}
</tr>
</table>
@code {
[Inject] public CounterService Service { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public RenderFragment Body { get; set; }
[Parameter] public string Name { get; set; } = "Add";
protected virtual string buttonclass => (ChildContent != null ||
Body != null) ? "col-2" : "col";
protected virtual string buttoncolor => "btn-secondary";
protected virtual int counter { get; set; }
protected override Task OnParametersSetAsync()
{
counter = Service.Counter;
return Task.CompletedTask;
}
}
EventCounter
EventCounter
继承自 BaseCounter
。它向服务 CounterChanged
事件注册一个本地 ReRender
事件处理程序。当计数器更新时,ReRender
通过 StateHasChanged
强制 UI 更新。
@inherits BaseCounter
// Markup the same as BaseCounter
@code {
protected override string buttoncolor => "btn-success";
protected override Task OnInitializedAsync()
{
Service.CounterChanged += ReRender;
return base.OnInitializedAsync();
}
protected void ReRender(object sender, EventArgs e) =>
this.InvokeAsync(this.StateHasChanged);
}
CascadedValueCounter
CascadedValueCounter
继承自 BaseCounter
。它添加了捕获级联的 CascadedCounter
的功能,并将本地 counter
设置为捕获的值。
@inherits BaseCounter
// Markup the same as BaseCounter
@code {
[CascadingParameter(Name ="CascadedCounter")] public int cascadedcounter { get; set; }
protected override string buttoncolor => "btn-danger";
protected override Task OnParametersSetAsync()
{
counter = cascadedcounter;
return Task.CompletedTask;
}
}
CascadedObjectCounter
CascadedObjectCounter
继承自 BaseCounter
。它添加了捕获级联的 CascadedService
的功能,并将本地 counter
设置为捕获的 CounterService.Counter
。
@inherits BaseCounter
// Markup the same as BaseCounter
@code {
protected override string buttoncolor => "btn-warning";
[CascadingParameter(Name = "CascadedService")]
public CounterService CascadedService { get; set; }
protected override Task OnParametersSetAsync()
{
counter = CascadedService?.Counter ?? 0;
return Task.CompletedTask;
}
}
Counter Page
最后一步是构建一个新的计数器页面,其中包含各种不同的计数器组件组合在组件树中。请注意根计数器、注入的 CounterService
和级联值。我们还为每个组件进行了标记,以便于引用。
<h1>Counter</h1>
<table class="border border-primary">
<tr>
<td class="text-nowrap">
<button type="button" class="btn btn-primary"
@onclick="() => this.Service.IncrementCounter()">
Root <span class="badge bg-light text-dark">@this.counter</span>
<span class="badge bg-dark text-white ml-1">@this.Service.Counter</span>
</button>
</td>
<td width="90%">
<CascadingValue Name="CascadedService" Value="this.Service">
<CascadingValue Name="CascadedCounter" Value="this.Service.Counter">
<BaseCounter Name="A">
<BaseCounter Name="A.A">
<BaseCounter Name="A.A.A"></BaseCounter>
<BaseCounter Name="A.A.B"><Body>With Body Content</Body>
</BaseCounter>
</BaseCounter>
<BaseCounter Name="A.B">
<BaseCounter Name="A.B.A">With Child Content</BaseCounter>
<BaseCounter Name="A.B.B"></BaseCounter>
<EventCounter Name="A.B.C">With Child Content</EventCounter>
<EventCounter Name="A.B.D"></EventCounter>
</BaseCounter>
</BaseCounter>
<BaseCounter Name="B">
<EventCounter Name="B.A">
<BaseCounter Name="B.A.A">With Child Content</BaseCounter>
<BaseCounter Name="B.A.B"></BaseCounter>
</EventCounter>
</BaseCounter>
<BaseCounter Name="C">
<EventCounter Name="C.A">
<BaseCounter Name="C.A.A"></BaseCounter>
</EventCounter>
</BaseCounter>
<EventCounter Name="D">
<BaseCounter Name="D.A">has content</BaseCounter>
</EventCounter>
<BaseCounter Name="E">
<BaseCounter Name="E.A">
<CascadedObjectCounter Name="E.A.A">With Child Content
</CascadedObjectCounter>
<CascadedObjectCounter Name="E.A.B"></CascadedObjectCounter>
<CascadedValueCounter Name="E.A.C">With Child Content
</CascadedValueCounter>
<CascadedValueCounter Name="E.A.D"></CascadedValueCounter>
</BaseCounter>
<EventCounter Name="E.B">
<CascadedValueCounter Name="E.B.A">With Child Content
</CascadedValueCounter>
<CascadedValueCounter Name="E.B.B"></CascadedValueCounter>
<CascadedObjectCounter Name="E.B.C">With Child Content
</CascadedObjectCounter>
<CascadedObjectCounter Name="E.B.D"></CascadedObjectCounter>
</EventCounter>
</BaseCounter>
</CascadingValue>
</CascadingValue>
</td>
</tr>
</table>
@code {
[Inject] public CounterService Service { get; set; }
protected virtual int counter { get; set; }
protected override Task OnParametersSetAsync()
{
counter = Service.Counter;
return Task.CompletedTask;
}
}
正在发生什么?
访问 allinoneserver.azurewebsites.net 查看页面实际效果。
左侧的值是本地 counter
变量的值。如果它不是当前值,则在上次事件(通常是按钮点击)时没有对组件调用 SetParametersAsync
。
右侧的值显示当前的 CounterService.Counter
。如果它已更新,则组件在上次事件中通过 StateHasChanged
进行了重新渲染。如果不是,则没有。
- 蓝色是根计数器(一个
BaseCounter
) - 灰色是
BaseCounters
- 绿色是
EventCounters
- 琥珀色是
CascadedObjectCounters
- 红色是
CascadedValueCounters
- 带子内容表示在
ChildContent
中有内容的计数器 - 带主体内容表示在
Body
中有内容的计数器
让我们看一些场景
点击根
大多数子项已更新。该事件已导致子组件级联调用 SetParametersAsync
。例如,A.A 的两个值都设置为当前值,因此已调用 SetParametersAsync
。请注意 SetParametersAsync
会自动调用 StateHasChanged
。
但是,A.A.A 未更新。与 A.A.B 不同,它没有子内容。您可以在其他地方看到这一点 - 例如 B.A.B 和 C.A.A。
点击 A
现在我们只看到部分已更新。A 已更新,但仅更新了第二个值。按钮点击自动触发了对 StateHasChanged
的调用,导致组件渲染,但没有调用 SetParametersAsync
。子组件的反应与根点击一样 - 只有带有子内容的组件已更新。
绿色计数器都已更新 - 例如 A.B.C 和 A.B.D - 无论它们是否有子内容。带有子内容的 A 的直接后代已运行 SetParametersAsync
,而没有子内容或不是后代的则没有。它们已通过连接到 CounterService.CounterChanged
事件的事件处理程序进行了渲染。请注意,EventCounters 的任何子项 - 例如 B.B.A - 如果带有子内容,也已更新。
点击 E
请注意,带内容的黄色和红色级联计数器已更新,但没有内容的计数器没有。另外请注意,在黄色 CascadedObject
计数器上,两个值都是最新的,而在红色 CascadedValue
计数器上,左侧值未更新。SetParametersAsync
已运行,但级联值未更新。它是一个原始类型,仅在页面加载或点击根时设置。对象级联是一个指向对象的指针,该对象是最新的。
新浏览器窗口
在同一页面上打开一个新的浏览器窗口。将两个窗口并排放置。点击其中一个窗口中的任意位置。
发生的情况取决于您是在 Server 还是 WASM 模式下运行。在 Server 模式下,所有绿色计数器(以及带内容的子项)将在另一个窗口中更新。在 WASM 模式下,什么也不会发生。在 Server 模式下,CounterService
作为单例运行,应用程序的所有副本都使用同一个对象,因此连接到同一个事件。在 WASM 模式下,没有像单例这样的东西 - 它们是作用域的。
结论
我可以坐在这里尝试定义一套编码规则来覆盖各种场景。但是……为什么要在“万能”方法 - 服务事件 - 如此方便的情况下,还要费力地去规避呢?
我的建议很简单,使用服务事件。
历史
- 2020年12月18日:初始版本