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

深入了解 Blazor 组件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (23投票s)

2020年8月25日

CPOL

15分钟阅读

viewsIcon

39496

深入了解 Blazor 服务器组件的结构和工作原理

引言

本文探讨了组件的结构、生命周期以及 Blazor 如何使用和管理组件来构建和运行 UI。

对组件的深入理解将极大地改变 Blazor 应用程序的开发体验。

什么是组件?

微软定义

组件是用户界面 (UI) 的一个独立部分,包含处理逻辑以实现动态行为。组件可以嵌套、重用、在项目之间共享,并用于 MVC 和 Razor Pages 应用程序。

组件使用 C# 和 HTML 标记的组合在带有 .razor 文件扩展名的 Razor 组件文件中实现。

它做什么,而不是它是什么,并且并非所有说法都绝对正确。

从编程角度来看,组件只是一个实现 IComponent 接口的类。仅此而已。当它附加到 RenderTreeRenderer 用于构建和更新的组件树)时,它就“活”过来了。UI IComponent 接口是 `Renderer` 用于与组件通信和接收来自组件的通信的接口。

在深入研究组件之前,我们需要了解 RendererRenderTree,以及应用程序的设置。

渲染器 (Renderer) 和渲染树 (Render Tree)

关于 RendererRenderTree 工作原理的详细描述超出了本文的范围,但您需要对这些概念有基本的掌握才能理解渲染过程。

RendererRenderTree 存在于 WASM 中的客户端应用程序内,以及 Server 中的 SignalR Hub 会话内,即每个连接的客户端应用程序都有一个。

UI - 由 DOM [文档对象模型] 中的 HTML 代码定义 - 在应用程序中表示为 RenderTree 并由 Renderer 管理。将 RenderTree 想象成一棵树,每个分支上都连接着一个或多个组件。每个组件都是一个实现 IComponent 接口的 C# 类。Renderer 有一个 RenderQueue,它运行代码来更新 UI。组件提交 RenderFragments,供 Renderer 运行以更新 RenderTree 和 UI。Renderer 使用差异比较过程来检测由 RenderTree 更新引起的 DOM 更改,并将这些更改传递给客户端代码,以便在浏览器 DOM 中实现并更新显示的页面。

下图是开箱即用的 Blazor 模板的渲染树的可视化表示。

客户端应用程序

Blazor Server

Blazor Server 在初始服务器/HTML 页面中定义了 <app> 组件。它看起来像这样

<app>
    <component type="typeof(App)" render-mode="ServerPrerendered" />
</app>

type 定义了路由组件类 - 在这种情况下是 App,而 render-mode 定义了初始服务器端渲染过程如何运行。您可以在其他地方阅读有关此内容。唯一重要的是要理解,如果它预渲染,页面将在初始加载时渲染两次 - 一次由服务器渲染以构建页面的静态版本,然后第二次由浏览器客户端代码渲染以构建页面的实时版本。

浏览器客户端代码通过以下方式加载

<script src="_framework/blazor.server.js"></script>

一旦 blazor.server.js 加载,客户端应用程序就会在浏览器页面中运行,并与服务器建立 SignalR 连接。为了完成初始加载,客户端应用程序会调用 Blazor Hub 会话并请求对 App 组件进行完整的服务器端渲染。然后,它将产生的 DOM 更改应用于客户端应用程序 DOM - 这主要是事件的连接。

下图显示了渲染请求如何传递到显示的页面

Blazor Web Assembly

在 Blazor WebAssembly 中,浏览器会收到一个 HTML 页面,其中有一个定义的 div 占位符,用于加载根组件

<div id="app">
    ....
</div>

客户端应用程序通过以下方式加载

<script src="_framework/blazor.webassembly.js"></script>

WASM 代码加载后,它会运行 program

builder.RootComponents.Add<App>("#app");

该代码告诉渲染器 App 类组件是 RenderTree 的根组件,并将它的 DOM 加载到浏览器 DOM 的 app 元素中。

