目录
介绍
什么是组件?
渲染器和渲染树
客户端应用程序
Blazor服务器
Blazor Web Assembly
App.razor
组件
HelloWorld组件
一个简单的IComponent实现
路由组件
组件库
ComponentBase生命周期和事件
渲染过程
SimpleComponent.razor
组件内容
SimpleComponent.razor
SimplePage.razor
组件事件
一些重要的较少记录的信息和经验教训
保持参数属性简单
重写SetParametersAsync
将参数视为不可变的
迭代器
组件编号
构建组件
所有都在一个Razor文件
背后的代码
C# 类
一些观察
本文着眼于组件的剖析、其生命周期以及Blazor在构建和运行UI时如何使用和管理组件。
对组件的深入理解使开发Blazor应用程序成为一种非常不同的体验。
微软定义:
组件是用户界面(UI)的自包含部分,具有处理逻辑以启用动态行为。组件可以嵌套、重用、在项目之间共享,以及在MVC和Razor 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 Server在初始server/html页面中定义
type定义路由组件类——在这种情况下,App和render-mode定义初始服务器端渲染过程的运行方式。你可以在别处读到。唯一需要理解的重要一点是,如果它预渲染,页面在初始加载时会渲染两次——一次由服务器构建页面的静态版本,然后第二次由浏览器客户端代码构建页面的实时版本。
浏览器客户端代码通过以下方式加载:
一旦blazor.server.js加载了,客户端应用程序在浏览器页面,并与服务器建立SignalR连接而运行。为了完成初始加载,客户端应用程序调用Blazor中心会话并请求App组件的完整服务器呈现。然后它将结果DOM更改应用于客户端应用程序DOM——这主要是事件连接。
下图显示了渲染请求如何传递到显示页面:
在Blazor WebAssembly中,浏览器会收到一个HTML页面,其中包含应加载根组件的已定义div占位符:
....
客户端应用程序通过以下方式加载:
加载WASM代码后,它会运行program。
builder.RootComponents.Add("#app");
代码告诉Renderer,App类组件是RenderTree的根组件,并将其DOM加载到浏览器DOM中的app元素中。
从中得出的关键点是,尽管定义和加载根组件的过程不同,但WebAssembly和服务器根组件或任何子组件之间没有区别。您可以使用相同的组件。
App.razor是“标准”根组件。它可以是任何IComponent定义的类。
App 看起来像这样:
Sorry, there's nothing at this address.
它是一个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 提供机制:
一个RenderHandle结构:
回到IComponent接口:
请注意,IComponent没有RenderTree的概念。它通过调用SetParametersAsync被触发执行,并通过调用RenderHandle上的方法传递更改。
为了演示IComponent接口,我们将构建一个简单的HelloWorld组件。
我们最简单的Hello World Razor组件如下所示:
@page "/helloworld"
Hello World
这是一个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。
该模式规定您的方法必须:
回顾上面的代码,我们定义了一个RenderFragment属性并为其分配了一个符合RenderFragment模式的匿名方法。它需要RenderTreeBuilder并且没有返回所以返回void。它使用提供的RenderTreeBuilder对象来构建内容:一个简单的hello world html div。对构建器的每次调用都会添加所谓的RenderTreeFrame。 注意每一帧都是按顺序编号的。
了解两点很重要:
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();
}
}
}
上面代码中需要注意的地方:
当组件附加到渲染树时,该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生命周期方法。
有大量文章重复相同的旧基本生命周期信息。我不会重复它。相反,我将专注于生命周期中某些经常被误解的方面:生命周期还有更多内容,大多数文章中仅涵盖初始组件加载。
我们需要考虑五种类型的事件:
有七个公开的事件/方法及其异步等效项:
标准类的实例化方法构建RenderFragment该StateHasChanged传递到Renderer呈现组件。它将两个private类变量设置为false并运行BuildRenderTree。
public ComponentBase()
{
_renderFragment = builder =>
{
_hasPendingQueuedRender = false;
_hasNeverRendered = false;
BuildRenderTree(builder);
};
}
SetParametersAsync设置提交参数的属性。它只在初始化时运行RunInitAndSetParametersAsync——因此OnInitialized紧随其OnInitializedAsync后运行。它总是调用CallOnParametersSetAsync.。注意:
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;
}
}
}
需要注意的一些关键点:
让我们详细了解一个简单的页面和组件是如何呈现的。
Loaded
SimplePage.razor
@page "/simple"
SimplePage
@if (loaded)
{
}
else
{
Loading.....
}
@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为:
@ChildContent
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
}
@page "/simple"
SimplePage
@if (loaded)
{
}
else
{
Loading.....
}
@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——的对象的上下文中运行。
内容也可以这样定义:
该页面也可以重新编写如下,现在谁拥有RenderFragment。
@page "/simple"
SimplePage
@if (loaded)
{
@_childContent
}
else
{
Loading.....
}
@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(this, ButtonClick));
builder.AddContent(3, "Click Me");
builder.CloseElement();
};
}
一个组件不限于单个RenderFragment。表组件可能如下所示:
...
...
了解组件事件的最重要的一点是它们不是一劳永逸的。默认情况下,所有事件都是异步的,如下所示:
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,您的方法应如下所示:
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);
}
当使用For迭代器循环遍历集合以构建select或数据表时,会出现一个常见问题。一个典型的例子如下所示:
@for (var counter = 0; counter < this.myList.Count; counter++)
{
}
@for (var counter = 0; counter < this.myList.Count; counter++)
{
}
Value = @this.value
@code {
private List myList => new List { 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];
}
@for (var counter = 0; counter < this.myList.Count; counter++)
{
var item = this.myList[counter];
var thiscount = counter;
}
最好的解决方案是使用ForEach。
@foreach (var item in this.myList)
{
}
使用迭代器来自动化组件元素的编号似乎是合乎逻辑的。别这么做。比较引擎使用编号系统来决定DOM的哪些位需要更新,哪些位不需要。RenderFragment中的编号必须是一致的。您可以使用OpenRegion和CloseRegion来定义具有自己的数字空间的区域。有关更详细的解释,请参阅此要点。
可以通过三种方式定义组件:
HelloWorld.razor
@HelloWorld
@code {
[Parameter]
public string HelloWorld {get; set;} = "Hello?";
}
HelloWorld.razor
@inherits ComponentBase
@namespace CEC.Blazor.Server.Pages
@HelloWorld
HelloWorld.razor.cs
namespace CEC.Blazor.Server.Pages
{
public partial class HelloWorld : ComponentBase
{
[Parameter]
public string HelloWorld {get; set;} = "Hello?";
}
}
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();
}
}
}
https://www.codeproject.com/Articles/5277618/A-Dive-into-Blazor-Components