目录

介绍

什么是组件?

渲染器和渲染树

客户端应用程序

Blazor服务器

Blazor Web Assembly

App.razor

组件

HelloWorld组件

一个简单的IComponent实现

路由组件

组件库

ComponentBase生命周期和事件

渲染过程

SimpleComponent.razor

组件内容

SimpleComponent.razor

SimplePage.razor

组件事件

一些重要的较少记录的信息和经验教训

保持参数属性简单

重写SetParametersAsync

将参数视为不可变的

迭代器

组件编号

构建组件

所有都在一个Razor文件

背后的代码

C# 类

一些观察


介绍

本文着眼于组件的剖析、其生命周期以及Blazor在构建和运行UI时如何使用和管理组件。

对组件的深入理解使开发Blazor应用程序成为一种非常不同的体验。

什么是组件?

微软定义:

组件是用户界面(UI)的自包含部分,具有处理逻辑以启用动态行为。组件可以嵌套、重用、在项目之间共享,以及在MVCRazor Pages应用程序中使用。

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

它做了什么而不是它是什么,并且并非完全正确。

从编程的角度来看,组件只是一个实现IComponent接口的类而已。当它附加到RenderTree时,它就会变得生动起来,使用Renderer构建和更新组件树。UI IComponent接口是`Renderer`,它是用来与组件通信和接收来自组件的通信的接口。

在我们深入研究组件之前,我们需要查看Renderer和RenderTree,以及应用程序设置。

渲染器和渲染树

对Renderer和RenderTree工作原理的详细描述超出了本文的范围,但您需要基本掌握概念才能理解渲染过程。

在Renderer和RenderTree驻留在WASM的客户端应用程序和服务器的SignalR Hub会话中,即,每个连接的客户端应用程序。

UI——由DOM[文档对象模型]中的HTML代码定义——在应用程序中表示为 RenderTree并由Renderer.管理。可以将其RenderTree视为一棵树,每个分支都附有一个或多个组件。每个组件都是一个实现IComponent接口的C#类。该Renderer有运行的代码更新UI的RenderQueue。组件提交RenderFragments以供Renderer运行以更新RenderTree和UI。Renderer使用一个不同的进程来检测由RenderTree更新引起的DOM变化,并将这些变化传递给客户端代码,以便在浏览器DOM中实现并更新显示的页面。

下图是开箱即用的Blazor模板的渲染树的直观表示。

客户端应用程序

Blazor服务器

Blazor Server在初始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中心会话并请求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");

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

从中得出的关键点是,尽管定义和加载根组件的过程不同,但WebAssembly和服务器根组件或任何子组件之间没有区别。您可以使用相同的组件。

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组件,定义一个子组件Router。Router有两个RenderFragments,Found和NotFound。如果Router找到一个路由,因此找到一个IComponent类,它会渲染RouteView组件并将路由类类型和默认Layout类一起传递给它。如果没有找到路由,它会渲染LayoutView并在其Body中渲染定义的内容。

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

组件

所有组件都是普通的DotNetCore类,实现了IComponent接口。

该IComponent接口的定义是:

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

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

让我们看看更详细的定义。Blazor中心会话具有为每个根组件Renderer运行的RenderTree。从技术上讲,您可以拥有多个,但我们将在本次讨论中忽略这一点。引用类文档:

Renderer 提供机制:

  1. 用于呈现IComponent实例的层次结构
  2. 向他们发送事件
  3. 更新用户界面时通知

一个RenderHandle结构:

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

回到IComponent接口:

  1. 当Renderer将IComponent对象附加到RenderTree时调用Attach。它传递组件RenderHandle struct。组件使用此渲染句柄排队RenderFragments到Renderer的RenderQueue中。我们很快就会更详细地研究RenderFragement。
  2. SetParametersAsync在组件第一次将其附加到RenderTree以及它认为一个或多个组件Parameters发生更改时,由Renderer调用。

请注意,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. 有没有OnInitialized,OnAfterRender,StateHasChanged...这些都是ComponentBase的一部分。

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

该组件非常简单,但它演示了基础知识。

路由组件

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

它们包含@page路由指令和可选的@Layout指令。

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

您可以直接在类上定义这些:

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

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

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

  • 当路由组件的行为不像页面时会感到困惑。
  • 尝试编写组件逻辑,就像它是一个网页一样。

组件库

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

ComponentBase生命周期和事件

有大量文章重复相同的旧基本生命周期信息。我不会重复它。相反,我将专注于生命周期中某些经常被误解的方面:生命周期还有更多内容,大多数文章中仅涵盖初始组件加载。

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

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

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

  1. SetParametersAsync
  2. OnInitialized 和 OnInitializedAsync
  3. OnParametersSet 和 OnParametersSetAsync
  4. OnAfterRender 和 OnAfterRenderAsync
  5. Dispose——如果IDisposable被实现
  6. StateHasChanged
  7. new——经常被遗忘