这里的关键点是,尽管定义和加载根组件的过程不同,但 WebAssembly 和 Server 的根组件或任何子组件之间没有区别。您可以使用相同的组件。

App.razor

App.razor 是“标准”的根组件。它可以是任何已定义的 IComponent 类。

App 看起来像这样

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

它是一个 Razor 组件,定义了一个子组件 RouterRouter 有两个 RenderFragmentsFoundNotFound。如果 Router 找到一个路由,因此找到一个 IComponent 类,它将渲染 RouteView 组件,并将路由类类型以及默认 Layout 类传递给它。如果找不到路由,它将渲染 LayoutView 并在其 Body 中渲染定义的 Gontent。

RouteView 检查 RouteData 组件是否定义了特定的布局类。如果定义了,它就使用它,否则它使用默认布局。它渲染布局并将要添加到 Body RenderFragment 的组件类型传递给它。

Components

所有组件都是实现 IComponent 的普通 DotNetCore 类。

IComponent 接口定义如下

public interface IComponent
{
    void Attach(RenderHandle renderHandle);
    Task SetParametersAsync(ParameterView parameters);
}

我看到这个的第一反应是“什么?这里少了什么。那些事件和初始化方法都在哪里?”您阅读的每篇文章都在谈论组件和 OnInitialized,……不要让他们迷惑您。这些是 ComponentBase 的一部分,它是 IComponent 的开箱即用 Blazor 实现。ComponentBase 并不定义一个组件。您将在下面看到一个更简单的实现。

让我们更详细地看看定义的内容。Blazor Hub 会话有一个 Renderer,它为每个根组件运行 RenderTree。技术上讲,可以有一个以上的根组件,但我们将在这次讨论中忽略这一点。引用类文档

Renderer 提供机制

  1. 用于渲染 IComponent 实例的层次结构
  2. 将事件分派给它们
  3. 在用户界面正在更新时发出通知

一个 RenderHandle 结构

  1. 允许组件与其渲染器进行交互。

回到 IComponent 接口

  1. RendererIComponent 对象附加到 RenderTree 时,会调用 Attach。它将一个 RenderHandle 结构传递给组件。组件使用此渲染句柄将 RenderFragments 排队到 RendererRenderQueue。我们很快会更详细地介绍 RenderFragement
  2. Renderer 首次将组件附加到 RenderTree 并认为该组件的一个或多个 Parameters 已更改时,它会调用组件的 SetParametersAsync

请注意,IComponent 没有 RenderTree 的概念。它通过调用 SetParametersAsync 来触发操作,并通过调用 RenderHandle 上的方法来传递更改。

HelloWorld 组件

为了演示 IComponent 接口,我们将构建一个简单的 HelloWorld 组件。

我们最简单的 Hello World Razor 组件如下所示

@page "/helloworld"
<div>
    Hello World
</div>

这是一个 Razor 定义的组件。

我们可以重构它,使其看起来像这样

@page "/helloworld"

@HelloWorld

@code {
    protected RenderFragment HelloWorld => (RenderTreeBuilder builder) =>
    {
        builder.OpenElement(0, "div");
        builder.AddContent(1, "Hello World 2");
        builder.CloseElement();
    };
}

这引入了 RenderFragment。引用官方微软文档。

RenderFragment 代表 UI 内容的一个片段,实现为将内容写入 RenderTreeBuilder 的委托。

RenderTreeBuilder 更加简洁

提供用于构建 RenderTreeFrame 条目集合的方法。

因此,RenderFragment 是一个委托 - 在 Microsoft.AspNetCore.Components 中定义如下

public delegate void RenderFragment(RenderTreeBuilder builder);

如果您不熟悉委托,请将其视为模式定义。任何符合 RenderFragment 委托定义的模式的函数都可以作为 RenderFragment 传递。

该模式规定您的方法必须

  1. 具有一个且仅一个类型为 RenderTreeBuilder 的参数
  2. 返回 void

