65.9K
CodeProject 正在变化。 阅读更多。
Home

探索 Blazor 组件渲染

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020年12月18日

CPOL

4分钟阅读

viewsIcon

11706

探索 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 实现基本的计数器组件功能。

example counter

一个按钮,带有两个徽章,中间有一个标签。左侧徽章显示本地 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.BC.A.A

Root

点击 A

现在我们只看到部分已更新。A 已更新,但仅更新了第二个值。按钮点击自动触发了对 StateHasChanged 的调用,导致组件渲染,但没有调用 SetParametersAsync。子组件的反应与根点击一样 - 只有带有子内容的组件已更新。

绿色计数器都已更新 - 例如 A.B.CA.B.D - 无论它们是否有子内容。带有子内容的 A 的直接后代已运行 SetParametersAsync,而没有子内容或不是后代的则没有。它们已通过连接到 CounterService.CounterChanged 事件的事件处理程序进行了渲染。请注意,EventCounters 的任何子项 - 例如 B.B.A - 如果带有子内容,也已更新。

A

点击 E

请注意,带内容的黄色和红色级联计数器已更新,但没有内容的计数器没有。另外请注意,在黄色 CascadedObject 计数器上,两个值都是最新的,而在红色 CascadedValue 计数器上,左侧值未更新。SetParametersAsync 已运行,但级联值未更新。它是一个原始类型,仅在页面加载或点击根时设置。对象级联是一个指向对象的指针,该对象是最新的。

E

新浏览器窗口

在同一页面上打开一个新的浏览器窗口。将两个窗口并排放置。点击其中一个窗口中的任意位置。

发生的情况取决于您是在 Server 还是 WASM 模式下运行。在 Server 模式下,所有绿色计数器(以及带内容的子项)将在另一个窗口中更新。在 WASM 模式下,什么也不会发生。在 Server 模式下,CounterService 作为单例运行,应用程序的所有副本都使用同一个对象,因此连接到同一个事件。在 WASM 模式下,没有像单例这样的东西 - 它们是作用域的。

结论

我可以坐在这里尝试定义一套编码规则来覆盖各种场景。但是……为什么要在“万能”方法 - 服务事件 - 如此方便的情况下,还要费力地去规避呢?

我的建议很简单,使用服务事件。

历史

  • 2020年12月18日:初始版本
© . All rights reserved.