标准类的实例化方法构建RenderFragment该StateHasChanged传递到Renderer呈现组件。它将两个private类变量设置为false并运行BuildRenderTree。

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

SetParametersAsync设置提交参数的属性。它只在初始化时运行RunInitAndSetParametersAsync——因此OnInitialized紧随其OnInitializedAsync后运行。它总是调用CallOnParametersSetAsync.。注意:

  1. CallOnParametersSetAsyncOnInitializedAsync在调用之前等待完成CallOnParametersSetAsync。
  2. 如果OnInitializedAsync任务在完成前产生则RunInitAndSetParametersAsync调用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到Renderer RenderQueue。当队列运行时_renderFragment——见上文——它将两个类标志设置为false并运行BuildRenderTree。

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

需要注意的一些关键点:

  1. OnInitialized和OnInitializedAsync仅在初始化期间被调用。OnInitialized首先运行。当且仅当OnInitializedAsync返回给内部调用方法RunInitAndSetParametersAsync,然后StateHasChanged被调用,提供向用户提供“加载”信息的机会。OnInitializedAsync在OnParametersSet并OnParametersSetAsync被调用之前完成。
  2. OnParametersSet和OnParametersSetAsync在父组件更改组件的参数集或捕获的级联参数更改时调用。任何需要响应参数变化的代码都需要在这里。OnParametersSet首先运行。请注意,如果OnParametersSetAsync产生,StateHasChanged则在产生后运行,提供向用户提供“加载”信息的机会。
  3. StateHasChanged在OnParametersSet{async}方法完成后调用以呈现组件。
  4. OnAfterRender和OnAfterRenderAsync在所有四个事件结束时发生。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控件的三个节点。

在我们的页面上,渲染树在第一次渲染时看起来像下图——我们有一个 yielding OnInitializedAsync方法,所以StateHasChanged在初始化过程中运行。

初始化完成后,StateHasChanged将运行第二次。现在Loaded是true并且SimpleComponent被添加到RenderFragment组件中。当Renderer运行RenderFragment,SimpleComponent被添加到渲染树,实例化和初始化。

组件内容

更改SimpleComponent和SimplePage为:

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期望在组件中找到命名为ChildContent的Parameter。标签之间的内容被预编译成一个 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等待并且在完成StateHasChanged之前不会执行ButtonClick。

一些重要的较少记录的信息和经验教训

保持参数属性简单

您的参数声明应如下所示:

[Parameter] MyClass myClass {get; set;}

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

重写SetParametersAsync

如果您重写SetParametersAsync,您的方法应如下所示:

public override Task SetParametersAsync(ParameterView parameters)
{// always call firstparameters.SetParameterProperties(this);// Your Code.....// pass an empty ParameterView, not parametersreturn 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);
}

迭代器

当使用For迭代器循环遍历集合以构建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。原因是在您单击按钮之前,迭代器已经完成,此时counter为10。

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

@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>
}

组件编号

使用迭代器来自动化组件元素的编号似乎是合乎逻辑的。别这么做。比较引擎使用编号系统来决定DOM的哪些位需要更新,哪些位不需要。RenderFragment中的编号必须是一致的。您可以使用OpenRegion和CloseRegion来定义具有自己的数字空间的区域。有关更详细的解释,请参阅此要点。

构建组件

可以通过三种方式定义组件:

  1. 作为在@code块内带有代码的.razor文件。
  2. 作为.razor文件和.razor.cs文件背后的代码。
  3. 作为继承自ComponentBase或ComponentBase继承类的纯.cs类文件,或实现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. 有一种倾向是在OnInitialized和OnInitializedAsync中堆积太多的代码,然后使用事件来驱动组件树的StateHasChanged更新。将相关代码放入生命周期中的正确位置,您就不需要事件了。
  2. 有一种诱惑是从非异步版本开始(因为它们更容易实现),并且仅在必须时才使用异步版本,而情况恰恰相反。大多数基于Web的活动本质上是异步的。我从不使用非异步版本——我的工作原则是在某些时候,我需要添加异步行为。
  3. StateHasChanged被频繁调用,通常是因为代码在组件生命周期中的位置错误,或者事件编码不正确。问自己一个具有挑战性的“为什么?” 当您键入StateHasChanged。
  4. 组件在UI中未得到充分利用。重复使用相同的代码/标记块。与C#代码相同的规则适用于代码/标记块。
  5. 一旦您真正地、真正地理解了组件,编写Blazor代码就会成为一种完全“不同”的体验。

https://www.codeproject.com/Articles/5277618/A-Dive-into-Blazor-Components