回顾上面的代码,我们正在定义一个 RenderFragment 属性,并为其分配一个匿名方法,该方法符合 RenderFragment 模式。它接受一个 RenderTreeBuilder 并且没有返回值,因此返回 void。它使用提供的 RenderTreeBuilder 对象来构建内容:一个简单的 hello world HTML div。对生成器的每次调用都会添加一个称为 RenderTreeFrame 的项。请注意,每个帧都按顺序编号。

理解两点很重要

  1. 组件本身永远不会“运行”RenderFragement。它被传递给渲染器,渲染器调用它。
  2. 即使 Renderer 调用代码,代码也在组件的上下文中运行,并且在执行时会发生组件的状态。

一个简单的 IComponent 实现

上面的 HelloWorld 组件继承自 ComponentBase。默认情况下,不显式定义继承的 Razor 组件会继承自 ComponentBase

我们现在可以构建我们的组件为一个简单的 C# 类。

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System.Threading.Tasks;

namespace Blazor.HelloWorld.Pages
{
    [RouteAttribute("/helloworld")]
    public class RendererComponent : IComponent
    {
        private RenderHandle _renderHandle;

        public void Attach(RenderHandle renderHandle)
        {
            _renderHandle = renderHandle;
        }

        public Task SetParametersAsync(ParameterView parameters)
        {
            parameters.SetParameterProperties(this);
            this.Render();
            return Task.CompletedTask;
        }

        public void Render()
            => _renderHandle.Render(RenderComponent);

        private void RenderComponent(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, "div");
            builder.AddContent(1, "Hello World 2");
            builder.CloseElement();
        }
    }
}

上述代码中需要注意的点

  1. 该类使用自定义属性 RouteAttribute 来定义路由。
  2. 该类继承自 IComponent
  3. 该类实现了 Attach。传递的对象 RenderHandle 被分配给本地类字段。
  4. 该类实现了 SetParametersAsync,该方法在组件首次渲染时调用,并且在任何 Parameters 更改时都会调用。在本例中,我们没有定义 Parameters,因此它永远不会被调用。它调用类方法 Render
  5. 其余代码是从 Razor 组件复制的。
  6. 没有 OnInitializedOnAfterRenderStateHasChanged……这些都是 ComponentBase 的一部分。

Render 方法在组件附加到渲染树时从 RenderHandle 上调用 RenderHandle.Render。它将 RenderComponent 方法作为委托传递。调用 Render 会将传递的委托排入 Rendererrender 队列。这就是代码实际执行的地方。作为委托,它在拥有它的对象的上下文中执行。

组件非常简单,但它演示了基本原理。

路由组件

一切都是组件,但并非所有组件都相等。路由组件有点特殊。

它们包含 @page 路由指令,并且可以选择包含 @Layout 指令。

@page "/WeatherForecast"
@page "/WeatherForecasts"
@layout MainLayout

您可以像这样直接在类上定义它们

[LayoutAttribute(typeof(MainLayout))]
[RouteAttribute("/helloworld")]
public class RendererComponent : IComponent {}

RouteAttribute 由路由器用于在应用程序中查找路由。

不要将路由组件视为页面。这样做可能很明显,但不要这样做。许多网页属性不适用于路由组件。您会

  • 在路由组件行为不像页面时感到困惑。
  • 尝试像编写网页一样编写组件逻辑。

ComponentBase

ComponentBaseIComponent 的“标准”开箱即用 Blazor 实现。所有 .razor 文件默认继承自它。虽然您可能永远不会超出 ComponentBase 的范围,但重要的是要理解它只是 IComponent 接口的一种实现。它不定义组件。OnInitialized 不是组件生命周期方法,而是 ComponentBase 的生命周期方法。

ComponentBase 生命周期和事件

有很多文章都在重复相同的基本生命周期信息。我不会重复。相反,我将专注于生命周期中一些容易被误解的方面:生命周期比大多数文章涵盖的初始组件加载要复杂得多。

我们需要考虑五种类型的事件

  1. 类的实例化
  2. 组件的初始化
  3. 组件参数更改
  4. 组件事件
  5. 组件的处置

有七个公开的事件/方法及其异步等效项

  1. SetParametersAsync
  2. OnInitializedOnInitializedAsync
  3. OnParametersSetOnParametersSetAsync
  4. OnAfterRenderOnAfterRenderAsync
  5. Dispose - 如果实现了 IDisposable
  6. StateHasChanged
  7. new - 经常被遗忘

标准的类实例化方法构建 RenderFragmentStateHasChanged 将其传递给 Renderer 以渲染组件。它将两个 private 类变量设置为 false 并运行 BuildRenderTree

public ComponentBase()
{
    _renderFragment = builder =>
    {
        _hasPendingQueuedRender = false;
        _hasNeverRendered = false;
        BuildRenderTree(builder);
    };
}

SetParametersAsync 设置已提交参数的属性。它仅在初始化时运行 RunInitAndSetParametersAsync - 因此是 OnInitialized 后跟 OnInitializedAsync。它总是调用 CallOnParametersSetAsync。注意

  1. CallOnParametersSetAsyncOnInitializedAsync 完成之前等待,然后再调用 CallOnParametersSetAsync
  2. RunInitAndSetParametersAsync 如果 OnInitializedAsync 任务在完成之前让步,则调用 StateHasChanged
public virtual Task SetParametersAsync(ParameterView parameters)
{
    parameters.SetParameterProperties(this);
    if (!_initialized)
    {
        _initialized = true;
        return RunInitAndSetParametersAsync();
    }
    else return CallOnParametersSetAsync();
}

private async Task RunInitAndSetParametersAsync()
{
    OnInitialized();
    var task = OnInitializedAsync();
    if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
    {
        StateHasChanged();
        try { await task;}
        catch { if (!task.IsCanceled) throw; }
    }
    await CallOnParametersSetAsync();

CallOnParametersSetAsync 调用 OnParametersSet 后跟 OnParametersSetAsync,最后调用 StateHasChanged。如果 OnParametersSetAsync() 任务让步,CallStateHasChangedOnAsyncCompletion 会等待任务并重新运行 StateHasChanged

private Task CallOnParametersSetAsync()
{
    OnParametersSet();
    var task = OnParametersSetAsync();
    var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
        task.Status != TaskStatus.Canceled;

    StateHasChanged();

    return shouldAwaitTask ?
        CallStateHasChangedOnAsyncCompletion(task) :
        Task.CompletedTask;
}

private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
    try { await task; }
    catch 
    {
        if (task.IsCanceled) return;
        throw;
    }
    StateHasChanged();
}

最后,让我们看看 StateHasChanged。如果正在等待渲染,即渲染器还没有来得及运行排队的渲染请求,它就会完成 - 任何更改都将在排队的渲染中捕获。否则,它会设置 _hasPendingQueuedRender 类标志,并调用 RenderHandle 上的 Render 方法。这会将 _renderFragement 排入 RendererRenderQueue。当队列运行时,_renderFragment - 见上文 - 将两个类标志设置为 false 并运行 BuildRenderTree

protected void StateHasChanged()
{
    if (_hasPendingQueuedRender) return;
    if (_hasNeverRendered || ShouldRender())
    {
        _hasPendingQueuedRender = true;
        try { _renderHandle.Render(_renderFragment);}
        catch {
            _hasPendingQueuedRender = false;
            throw;
        }
    }
}