深入了解Blazor组件相关推荐

  1. Blazor 组件库开发指南

    翻译自 Waqas Anwar 2021年5月21日的文章 <A Developer's Guide To Blazor Component Libraries> [1] Blazor 的 ...

  2. Blazor 组件之间使用 EventCallback 进行通信

    翻译自 Waqas Anwar 2021年3月28日的文章 <Communication between Blazor Components using EventCallback> [1 ...

  3. 使用 Blazor 开发内部后台(二):了解 Blazor 组件

    James:  转载技术社区中一位朋友最新的文章,介绍自己为公司的 WebForm 遗留系统使用 Blazor 重写前端 UI 的经历. 什么是Blazor组件 Blazor 应用是使用 Razor ...

  4. Ant Design Blazor 组件库的路由复用多标签页介绍

    前言 Blazor 是 .NET 最新的前端框架,可以基于 WebAssembly 或 SignalR (WebSocket)构建前端应用程序,基于 WebAssembly 托管模型的 Blazor ...

  5. Blazor组件自做十二 : Blazor Pdf Reader PDF阅读器 组件

    原文链接 [https://www.cnblogs.com/densen2014/p/16954812.html] Blazor Pdf Reader PDF阅读器 组件 [外链图片转存失败,源站可能 ...

  6. Telerik UI for Blazor组件,可访问和可定制的组件

    Telerik UI for Blazor组件,可访问和可定制的组件 使用这个真正本机.完全可自定义的 Blazor UI 组件集合,将开发高性能应用程序的时间缩短一半. 适用于 Blazor 的 T ...

  7. Blazor组件自做四 : 使用JS隔离封装signature_pad签名组件

    运行截图 演示地址: 响应式: 感谢szimek写的棒棒的signature_pad.js项目, 来源: https://github.com/szimek/signature_pad 正式开始 1. ...

  8. Blazor组件自做十三: VideoPlayer 视频播放器

    Video.js 是一个具有大量功能的流行的视频和音频 JavaScript 库,今天我们试试集成到 Blazor . Blazor VideoPlayer 视频播放器 组件 [外链图片转存失败,源站 ...

  9. Blazor组件自做九 : 使用JS隔离制作蓝牙打印组件(通用跨平台隔空打印小票/标签方案)

    各位,好久不见,这段时间事情太多了,一直没空更新文章,sosososorry. 如果我告诉您网站能以安全和隐私保护的方式与附近的蓝牙设备进行通信,您会怎么想?如此一来,心率监测器.会唱歌的灯,甚至海龟 ...

最新文章

  1. 细说PHP中strlen和mb_strlen的区别(转)
  2. POJ1787多重背包中在最优方案下输出具体的选择方案
  3. Talend open studio数据导入、导出、同步Mysql、oracle、sqlserver简单案例
  4. Redis字符串操作
  5. python unit test 访问开发代码_python unittest 源码分析
  6. 敏捷开发生态系统系列之二:敏捷生态系统-计划跟踪 I(跨职能团队-共同估算-每日立会-同行压力)...
  7. 树的非递归(前序,中序,后序)
  8. 开发内功修炼CPU篇
  9. 最全整理:中国人工智能百强企业(100)榜单
  10. 联通4g满格但是网速慢_手机网速太慢怎么办 教你一招(4g信号满格网速很慢)...
  11. js压缩文件或文件夹
  12. 一文讲清楚什么是类型化数组、ArrayBuffer、TypedArray、DataView等概念
  13. 安卓电子书格式_纯干货|提升电子书阅读体验的四点感受
  14. VS 点击页面自动定位到解决方案资源管理器目录位置
  15. 乱舞之双刀--mhp2怪物猎人双刀攻略…
  16. 实现气泡效果的聊天框
  17. 学生台灯护眼灯哪个牌子好性价比高?学生护眼台灯十大牌子
  18. php 获取qq头像,php通过QQ号获取用户QQ昵称、QQ头像、QQ邮箱等信息!
  19. 基于STM32F469I-DISCOVERY的触屏版中国象棋
  20. 数据结构字符串模式匹配中计算next和nextval的值(C语言)

热门文章

  1. 实现根据条件删除_强大的定位空值法,1秒删除所有不想要的数据
  2. linux 防arp 带宽,linux下防arp
  3. C语言中定义整形可以连等吗,关于一道分解整数为N个连数整数的编程题
  4. eclipse支持html,让eclipse完全支持HTML/JS/CSS智能提示
  5. html 收藏功能,用react怎么实现收藏功能?
  6. 值得借鉴的促销海报模板素材,卖不出去都难
  7. 3D字体海报的这么玩?效果很赞,不得不学!
  8. 电商促销类插画素材,适合各种活动banner设计
  9. 手机APP夏季促销UI设计PSD模板|糖果色彩,抓住眼球
  10. html.textboxfor属性,label标签中的for属性与form属性