一些需要注意的关键点

  1. OnInitializedOnInitializedAsync 只在初始化期间调用。OnInitialized 先运行。如果,并且仅当 OnInitializedAsync 让步回内部调用方法 RunInitAndSetParametersAsync 时,才会调用 StateHasChanged,从而有机会向用户提供“正在加载”信息。OnInitializedAsync 在调用 OnParametersSetOnParametersSetAsync 之前完成。
  2. OnParametersSetOnParametersSetAsync 在父组件更改组件的参数集或捕获的级联参数更改时被调用。任何需要响应参数更改的代码都需要放在这里。OnParametersSet 先运行。请注意,如果 OnParametersSetAsync 让步,StateHasChanged 将在让步后运行,从而有机会向用户提供“正在加载”信息。
  3. OnParametersSet{async} 方法完成和任何事件回调之后,会调用 StateHasChanged 来渲染组件。
  4. OnAfterRenderOnAfterRenderAsync 发生在所有四个事件的末尾。firstRender 仅在组件初始化时为 true。请注意,此处对参数所做的任何更改直到组件重新渲染后才能应用于显示值。
  5. 如果满足上述条件,StateHasChanged 将在初始化过程中,在 OnParametersSet 过程和任何事件回调之后被调用。除非您需要,否则不要在渲染或参数设置过程中显式调用它。如果您调用它,您很可能做错了什么。

渲染过程

让我们详细看看一个简单的页面和组件是如何渲染的。

SimpleComponent.razor

<div class="h4 bg-success text-white p-2">Loaded</div>

SimplePage.razor

@page "/simple"
<h3>SimplePage</h3>
@if (loaded)
{
    <SimpleComponent></SimpleComponent>
}
else
{
    <div class="h4 bg-danger text-white p-2">Loading.....</div>
}

@code {
    private bool loaded;

    protected async override Task OnInitializedAsync()
    {
        await Task.Delay(2000);
        loaded = true;
    }
}

下面的图显示了一个简单的“/”路由的简化 RenderTree

请注意 NavMenu 中用于三个 NavLink 控件的三个节点。

在我们的页面上,首次渲染时,渲染树看起来像下图 - 我们有一个让步的 OnInitializedAsync 方法,因此 StateHasChanged 在初始化过程中被运行。

初始化完成后,StateHasChanged 将再次运行。现在 Loadedtrue,并且 SimpleComponent 被添加到组件 RenderFragment。当 Renderer 运行 RenderFragment 时,SimpleComponent 会被添加到渲染树,实例化并初始化。

组件内容

更改 SimpleComponentSimplePage

SimpleComponent.razor

<div class="h4 bg-success text-white p-2">@ChildContent</div>

@code {
    [Parameter] public RenderFragment ChildContent { get; set; }
}

SimplePage.razor

@page "/simple"
<h3>SimplePage</h3>
@if (loaded)
{
    <SimpleComponent>
        <button class="btn btn-primary" @onclick="ButtonClick">Click Me</button>
    </SimpleComponent>
}
else
{
    <div class="h4 bg-danger text-white p-2">Loading.....</div>
}

@code {
    private bool loaded;

    protected async override Task OnInitializedAsync()
    {
        await Task.Delay(2000);
        loaded = true;
    }

    protected void ButtonClick(MouseEventArgs e)
    {
        var x = true;
    }
}

SimpleComponent 中现在有内容。当应用程序运行时,该内容将在父组件的上下文中执行。如何执行?

答案就在 SimpleComponent 中。删除 SimpleComponent 上的 [Parameter] 属性并运行页面。它会出错

InvalidOperationException: Object of type 'xxx.SimpleComponent' 
has a property matching the name 'ChildContent', 
but it does not have [ParameterAttribute] or [CascadingParameterAttribute] applied.

如果组件有“内容”,即标签之间的标记,Blazor 会期望在组件中找到一个名为 ChildContentParameter。标签之间的内容会被预编译成一个 RenderFragment,然后添加到组件中。RenderFragment 的内容将在其所有者的对象(SimplePage)的上下文中运行。

内容也可以这样定义

<SimpleComponent>
    <ChildContent>
        <button class="btn btn-primary" @onclick="ButtonClick">
            Click Me
        </button>
    </ChildContent>
</SimpleComponent>

页面也可以重写如下,这时谁拥有 RenderFragment 就更明显了。

@page "/simple"
<h3>SimplePage</h3>
@if (loaded)
{
    <SimpleComponent>
        @_childContent
    </SimpleComponent>
}
else
{
    <div class="h4 bg-danger text-white p-2">Loading.....</div>
}

@code {

    private bool loaded;

    protected async override Task OnInitializedAsync()
    {
        await Task.Delay(2000);
        loaded = true;
    }

    protected void ButtonClick(MouseEventArgs e)
    {
        var x = true;
    }

    private RenderFragment _childContent => (builder) =>
    {
        builder.OpenElement(0, "button");
        builder.AddAttribute(1, "class", "btn btn-primary");
        builder.AddAttribute(2, "onclick", 
        EventCallback.Factory.Create<MouseEventArgs>(this, ButtonClick));
        builder.AddContent(3, "Click Me");
        builder.CloseElement();
    };
}

一个组件不限于一个 RenderFragment。一个表组件可能看起来像这样

<TableComponent>
    <Header>
        ...
    </Header>
    <Rows>
        ...
    </Rows>
    <Footer>
        ...
    </Footer>
</TableComponent>

组件事件

关于组件事件,最重要的一点是它们不是即时执行即忘。默认情况下,所有事件都是异步的,并且看起来像这样

await calltheeventmethod
StateHasChanged();

因此,以下代码不会按预期执行

void async ButtonClick(MouseEventArgs e) 
{
  await Task.Delay(2000);
  UpdateADisplayProperty();
}

DisplayProperty 在另一个 StateHasChanged 事件发生之前不会显示当前值。为什么?ButtonClick 没有返回 Task,因此事件处理程序没有可以等待的内容。它在 UpdateADisplayProperty 完成之前运行 StateHasChanged

这是一个权宜之计 - 这是糟糕的做法。

void async ButtonClick(MouseEventArgs e) 
{
  await Task.Delay(2000);
  UpdateADisplayProperty();
  StateHasChanged();
}

正确的解决方案是

Task async ButtonClick(MouseEventArgs e) 
{
  await Task.Delay(2000);
  UpdateADisplayProperty();
}

现在事件处理程序有一个 Task 来等待,并且在 ButtonClick 完成之前不会执行 StateHasChanged

一些重要的、文档较少的信息和经验教训

保持参数属性简单

您的参数声明应如下所示

[Parameter] MyClass myClass {get; set;}

不要在 getter 或 setter 中添加代码。为什么?任何 setter 都必须作为渲染过程的一部分来运行,并且可能对渲染速度和组件状态产生重大影响。

重写 SetParametersAsync

如果您重写 SetParametersAsync,您的方法应该看起来像这样

    public override Task SetParametersAsync(ParameterView parameters)
    {
        // always call first
        parameters.SetParameterProperties(this);
        // Your Code
        .....
        // pass an empty ParameterView, not parameters
        return base.SetParametersAsync(ParameterView.Empty);
    }

在第一行设置参数,然后调用基方法并传递 ParameterView.Empty。不要尝试传递 parameters - 您将收到一个错误。

将参数视为不可变

切勿在代码中设置参数。如果您想进行或跟踪更改,请执行此操作

    [Parameter] public int MyParameter { get; set; }
    private int _MyParameter;
    public event EventHandler MyParameterChanged;

    public async override Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);
        if (!_MyParameter.Equals(MyParameter))
        {
            _MyParameter = MyParameter;
            MyParameterChanged?.Invoke(_MyParameter, EventArgs.Empty);
        }
        await base.SetParametersAsync(ParameterView.Empty);
    }

迭代器

当使用迭代器循环遍历集合以构建 select 或数据表时,会发生常见问题。下面是一个典型的例子

@for (var counter = 0; counter < this.myList.Count; counter++)
{
    <button class="btn btn-dark m-3" @onclick="() => ButtonClick
            (this.myList[counter])">@this.myList[counter]</button>
}
@for (var counter = 0; counter < this.myList.Count; counter++)
{
    <button class="btn btn-dark m-3" @onclick="() => ButtonClick
            (counter)">@this.myList[counter]</button>
}
<div>Value = @this.value </div>

@code {
    private List<int> myList => new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    private int value;

    private Task ButtonClick(int value)
    {
        this.value = value;
        return Task.CompletedTask;
    }
}

如果您单击第一行的按钮,您将收到一个 *Index was out of range* 错误。单击第二行的按钮,值始终是 10。原因是迭代器在您单击按钮之前就已经完成,此时 counter10

要解决此问题,请在循环内设置一个局部变量,如下所示

@for (var counter = 0; counter < this.myList.Count; counter++)
{
    var item = this.myList[counter];
    <button class="btn btn-dark m-3" @onclick="() => ButtonClick(item)">@item</button>
}
@for (var counter = 0; counter < this.myList.Count; counter++)
{
    var item = this.myList[counter];
    var thiscount = counter;
    <button class="btn btn-info m-3" @onclick="() => ButtonClick(thiscount)">@item</button>
}

最好的解决方案是使用 ForEach

@foreach  (var item in this.myList)
{
    <button class="btn btn-primary m-3" @onclick="() => ButtonClick(item)">@item</button>
}

组件编号

使用迭代器自动编号组件元素似乎是合乎逻辑的。不要这样做。编号系统由 diffing 引擎使用,以决定 DOM 的哪些部分需要更新,哪些部分不需要。编号必须在 RenderFragment 内保持一致。您可以使用 OpenRegionCloseRegion 来定义具有自己编号空间的区域。 有关更详细的解释,请参阅此 gist

构建组件

组件可以三种方式定义

  1. 作为带有 @code 块内代码的 .razor 文件。
  2. 作为 .razor 文件和一个代码隐藏文件 .razor.cs
  3. 作为纯 .cs 类文件,继承自 ComponentBase 或继承自 ComponentBase 的类,或实现 IComponent
全部在一个 Razor 文件中

HelloWorld.razor

<div>
@HelloWorld
</div>

@code {
[Parameter]
public string HelloWorld {get; set;} = "Hello?";
}
代码后置

HelloWorld.razor

@inherits ComponentBase
@namespace CEC.Blazor.Server.Pages

<div>
@HelloWorld
</div>

HelloWorld.razor.cs

namespace CEC.Blazor.Server.Pages
{
    public partial class HelloWorld : ComponentBase
    {
        [Parameter]
        public string HelloWorld {get; set;} = "Hello?";
    }
}
C# 类

HelloWorld.cs

namespace CEC.Blazor.Server.Pages
{
    public class HelloWorld : ComponentBase
    {
        [Parameter]
        public string HelloWorld {get; set;} = "Hello?";

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, "div");
            builder.AddContent(1, (MarkupString)this._Content);
            builder.CloseElement();
        }
    }
}

一些观察

  1. 人们倾向于将过多的代码堆积在 OnInitializedOnInitializedAsync 中,然后使用事件来驱动组件树中的 StateHasChanged 更新。将相关代码放在生命周期的正确位置,您将不需要事件。
  2. 人们有使用非异步版本的冲动(因为它们更容易实现),并且只在需要时才使用异步版本,而事实应该相反。大多数基于 Web 的活动本质上都是异步的。我从不使用非异步版本 - 我遵循的原则是,总有一天,我将需要添加异步行为。
  3. StateHasChanged 被调用得太频繁了,通常是因为代码在组件生命周期的错误位置,或者事件的编码不正确。在键入 StateHasChanged 时,请问自己一个具有挑战性的“为什么?”。
  4. UI 中的组件使用不足。相同的代码/标记块被重复使用。代码/标记块的规则与 C# 代码的规则相同。
  5. 一旦您真正、真正理解了组件,编写 Blazor 代码就会是一种完全“不同”的体验。

历史

  • 2020年8月25日:初始版本
  • 2021年6月16日:主要修订
© . All rights reserved.