高大的灌汤包 · 利用matplotlib绘制多个实时刷新的动 ...· 1 年前 · |
俊秀的回锅肉 · 什么是Android逆向?如何学习安卓逆向? ...· 1 年前 · |
焦虑的灯泡 · 如何在不打开浏览器的情况下通过WebRTC从 ...· 1 年前 · |
面冷心慈的绿茶 · AutoGPT:如何安装和使用 - 知乎· 1 年前 · |
本文介绍如何在 Blazor 应用中创建和使用 Razor 组件,包括有关 Razor 语法、组件命名、命名空间和组件参数的指导。
Blazor 应用是使用 Razor 组件 (非正式地称为 Blazor 组件 )构建的。 组件是用户界面 (UI) 的自包含部分,具有用于启用动态行为的处理逻辑。 组件可以嵌套、重复使用、在项目间共享,并可 在 MVC 和 Razor Pages 应用中使用 。
组件是使用 C# 和 HTML 标记的组合在
Razor
组件文件(文件扩展名为
.razor
)中实现的。
组件使用
Razor 语法
。 组件广泛使用了两个 Razor 功能,即指令和指令特性 。 这两个功能是前缀为
@
的保留关键字,出现在 Razor 标记中:
@page
指令使用路由模板指定可路由组件,可以由用户请求在浏览器中按特定 URL 直接访问。
<input>
元素的
@bind
指令特性会将数据绑定到元素的值。
本文和 Blazor 文档集的其他文章中进一步说明了在组件中使用的指令和指令特性。 有关 Razor 语法的一般信息,请参阅 ASP.NET Core 的 Razor 语法参考 。
组件的名称必须以大写字符开头:
ProductDetail.razor
有效。
productDetail.razor
无效。
整个 Blazor 文档中使用的常见 Blazor 命名约定包括:
Pages/ProductDetail.razor
指示
ProductDetail
组件具有文件名
ProductDetail.razor
,并位于应用的
Pages
文件夹中。
/product-detail
请求具有路由模板
/product-detail
(
@page "/product-detail"
) 的
ProductDetail
组件。
†Pascal 大小写(大写 camel 形式)是不带空格和标点符号的命名约定,其中每个单词的首字母大写(包括第一个单词)。
可以通过使用
@page
指令为应用中的每个可访问组件提供路由模板来实现 Blazor 中的路由。 编译具有
@page
指令的 Razor 文件时,将为生成的类提供指定路由模板的
RouteAttribute
。 在运行时,路由器将使用
RouteAttribute
搜索组件类,并呈现具有与请求的 URL 匹配的路由模板的任何组件。
以下
HelloWorld
组件使用路由模板
/hello-world
。 可通过相对 URL
/hello-world
访问组件呈现的网页。 当使用默认协议、主机和端口在本地运行 Blazor 应用时,会在浏览器中通过
https://localhost:5001/hello-world
请求
HelloWorld
组件。 生成网页的组件通常位于
Pages
文件夹中,但可以使用任何文件夹保存组件(包括在嵌套文件夹中)。
Pages/HelloWorld.razor
:
@page "/hello-world"
<h1>Hello World!</h1>
前面的组件在浏览器中通过
/hello-world
进行加载,无论是否将组件添加到应用的 UI 导航。 (可选)组件可以添加到
NavMenu
组件,以便在应用基于 UI 的导航中显示组件链接。
对于前面的
HelloWorld
组件,可以将
NavLink
组件添加到
Shared
文件夹中的
NavMenu
组件。 有关详细信息(包括对
NavLink
和
NavMenu
组件的描述),请参阅
ASP.NET Core Blazor 路由和导航
。
组件的 UI 使用由 Razor 标记、C# 和 HTML 组成的
Razor 语法
进行定义。 在编译应用时,HTML 标记和 C# 呈现逻辑转换为组件类。 生成的类的名称与文件名匹配。
组件类的成员在一个或多个
@code
块中定义。 在
@code
块中,组件状态使用 C# 进行指定和处理:
属性和字段初始化表达式。
由父组件和路由参数传递的自变量的参数值。
用于用户事件处理、生命周期事件和自定义组件逻辑的方法。
组件成员使用以
@
符号开头的 C# 表达式在呈现逻辑中进行使用。 例如,通过为字段名称添加
@
前缀来呈现 C# 字段。 下面
Markup
示例计算并呈现:
headingFontStyle
,表示标题元素的 CSS 属性值
font-style
。
headingText
,表示标题元素的内容。
Pages/Markup.razor
:
@page "/markup"
<h1 style="font-style:@headingFontStyle">@headingText</h1>
@code {
private string headingFontStyle = "italic";
private string headingText = "Put on your new Blazor!";
Blazor 文档中的示例会为私有成员指定
private
访问修饰符
。 私有成员的范围限定为组件的类。 但是,C# 会在没有访问修饰符存在时采用
private
访问修饰符,因此在自己的代码中将成员显式标记为“
private
”是可选的。 有关访问修饰符的详细信息,请参阅
访问修饰符(C# 编程指南)
。
Blazor 框架在内部将组件作为
呈现树
进行处理,该树是组件的
文档对象模型 (DOM)
和
级联样式表对象模型 (CSSOM)
的组合。 最初呈现组件后,会重新生成组件的呈现树以响应事件。 Blazor 会将新呈现树与以前的呈现树进行比较,并将所有修改应用于浏览器的 DOM 以进行显示。 有关详细信息,请参阅
ASP.NET Core Razor 组件呈现
。
组件是普通
C# 类
,可以放置在项目中的任何位置。 生成网页的组件通常位于
Pages
文件夹中。 非页面组件通常放置在
Shared
文件夹或添加到项目的自定义文件夹中。
C# 控件结构、指令和指令属性的 Razor 语法需要小写(示例:
@if
、
@code
、
@bind
)。 属性名称需要大写(示例:
LayoutComponentBase.Body
的
@Body
)。
异步方法 (
async
) 不支持返回
void
Blazor 框架不跟踪返回
void
的异步方法 (
async
)。 因此,如果
void
返回,则不会捕获异常。 始终从异步方法返回
Task
。
通过使用 HTML 语法声明组件,组件可以包含其他组件。 使用组件的标记类似于 HTML 标记,其中标记的名称是组件类型。
请考虑以下
Heading
组件,其他组件可以使用该组件显示标题。
Shared/Heading.razor
:
<h1 style="font-style:@headingFontStyle">Heading Example</h1>
@code {
private string headingFontStyle = "italic";
HeadingExample
组件中的以下标记会在
<Heading />
标记出现的位置呈现前面的
Heading
组件。
Pages/HeadingExample.razor
:
@page "/heading-example"
<Heading />
如果某个组件包含一个 HTML 元素,该元素的大写首字母与相同命名空间中的组件名称不匹配,则会发出警告,指示该元素名称异常。 为组件的命名空间添加
@using
指令使组件可用,这可解决此警告。 有关详细信息,请参阅
命名空间
部分。
此部分中显示的
Heading
组件示例没有
@page
指令,因此用户无法在浏览器中通过直接请求直接访问
Heading
组件。 但是,具有
@page
指令的任何组件都可以嵌套在另一个组件中。 如果通过在 Razor 文件顶部包含
@page "/heading"
可直接访问
Heading
组件,则会在
/heading
和
/heading-example
处为浏览器请求呈现该组件。
通常,组件的命名空间是从应用的根命名空间和该组件在应用内的位置(文件夹)派生而来的。 如果应用的根命名空间是
BlazorSample
,并且
Counter
组件位于
Pages
文件夹中:
Counter
组件的命名空间为
BlazorSample.Pages
。
组件的完全限定类型名称为
BlazorSample.Pages.Counter
。
对于保存组件的自定义文件夹,将
@using
指令添加到父组件或应用的
_Imports.razor
文件。 下面的示例提供
Components
文件夹中的组件:
@using BlazorSample.Components
_Imports.razor
文件中的 @using
指令仅适用于 Razor 文件 (.razor
),而不适用于 C# 文件 (.cs
)。
还可以使用其完全限定的名称来引用组件,而不需要 @using
指令。 以下示例直接引用应用的 Components
文件夹中的 ProductDetail
组件:
<BlazorSample.Components.ProductDetail />
使用 Razor 创建的组件的命名空间基于以下内容(按优先级顺序):
Razor 文件标记中的 @namespace
指令(例如 @namespace BlazorSample.CustomNamespace
)。
项目文件中项目的 RootNamespace
(例如 <RootNamespace>BlazorSample</RootNamespace>
)。
项目名称,取自项目文件的文件名 (.csproj
),以及从项目根到组件的路径。 例如,框架将具有项目命名空间 BlazorSample
(BlazorSample.csproj
) 的 {PROJECT ROOT}/Pages/Index.razor
解析到 Index
组件的命名空间 BlazorSample.Pages
。 {PROJECT ROOT}
是项目根路径。 组件遵循 C# 名称绑定规则。 对于本示例中的 Index
组件,范围内的组件是所有组件:
- 在同一文件夹
Pages
中。
- 未显式指定其他命名空间的项目根中的组件。
不支持以下项目:
global::
限定。
- 导入具有别名
using
语句的组件。 例如,@using Foo = Bar
不受支持。
- 部分限定的名称。 例如,无法将
@using BlazorSample
添加到组件中,然后使用 <Shared.NavMenu></Shared.NavMenu>
在应用的 Shared
文件夹中引用 NavMenu
组件 (Shared/NavMenu.razor
)。
分部类支持
组件以 C# 分部类的形式生成,使用以下任一方法进行创作:
- 单个文件包含在一个或多个
@code
块、HTML 标记和 Razor 标记中定义的 C# 代码。 Blazor 项目模板使用此单文件方法来定义其组件。
- HTML 和 Razor 标记位于 Razor 文件 (
.razor
) 中。 C# 代码位于定义为分部类的代码隐藏文件 (.cs
) 中。
定义特定于组件的样式的组件样式表是单独的文件 (.css
)。 Blazor CSS 隔离稍后在 ASP.NET Core Blazor CSS 隔离 中进行介绍。
下面的示例显示了从 Blazor 项目模板生成的应用中具有 @code
块的默认 Counter
组件。 标记和 C# 代码位于同一个文件中。 这是在创作组件时采用的最常见方法。
Pages/Counter.razor
:
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
currentCount++;
以下 Counter
组件使用带有分部类的代码隐藏文件从 C# 代码中拆分 HTML 和 Razor 标记:
Pages/CounterPartialClass.razor
:
@page "/counter-partial-class"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
Pages/CounterPartialClass.razor.cs
:
namespace BlazorSample.Pages
public partial class CounterPartialClass
private int currentCount = 0;
void IncrementCount()
currentCount++;
_Imports.razor
文件中的 @using
指令仅适用于 Razor 文件 (.razor
),而不适用于 C# 文件 (.cs
)。 根据需要将命名空间添加到分部类文件中。
组件使用的典型命名空间:
using System.Net.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.JSInterop;
典型命名空间还包含应用的命名空间以及与应用的 Shared
文件夹对应的命名空间:
using BlazorSample;
using BlazorSample.Shared;
@inherits
指令用于指定组件的基类。 下面的示例演示组件如何继承基类以提供组件的属性和方法。 BlazorRocksBase
基类派生自 ComponentBase。
Pages/BlazorRocks.razor
:
@page "/blazor-rocks"
@inherits BlazorRocksBase
<h1>@BlazorRocksText</h1>
BlazorRocksBase.cs
:
using Microsoft.AspNetCore.Components;
namespace BlazorSample
public class BlazorRocksBase : ComponentBase
public string BlazorRocksText { get; set; } =
"Blazor rocks the browser!";
组件参数将数据传递给组件,使用组件类中包含 [Parameter]
特性的公共 C# 属性进行定义。 在下面的示例中,内置引用类型 (System.String) 和用户定义的引用类型 (PanelBody
) 作为组件参数进行传递。
PanelBody.cs
:
public class PanelBody
public string? Text { get; set; }
public string? Style { get; set; }
Shared/ParameterChild.razor
:
<div class="card w-25" style="margin-bottom:15px">
<div class="card-header font-weight-bold">@Title</div>
<div class="card-body" style="font-style:@Body.Style">
@Body.Text
@code {
[Parameter]
public string Title { get; set; } = "Set By Child";
[Parameter]
public PanelBody Body { get; set; } =
new()
Text = "Set by child.",
Style = "normal"
支持为组件参数提供初始值,但不会创建在首次呈现后向自身参数写入的组件。 有关详细信息,请参阅本文的重写参数部分。
ParameterChild
组件的 Title
和 Body
组件参数通过自变量在呈现组件实例的 HTML 标记中进行设置。 以下 ParameterParent
组件会呈现两个 ParameterChild
组件:
- 第一个
ParameterChild
组件在呈现时不提供参数自变量。
- 第二个
ParameterChild
组件从 ParameterParent
组件接收 Title
和 Body
的值,后者使用显式 C# 表达式设置 PanelBody
的属性值。
Pages/ParameterParent.razor
:
@page "/parameter-parent"
<h1>Child component (without attribute values)</h1>
<ParameterChild />
<h1>Child component (with attribute values)</h1>
<ParameterChild Title="Set by Parent"
Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
当 ParameterParent
组件未提供组件参数值时,来自 ParameterParent
组件的以下呈现 HTML 标记会显示 ParameterChild
组件默认值。 当 ParameterParent
组件提供组件参数值时,它们会替换 ParameterChild
组件的默认值。
为清楚起见,呈现 CSS 样式类未显示在以下呈现 HTML 标记中。
<h1>Child component (without attribute values)</h1>
<div>Set By Child</div>
<div>Set by child.</div>
<h1>Child component (with attribute values)</h1>
<div>Set by Parent</div>
<div>Set by parent.</div>
使用 Razor 的保留 @
符号,将方法的 C# 字段、属性或结果作为 HTML 特性值分配给组件参数。 以下 ParameterParent2
组件显示前面 ParameterChild
组件的四个实例,并将其 Title
参数值设置为:
title
字段的值。
GetTitle
C# 方法的结果。
- 带有ToLongDateString 的长格式当前本地日期,使用隐式 C# 表达式。
panelData
对象的 Title
属性。
字符串参数需要 @
前缀。 否则,框架假定设置了字符串字面量。
在字符串参数之外,我们建议对非文本使用 @
前缀,即使它们不是绝对必需的。
我们不建议对文本(例如,布尔值)、关键字(例如,this
)或 null
使用 @
前缀,但可以根据需要选择使用它们。 例如,IsFixed="@true"
不常见,但受支持。
在大多数情况下,根据 HTML5 规范,参数属性值的引号是可选的。 例如,支持 Value=this
,而不是 Value="this"
。 但是,我们建议使用引号,因为它更易于记住,并且在基于 Web 的技术中被广泛采用。
在整个文档中,代码示例:
- 始终使用引号。 示例:
Value="this"
。
- 非文本总是使用
@
前缀,即使它是可选的。 示例:Title="@title"
,其中 title
是字符串类型的变量。 Count="@ct"
,其中 ct
是数字类型的变量。
- Razor 表达式之外的文本始终避免使用
@
。 示例:IsFixed="true"
。
Pages/ParameterParent2.razor
:
@page "/parameter-parent-2"
<ParameterChild Title="@title" />
<ParameterChild Title="@GetTitle()" />
<ParameterChild Title="@DateTime.Now.ToLongDateString()" />
<ParameterChild Title="@panelData.Title" />
@code {
private string title = "From Parent field";
private PanelData panelData = new();
private string GetTitle()
return "From Parent method";
private class PanelData
public string Title { get; set; } = "From Parent object";
将 C# 成员分配给组件参数时,请使用符号 @
为成员添加前缀,绝不要为参数的 HTML 特性添加前缀。
<ParameterChild Title="@title" />
<ParameterChild @Title="title" />
与 Razor 页面 (.cshtml
) 不同,在呈现组件时,Blazor 不能在 Razor 表达式中执行异步工作。 这是因为 Blazor 是为呈现交互式 UI 而设计的。 在交互式 UI 中,屏幕必须始终显示某些内容,因此阻止呈现流是没有意义的。 相反,异步工作是在一个异步生命周期事件期间执行的。 在每个异步生命周期事件之后,组件可能会再次呈现。 不支持以下 Razor 语法:
<ParameterChild Title="@await ..." />
生成应用时,前面示例中的代码会生成编译器错误:
“await”运算符只能用于异步方法中。 请考虑用“async”修饰符标记此方法,并将其返回类型更改为“Task”。
若要在前面的示例中异步获取 Title
参数的值,组件可以使用 OnInitializedAsync
生命周期事件,如以下示例所示:
<ParameterChild Title="@title" />
@code {
private string? title;
protected override async Task OnInitializedAsync()
title = await ...;
有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
不支持使用显式 Razor 表达式连接文本和表达式结果以赋值给参数。 下面的示例尝试将文本“Set by
”与对象属性值连接在一起。 尽管 Razor 页面 (.cshtml
) 支持此语法,但对在组件中赋值给子级的 Title
参数无效。 不支持以下 Razor 语法:
<ParameterChild Title="Set by @(panelData.Title)" />
生成应用时,前面示例中的代码会生成编译器错误:
组件属性不支持复杂内容(混合 C# 和标记)。
若要支持组合值赋值,请使用方法、字段或属性。 下面的示例在 C# 方法 GetTitle
中将“Set by
”与对象属性值连接在一起:
Pages/ParameterParent3.razor
:
@page "/parameter-parent-3"
<ParameterChild Title="@GetTitle()" />
@code {
private PanelData panelData = new();
private string GetTitle() => $"Set by {panelData.Title}";
private class PanelData
public string Title { get; set; } = "Parent";
有关详细信息,请参阅 ASP.NET Core 的 Razor 语法参考。
支持为组件参数提供初始值,但不会创建在首次呈现后向自身参数写入的组件。 有关详细信息,请参阅本文的重写参数部分。
应将组件参数声明为自动属性,这意味着它们不应在其 get
或 set
访问器中包含自定义逻辑。 例如,下面的 StartData
属性是自动属性:
[Parameter]
public DateTime StartData { get; set; }
不要在 get
或 set
访问器中放置自定义逻辑,因为组件参数专门用作父组件向子组件传送信息的通道。 如果子组件属性的 set
访问器包含导致父组件重新呈现的逻辑,则会导致一个无限的呈现循环。
若要转换已接收的参数值,请执行以下操作:
- 将参数属性保留为自动属性,以表示所提供的原始数据。
- 创建另一个属性或方法,用于基于参数属性提供转换后的数据。
重写 OnParametersSetAsync
以在每次收到新数据时转换接收到的参数。
支持将初始值写入组件参数,因为初始值赋值不会干扰 Blazor 的自动组件呈现。 在组件中,使用 DateTime.Now 将当前本地 DateTime 赋予 StartData
是有效语法:
[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;
进行 DateTime.Now 的初始赋值之后,请勿在开发人员代码中向 StartData
赋值。 有关详细信息,请参阅本文的重写参数部分。
应用 [EditorRequired]
特性以指定所需的组件参数。 如果未提供参数值,编辑器或生成工具可能会向用户显示警告。 此特性仅在也用 [Parameter]
特性标记的属性上有效。 在设计时和生成应用时需强制使用 EditorRequiredAttribute。 在运行时则不强制使用该特性,因为它无法保证非 null
的参数值。
[Parameter]
[EditorRequired]
public string? Title { get; set; }
还支持单行特性列表:
[Parameter, EditorRequired]
public string? Title { get; set; }
组件参数和 RenderFragment
类型支持 Tuples
(API 文档)。 下面的组件参数示例在 Tuple
中传递三个值:
Shared/RenderTupleChild.razor
:
<div class="card w-50" style="margin-bottom:15px">
<div class="card-header font-weight-bold"><code>Tuple</code> Card</div>
<div class="card-body">
<li>Integer: @Data?.Item1</li>
<li>String: @Data?.Item2</li>
<li>Boolean: @Data?.Item3</li>
@code {
[Parameter]
public Tuple<int, string, bool>? Data { get; set; }
Pages/RenderTupleParent.razor
:
@page "/render-tuple-parent"
<h1>Render <code>Tuple</code> Parent</h1>
<RenderTupleChild Data="@data" />
@code {
private Tuple<int, string, bool> data = new(999, "I aim to misbehave.", true);
Razor 组件中的 C# 7.0 或更高版本仅支持未命名元组。 计划在未来的 ASP.NET Core 版本中支持 Razor 组件中的命名元组。 有关详细信息,请参阅命名元组的 Blazor 转译器问题 (dotnet/aspnetcore #28982)。
组件可以在 @page
指令的路由模板中指定路由参数。 Blazor 路由器使用路由参数来填充相应的组件参数。
支持可选路由参数。 在下面的示例中,text
可选参数将 route 段的值赋给组件的 Text
属性。 如果该段不存在,则在 OnInitialized
生命周期方法中将 Text
的值设置为 fantastic
。
Pages/RouteParameter.razor
:
@page "/route-parameter/{text?}"
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string? Text { get; set; }
protected override void OnInitialized()
Text = Text ?? "fantastic";
有关捕获跨越多个文件夹边界的路径的 catch-all 路由参数 ({*pageRoute}
) 的信息,请参阅 ASP.NET Core Blazor 路由和导航。
子内容呈现片段
组件可以设置另一个组件的内容。 分配组件提供子组件的开始标记与结束标记之间的内容。
在下面的示例中,RenderFragmentChild
组件具有一个 ChildContent
组件参数,它将要呈现的 UI 段表示为 RenderFragment。 ChildContent
在组件 Razor 标记中的位置是在最终 HTML 输出中呈现内容的位置。
Shared/RenderFragmentChild.razor
:
<div class="card w-25" style="margin-bottom:15px">
<div class="card-header font-weight-bold">Child content</div>
<div class="card-body">@ChildContent</div>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
必须按约定将接收 RenderFragment 内容的属性命名为 ChildContent
。
RenderFragment 不支持事件回叫。
以下 RenderFragmentParent
组件通过将内容置于子组件的开始标记和结束标记内,来提供用于呈现 RenderFragmentChild
的内容。
Pages/RenderFragmentParent.razor
:
@page "/render-fragment-parent"
<h1>Render child content</h1>
<RenderFragmentChild>
Content of the child component is supplied
by the parent component.
</RenderFragmentChild>
由于 Blazor 呈现子内容的方式,如果在 RenderFragmentChild
组件的内容中使用递增循环变量,则在 for
循环内呈现组件需要本地索引变量。 下面的示例可以添加到前面的 RenderFragmentParent
组件:
<h1>Three children with an index variable</h1>
@for (int c = 0; c < 3; c++)
var current = c;
<RenderFragmentChild>
Count: @current
</RenderFragmentChild>
或者,将 foreach
循环与 Enumerable.Range 结合使用,而不是使用 for
循环。 下面的示例可以添加到前面的 RenderFragmentParent
组件:
<h1>Second example of three children with an index variable</h1>
@foreach (var c in Enumerable.Range(0,3))
<RenderFragmentChild>
Count: @c
</RenderFragmentChild>
呈现片段用于在整个 Blazor 应用中呈现子内容,在下面的文章和文章部分中有示例介绍:
- Blazor 布局
- 跨组件层次结构传递数据
- 模板化组件
- 全局异常处理
Blazor 框架的内置 Razor 组件使用相同的 ChildContent
组件参数约定来设置其内容。 可以通过在 API 文档(使用搜索词“ChildContent”筛选 API)中搜索组件参数属性名称 ChildContent
来查看设置子内容的组件。
可重用呈现逻辑的呈现片段
你可以分解出子组件,纯粹作为重复使用呈现逻辑的方法。 在任何组件的 @code
块中,根据需要定义 RenderFragment 并呈现任意位置的片段:
<h1>Hello, world!</h1>
@RenderWelcomeInfo
<p>Render the welcome info a second time:</p>
@RenderWelcomeInfo
@code {
private RenderFragment RenderWelcomeInfo = __builder =>
<p>Welcome to your new app!</p>
有关详细信息,请参阅重复使用呈现逻辑。
Blazor 框架通常会施加安全的父级到子级参数的赋值:
- 不会意外覆盖参数。
- 最大程度地减少副作用。 例如,可避免附加的呈现,因为它们可能会创建无限的呈现循环。
当父组件重新呈现时,子组件会接收可能覆盖现有值的新参数值。 当开发带有一个或多个数据绑定参数的组件,并且开发人员直接写入子组件中的参数时,经常会发生意外覆盖子组件中的参数值:
- 子组件通过父组件中的一个或多个参数值呈现。
- 子级直接写入参数的值。
- 父组件重新呈现并覆盖子参数的值。
覆盖参数值的可能性也会延伸到子组件的属性 set
访问器中。
我们的通用指南不是创建在首次呈现后直接向自身参数写入的组件。
请考虑使用以下 Expander
组件,它们会:
- 呈现子内容。
- 切换以使用组件参数 (
Expanded
) 显示子内容。
在下面的 Expander
组件演示了已覆盖参数之后,会显示修改后的 Expander
组件以演示此方案的正确方法。 下面的示例可以放置在本地示例应中,以体验所述的行为。
Shared/Expander.razor
:
<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
<div class="card-body">
<h2 class="card-title">Toggle (<code>Expanded</code> = @Expanded)</h2>
@if (Expanded)
<p class="card-text">@ChildContent</p>
@code {
[Parameter]
public bool Expanded { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
private void Toggle()
Expanded = !Expanded;
Expander
组件会添加到可调用 StateHasChanged 的以下 ExpanderExample
父组件中:
- 在开发人员代码中调用 StateHasChanged 会向组件通知其状态已更改,并且通常会触发组件呈现以更新 UI。 StateHasChanged 稍后将在 ASP.NET Core Razor 组件生命周期和 ASP.NET Core Razor 组件呈现中详细介绍。
- 按钮的
@onclick
指令特性会将事件处理程序附加到按钮的 onclick
事件。 稍后在 ASP.NET Core Blazor 事件处理中更详细地介绍事件处理。
Pages/ExpanderExample.razor
:
@page "/expander-example"
<Expander Expanded="true">
Expander 1 content
</Expander>
<Expander Expanded="true" />
<button @onclick="StateHasChanged">
Call StateHasChanged
</button>
最初,在切换 Expanded
属性时,Expander
组件独立地作出行为。 子组件会按预期方式维护其状态。
如果在父组件中调用 StateHasChanged,则 Blazor 框架会重新呈现子组件(如果其参数可能已更改):
- 对于 Blazor 显式检查的一组参数类型,如果 Blazor 检测到有任何参数发生更改,则会重新呈现子组件。
- 对于未选中的参数类型,无论参数是否发生更改,Blazor 都会呈现子组件。 子内容属于此类参数类型,因为子内容属于 RenderFragment 类型,它是引用其他可变对象的委托。
对于 ExpanderExample
组件:
- 第一个
Expander
组件在可能可变的 RenderFragment 中设置子内容,因此在父组件中调用 StateHasChanged 会自动重新呈现该组件,并可能将 Expanded
的值覆盖为其初始值 true
。
- 第二个
Expander
组件未设置子内容。 因此,不存在可能可变的 RenderFragment。 在父组件中调用 StateHasChanged 不会自动呈现子内容,因此组件的 Expanded
值不会被覆盖。
要维持在前述情况中的状态,请在 Expander
组件中使用私有字段来保留它的切换状态。
以下经修定的 Expander
组件:
- 接受父项中的
Expanded
组件参数值。
- 将组件参数值分配给
OnInitialized
事件中的私有字段 (expanded
)。
- 使用私有字段来维护其内部切换状态,该状态演示如何避免直接写入参数。
此部分中的建议可扩展到组件参数 set
访问器中的类似逻辑,这可能会产生类似的不良副作用。
Shared/Expander.razor
:
<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
<div class="card-body">
<h2 class="card-title">Toggle (<code>expanded</code> = @expanded)</h2>
@if (expanded)
<p class="card-text">@ChildContent</p>
@code {
private bool expanded;
[Parameter]
public bool Expanded { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
expanded = Expanded;
private void Toggle()
expanded = !expanded;
有关双向父子绑定示例,请参阅 ASP.NET Core Blazor 绑定。 有关其他信息,请参阅 Blazor 双向绑定错误 (dotnet/aspnetcore #24599)。
有关更改检测的详细信息,包括有关 Blazor 检查的完全匹配类型的信息,请参阅 ASP.NET Core Razor 组件呈现。
属性展开和任意参数
除了组件的声明参数外,组件还可以捕获和呈现其他属性。 其他特性可以在字典中捕获,然后在使用 @attributes
Razor 指令特性呈现组件时,将其展开到元素上。 对于定义生成支持各种自定义项的标记元素的组件,此方案非常有用。 例如,为支持多个参数的 <input>
单独定义属性可能比较繁琐。
在下面的 Splat
组件中:
- 第一个
<input>
元素 (id="useIndividualParams"
) 使用单个组件参数。
- 第二个
<input>
元素 (id="useAttributesDict"
) 使用特性展开。
Pages/Splat.razor
:
@page "/splat"
<input id="useIndividualParams"
maxlength="@maxlength"
placeholder="@placeholder"
required="@required"
size="@size" />
<input id="useAttributesDict"
@attributes="InputAttributes" />
@code {
private string maxlength = "10";
private string placeholder = "Input placeholder text";
private string required = "required";
private string size = "50";
private Dictionary<string, object> InputAttributes { get; set; } =
new()
{ "maxlength", "10" },
{ "placeholder", "Input placeholder text" },
{ "required", "required" },
{ "size", "50" }
网页中呈现的 <input>
元素是相同的:
<input id="useIndividualParams"
maxlength="10"
placeholder="Input placeholder text"
required="required"
size="50">
<input id="useAttributesDict"
maxlength="10"
placeholder="Input placeholder text"
required="required"
size="50">
若要接受任意特性,请定义组件参数,并将 CaptureUnmatchedValues 属性设置为 true
:
@code {
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? InputAttributes { get; set; }
[Parameter]
上的 CaptureUnmatchedValues 属性允许参数匹配所有不匹配任何其他参数的特性。 组件只能使用 CaptureUnmatchedValues 定义单个参数。 与 CaptureUnmatchedValues 一起使用的属性类型必须可以使用字符串键从 Dictionary<string, object>
中分配。 使用 IEnumerable<KeyValuePair<string, object>>
或 IReadOnlyDictionary<string, object>
也是此方案中的选项。
相对于元素特性位置的 @attributes
位置很重要。 在元素上展开 @attributes
时,将从右到左(从最后一个到第一个)处理特性。 请考虑以下使用子组件的父组件示例:
Shared/AttributeOrderChild1.razor
:
<div @attributes="AdditionalAttributes" extra="5" />
@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }
Pages/AttributeOrderParent1.razor
:
@page "/attribute-order-parent-1"
<AttributeOrderChild1 extra="10" />
AttributeOrderChild1
组件的 extra
属性设置为 @attributes
右侧。 通过附加特性传递时,AttributeOrderParent1
组件的呈现的 <div>
包含 extra="5"
,因为特性是从右到左(从最后一个到第一个)处理的:
<div extra="5" />
在下面的示例中,extra
和 @attributes
的顺序在子组件的 <div>
中反转:
Shared/AttributeOrderChild2.razor
:
<div extra="5" @attributes="AdditionalAttributes" />
@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }
Pages/AttributeOrderParent2.razor
:
@page "/attribute-order-parent-2"
<AttributeOrderChild2 extra="10" />
通过附加特性传递时,父组件呈现的网页中的 <div>
包含 extra="10"
:
<div extra="10" />
捕获对组件的引用
组件引用提供了一种引用组件实例以便发出命令的方法。 若要捕获组件引用,请执行以下操作:
- 向子组件添加
@ref
特性。
- 定义与子组件类型相同的字段。
呈现组件时,将用组件实例填充字段。 然后,可以在实例上调用 .NET 方法。
请考虑以下 ReferenceChild
组件,它会在调用其 ChildMethod
时记录消息。
Shared/ReferenceChild.razor
:
@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> logger
@code {
public void ChildMethod(int value)
logger.LogInformation("Received {Value} in ChildMethod", value);
组件引用仅在呈现组件后才进行填充,其输出包含 ReferenceChild
的元素。 在呈现组件之前,没有任何可引用的内容。
若要在组件完成呈现后操作组件引用,请使用 OnAfterRender
或 OnAfterRenderAsync
方法。
若要结合使用事件处理程序和引用变量,请使用 Lambda 表达式,或在 OnAfterRender
或 OnAfterRenderAsync
方法中分配事件处理程序委托。 这可确保在分配事件处理程序之前先分配引用变量。
以下 lambda 方法使用前面的 ReferenceChild
组件。
Pages/ReferenceParent1.razor
:
@page "/reference-parent-1"
<button @onclick="@(() => childComponent?.ChildMethod(5))">
Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>
<ReferenceChild @ref="childComponent" />
@code {
private ReferenceChild? childComponent;
以下委托方法使用前面的 ReferenceChild
组件。
Pages/ReferenceParent2.razor
:
@page "/reference-parent-2"
<button @onclick="@(() => callChildMethod?.Invoke())">
Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>
<ReferenceChild @ref="childComponent" />
@code {
private ReferenceChild? childComponent;
private Action? callChildMethod;
protected override void OnAfterRender(bool firstRender)
if (firstRender)
callChildMethod = CallChildMethod;
private void CallChildMethod()
childComponent?.ChildMethod(5);
尽管捕获组件引用使用与捕获元素引用类似的语法,但捕获组件引用不是 JavaScript 互操作功能。 组件引用不会传递给 JavaScript 代码。 组件引用只在 .NET 代码中使用。
不要使用组件引用来改变子组件的状态。 请改用常规声明性组件参数将数据传递给子组件。 使用组件参数使子组件在正确的时间自动重新呈现。 有关详细信息,请参阅组件参数部分和 ASP.NET Core Blazor 数据绑定一文。
同步上下文
Blazor 使用同步上下文 (SynchronizationContext) 来强制执行单个逻辑线程。 组件的生命周期方法和 Blazor 引发的事件回调都在此同步上下文上执行。
Blazor Server的同步上下文尝试模拟单线程环境,使其与浏览器中的单线程 WebAssembly 模型紧密匹配。 在任意给定的时间点,工作只在一个线程上执行,这会造成单个逻辑线程的印象。 不会同时执行两个操作。
避免阻止线程的调用
通常,不要在组件中调用以下方法。 以下方法阻止执行线程,进而阻止应用继续工作,直到基础 Task 完成:
- Result
- WaitAny
- WaitAll
- Sleep
- GetResult
使用此部分中所述的线程阻止方法的 Blazor 文档示例只是使用方法进行演示,而不是用作建议编码指导。 例如,一些组件代码演示通过调用 Thread.Sleep 来模拟长时间运行的进程。
在外部调用组件方法以更新状态
如果组件必须根据外部事件(如计时器或其他通知)进行更新,请使用 InvokeAsync
方法,它将代码执行调度到 Blazor 的同步上下文。 例如,请考虑以下通告程序服务,它可向任何侦听组件通知更新的状态。 可以从应用中的任何位置调用 Update
方法。
TimerService.cs
:
public class TimerService : IDisposable
private int elapsedCount;
private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private PeriodicTimer? timer;
public TimerService(NotifierService notifier,
ILogger<TimerService> logger)
this.notifier = notifier;
this.logger = logger;
public async Task Start()
if (timer is null)
timer = new(heartbeatTickRate);
logger.LogInformation("Started");
using (timer)
while (await timer.WaitForNextTickAsync())
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation($"elapsedCount: {elapsedCount}");
public void Dispose()
timer?.Dispose();
NotifierService.cs
:
public class NotifierService
public async Task Update(string key, int value)
if (Notify != null)
await Notify.Invoke(key, value);
public event Func<string, int, Task>? Notify;
注册服务:
在 Blazor WebAssembly 应用中,在 Program.cs
中将服务注册为单一实例:
builder.Services.AddSingleton<NotifierService>();
builder.Services.AddSingleton<TimerService>();
在 Blazor Server 应用中,在 Program.cs
中将服务注册为有作用域:
builder.Services.AddScoped<NotifierService>();
builder.Services.AddScoped<TimerService>();
使用 NotifierService
更新组件。
Pages/ReceiveNotifications.razor
:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
Status:
@if (lastNotification.key is not null)
<span>@lastNotification.key = @lastNotification.value</span>
<span>Awaiting first notification</span>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
await InvokeAsync(() =>
lastNotification = (key, value);
StateHasChanged();
private async Task StartTimer()
await Timer.Start();
public void Dispose()
Notifier.Notify -= OnNotify;
在上面的示例中:
NotifierService
在 Blazor 的同步上下文之外调用组件的 OnNotify
方法。 InvokeAsync
用于切换到正确的上下文,并将呈现排入队列。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现。
- 组件会实现 IDisposable。
OnNotify
委托在 Dispose
方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
使用 @key
控制是否保留元素和组件
在呈现元素或组件的列表并且元素或组件随后发生更改时,Blazor 必须决定之前的元素或组件中有哪些可以保留,以及模型对象应如何映射到这些元素或组件。 通常,此过程是自动的,可以忽略,但在某些情况下,可能需要控制该过程。
请考虑使用以下 Details
和 People
组件:
Details
组件从 People
父组件接收数据 Data
,这些数据会显示在 <input>
元素中。 当用户选择一个 <input>
元素时,显示的任何给定 <input>
元素都可以从用户接收页面焦点。
People
组件使用 Details
组件创建人员对象列表以进行显示 。 每三秒向集合中添加一个新人员。
此演示使你可以:
- 从多个呈现的
Details
组件中选择一个 <input>
。
- 随着人员集合自动增长,研究页面焦点的行为。
Shared/Details.razor
:
<input value="@Data" />
@code {
[Parameter]
public string? Data { get; set; }
在以下 People
组件中,在 OnTimerCallback
中添加人员的每个迭代都会导致 Blazor 重新生成整个集合。 页面的焦点保持在 <input>
元素的相同索引位置处,因此每次添加人员时,焦点都会移动。 将焦点从用户选择的内容移开是不可取的行为。 使用以下组件演示不良行为后,使用 @key
指令特性改善用户的体验。
Pages/People.razor
:
@page "/people"
@using System.Timers
@implements IDisposable
@foreach (var person in people)
<Details Data="@person.Data" />
@code {
private Timer timer = new Timer(3000);
public List<Person> people =
new()
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
protected override void OnInitialized()
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
private void OnTimerCallback()
_ = InvokeAsync(() =>
people.Insert(0,
new Person
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
StateHasChanged();
public void Dispose() => timer.Dispose();
public class Person
public string? Data { get; set; }
people
集合的内容会随插入、删除或重新排序的条目而更改。 重新呈现可能会导致可见的行为差异。 每次向 people
集合中插入一个人员时,当前具有焦点的元素的前一个元素便会接收焦点。 用户的焦点会丢失。
可通过 @key
指令特性来控制元素或组件到集合的映射过程。 使用 @key
可保证基于键的值保留元素或组件。 如果前面示例中的 Details
组件在 person
项上键入,则 Blazor 会忽略重新呈现未更改的 Details
组件。
若要修改 People
组件以将 @key
指令特性用于 people
集合,请将 <Details>
元素更新为以下内容:
<Details @key="person" Data="@person.Data" />
当 people
集合发生更改时,会保留 Details
实例与 person
实例之间的关联。 当在集合的开头插入 Person
时,会在相应位置插入一个新的 Details
实例。 其他实例保持不变。 因此,在将人员添加到集合时,用户的焦点不会丢失。
使用 @key
指令特性时,其他集合更新会表现出相同的行为:
- 如果从集合中删除实例,则仅从 UI 中删除相应的组件实例。 其他实例保持不变。
- 如果对集合项进行重新排序,则保留相应的组件实例,并在 UI 中重新排序。
对于每个容器元素或组件而言,键是本地的。 不会在整个文档中全局地比较键。
何时使用 @key
通常,每当呈现列表(例如,在 foreach
块中)以及存在适当的值定义 @key
时,都可以使用 @key
。
在对象未更改时,也可以使用 @key
保留元素或组件子树,如下面的示例所示。
示例 1:
<li @key="person">
<input value="@person.Data" />
示例 2:
<div @key="person">
@* other HTML elements *@
如果 person
实例更改,则 @key
特性指令会强制 Blazor:
- 放弃整个
<li>
或 <div>
及其后代。
- 在 UI 中使用新元素和组件重新生成子树。
这有助于保证在子树中的集合发生更改时不保留 UI 状态。
@key
的范围
@key
属性指令的范围是其父级中自己的同级。
请考虑以下示例。 first
和 second
键在外部 <div>
元素的同一范围内相互比较:
<div @key="first">...</div>
<div @key="second">...</div>
以下示例演示位于其自己的范围内的 first
键和 second
键,它们彼此不相关且彼此没有影响。 每个 @key
范围仅适用于其父级 <div>
元素,不适用于整个父级 <div>
元素:
<div @key="first">...</div>
<div @key="second">...</div>
对于上述 Details
组件,以下示例显示位于同一 @key
范围的 person
数据,并演示 @key
的典型用例:
@foreach (var person in people)
<Details @key="person" Data="@person.Data" />
@foreach (var person in people)
<div @key="person">
<Details Data="@person.Data" />
@foreach (var person in people)
<li @key="person">
<Details Data="@person.Data" />
以下示例将@key
的范围仅设为围绕每个 Details
组件实例的 <div>
或 <li>
元素。 因此,people
集合的每个成员的 person
数据的键不指向所呈现的 Details
组件中的每个 person
实例。 使用 @key
时,请避免以下模式:
@foreach (var person in people)
<Details @key="person" Data="@person.Data" />
@foreach (var person in people)
<Details @key="person" Data="@person.Data" />
何时不使用 @key
使用 @key
进行呈现时,会产生性能成本。 性能成本不是很大,但仅在保留元素或组件对应用有利的情况下才指定 @key
。
即使未使用 @key
,Blazor 也会尽可能地保留子元素和组件实例。 使用 @key
的唯一优点是控制如何将模型实例映射到保留的组件实例,而不是选择映射的 Blazor。
要用于 @key
的值
通常,为 @key
提供以下值之一:
- 模型对象实例。 例如,在前面的示例中使用了
Person
实例 (person
)。 这可确保基于对象引用相等性的保留。
- 唯一标识符。 例如,唯一标识符可以基于类型为
int
、string
或 Guid
的主键值。
确保用于 @key
的值不冲突。 如果在同一父元素内检测到冲突值,则 Blazor 引发异常,因为它无法明确地将旧元素或组件映射到新元素或组件。 仅使用非重复值,例如对象实例或主键值。
可以通过 @attribute
指令将特性应用于组件。 下面的示例将 [Authorize]
特性应用于组件的类:
@page "/"
@attribute [Authorize]
条件 HTML 元素属性
HTML 元素特性属性基于 .NET 值有条件地设置。 如果值为 false
或 null
,则属性未设置。 如果值为 true
,则属性已设置。
在下面的示例中,IsCompleted
确定是否设置了 <input>
元素的 checked
属性。
Pages/ConditionalAttribute.razor
:
@page "/conditional-attribute"
<label>
<input type="checkbox" checked="@IsCompleted" />
Is Completed?
</label>
<button @onclick="@(() => IsCompleted = !IsCompleted)">
Change IsCompleted
</button>
@code {
[Parameter]
public bool IsCompleted { get; set; }
有关详细信息,请参阅 ASP.NET Core 的 Razor 语法参考。
.NET 类型为 bool
时,某些 HTML 属性(如 aria-pressed
)无法正常运行。 在这些情况下,请使用 string
类型,而不是 bool
。
原始 HTML
通常使用 DOM 文本节点呈现字符串,这意味着将忽略它们可能包含的任何标记,并将其视为文字文本。 若要呈现原始 HTML,请将 HTML 内容包装在 MarkupString 值中。 将该值分析为 HTML 或 SVG,并插入到 DOM 中。
呈现从任何不受信任的源构造的原始 HTML 存在安全风险,应始终避免 。
下面的示例演示如何使用 MarkupString 类型向组件的呈现输出添加静态 HTML 内容块。
Pages/MarkupStringExample.razor
:
@page "/markup-string-example"
@((MarkupString)myMarkup)
@code {
private string myMarkup =
"<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
Razor 模板
可以使用 Razor 模板语法定义呈现片段,从而定义 UI 片段。 Razor 模板使用以下格式:
@<{HTML tag}>...</{HTML tag}>
下面的示例演示如何在组件中指定 RenderFragment 和 RenderFragment<TValue> 值并直接呈现模板。 还可以将呈现片段作为参数传递给模板化组件。
Pages/RazorTemplate.razor
:
@page "/razor-template"
@timeTemplate
@petTemplate(new Pet { Name = "Nutty Rex" })
@code {
private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;
private class Pet
public string? Name { get; set; }
以上代码的呈现输出:
<p>The time is 4/19/2021 8:54:46 AM.</p>
<p>Pet: Nutty Rex</p>
Blazor 遵循 ASP.NET Core 应用对于静态资产的约定。 静态资产位于项目的 web root
(wwwroot
) 文件夹中或是 wwwroot
文件夹下的文件夹中。
使用基相对路径 (/
) 来引用静态资产的 Web 根。 在下面的示例中,logo.png
实际位于 {PROJECT ROOT}/wwwroot/images
文件夹中。 {PROJECT ROOT}
是应用的项目根。
<img alt="Company logo" src="/images/logo.png" />
组件不支持波浪符斜杠表示法 (~/
)。
有关设置应用基本路径的信息,请参阅托管和部署 ASP.NET Core Blazor。
组件中不支持标记帮助程序
组件中不支持 Tag Helpers
。 若要在 Blazor 中提供类似标记帮助程序的功能,请创建一个具有与标记帮助程序相同功能的组件,并改为使用该组件。
可缩放的向量图形 (SVG) 图像
由于 Blazor 呈现 HTML,因此通过 <img>
标记支持浏览器支持的图像,包括可缩放的矢量图形 (SVG) 图像 (.svg
):
<img alt="Example image" src="image.svg" />
同样,样式表文件 (.css
) 的 CSS 规则支持 SVG 图像:
.element-class {
background-image: url("image.svg");
Blazor 支持 <foreignObject>
元素在 SVG 中显示任意 HTML。 标记可表示任意 HTML、RenderFragment 或 Razor 组件。
以下示例演示了:
string
(@message
) 的显示情况。
- 具有
<input>
元素和 value
字段的双向绑定。
Robot
组件。
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" rx="10" ry="10" width="200" height="200" stroke="black"
fill="none" />
<foreignObject x="20" y="20" width="160" height="160">
<p>@message</p>
</foreignObject>
<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="200" height="200">
<label>
Two-way binding:
<input @bind="value" @bind:event="oninput" />
</label>
</foreignObject>
<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject>
<Robot />
</foreignObject>
@code {
private string message = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
"elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
private string? value;
空白的呈现行为
除非将 @preservewhitespace
指令与值 true
一起使用,否则在以下情况下默认删除额外的空白:
- 元素中的前导或尾随空白。
- RenderFragment/RenderFragment<TValue> 参数中的前导或尾随(例如,传递到另一个组件的子内容)。
- 在 C# 代码块(例如
@if
和 @foreach
)之前或之后。
但是,在使用 CSS 规则(例如 white-space: pre
)时,删除空白可能会影响呈现输出。 若要禁用此性能优化并保留空白,请执行以下任一操作:
- 将
@preservewhitespace true
指令添加到 Razor 文件 (.razor
) 的顶部,从而将首选项应用于特定组件。
- 将
@preservewhitespace true
指令添加到 _Imports.razor
文件中,从而将首选项应用于子目录或整个项目。
在大多数情况下,不需要执行任何操作,因为应用程序通常会继续正常运行(但速度会更快)。 如果去除空白会导致特定组件出现呈现问题,请在该组件中使用 @preservewhitespace true
来禁用此优化。
泛型类型参数支持
@typeparam
指令声明生成的组件类的泛型类型参数:
@typeparam TItem
支持具有 where
类型约束的 C# 语法:
@typeparam TEntity where TEntity : IEntity
在下面的示例中,ListGenericTypeItems1
组件会泛型类型化为 TExample
。
Shared/ListGenericTypeItems1.razor
:
@typeparam TExample
@if (ExampleList is not null)
@foreach (var item in ExampleList)
<li>@item</li>
@code {
[Parameter]
public IEnumerable<TExample>? ExampleList{ get; set; }
以下 GenericTypeExample1
组件会呈现两个 ListGenericTypeItems1
组件:
- 字符串或整数数据将分配给每个组件的
ExampleList
参数。
- 会为每个组件的类型参数 (
TExample
) 设置与分配的数据类型匹配的类型 string
或 int
。
Pages/GenericTypeExample1.razor
:
@page "/generic-type-example-1"
<h1>Generic Type Example 1</h1>
<ListGenericTypeItems1 ExampleList="@(new List<string> { "Item 1", "Item 2" })"
TExample="string" />
<ListGenericTypeItems1 ExampleList="@(new List<int> { 1, 2, 3 })"
TExample="int" />
有关详细信息,请参阅 ASP.NET Core 的 Razor 语法参考。 有关使用模板化组件进行泛型类型化的示例,请参阅 ASP.NET Core Blazor 模板化组件。
级联的泛型类型支持
上级组件可以使用 [CascadingTypeParameter]
特性将类型参数按名称级联到下级。 此特性允许泛型类型推理自动使用指定的类型参数以及具有相同名称的类型参数的下级。
通过将 @attribute [CascadingTypeParameter(...)]
添加到组件中,符合以下条件的下级会自动使用指定的泛型类型参数:
- 在同一个
.razor
文档中嵌套为组件的子内容。
- 同时还使用完全相同的名称声明了
@typeparam
。
- 没有为类型参数显式提供或隐式推断另一个值。 如果提供或推断了其他值,则其优先于级联泛型类型。
接收级联类型参数时,组件从具有 CascadingTypeParameterAttribute 和一个匹配名称的最接近的上级中获取参数值。 级联泛型类型参数在特定子树内被重写。
匹配只按名称进行。 因此,建议使用通用名称(例如 T
或 TItem
)来避免使用级联泛型类型参数。 如果开发人员选择级联一个类型参数,则会隐式承诺其名称是唯一的,不会与来自无关组件的其他级联类型参数冲突。
可以通过以下任一使用上级(父)组件的方法将泛型类型级联到子组件,如以下两个子部分中所示:
- 显式设置级联的泛型类型。
- 推断级联的泛型类型。
以下子部分使用以下两个 ListDisplay
组件提供了上述方法的示例。 组件接收和呈现列表数据,并泛型类型化为 TExample
。 这些组件用于演示目的,只是在呈现列表的文本颜色方面有所不同。 如果你想要在本地测试应用中试验以下子部分中的组件,请先将以下两个组件添加到应用中。
Shared/ListDisplay1.razor
:
@typeparam TExample
@if (ExampleList is not null)
<ul style="color:blue">
@foreach (var item in ExampleList)
<li>@item</li>
@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }
Shared/ListDisplay2.razor
:
@typeparam TExample
@if (ExampleList is not null)
<ul style="color:red">
@foreach (var item in ExampleList)
<li>@item</li>
@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }
基于上级组件的显式泛型类型
本部分中的演示为 TExample
显式级联了一个类型。
本部分使用级联的泛型类型支持部分中的两个 ListDisplay
组件。
以下 ListGenericTypeItems2
组件接收数据,并将名为 TExample
的泛型类型参数级联到其下级组件。 在即将到来的父组件中,ListGenericTypeItems2
组件用于显示前面 ListDisplay
组件的列表数据。
Shared/ListGenericTypeItems2.razor
:
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample
<h2>List Generic Type Items 2</h2>
@ChildContent
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
下面的 GenericTypeExample2
父组件将设置指定 ListGenericTypeItems2
类型 (TExample
) 的两个 ListGenericTypeItems2
组件的子内容 (RenderFragment),这些类型会级联到子组件。 ListDisplay
组件呈现为示例中所示的列表项数据。 字符串数据与第一个 ListGenericTypeItems2
组件一起使用,整数数据与第二个 ListGenericTypeItems2
组件一起使用。
Pages/GenericTypeExample2.razor
:
@page "/generic-type-example-2"
<h1>Generic Type Example 2</h1>
<ListGenericTypeItems2 TExample="string">
<ListDisplay1 ExampleList="@(new List<string> { "Item 1", "Item 2" })" />
<ListDisplay2 ExampleList="@(new List<string> { "Item 3", "Item 4" })" />
</ListGenericTypeItems2>
<ListGenericTypeItems2 TExample="int">
<ListDisplay1 ExampleList="@(new List<int> { 1, 2, 3 })" />
<ListDisplay2 ExampleList="@(new List<int> { 4, 5, 6 })" />
</ListGenericTypeItems2>
显式指定类型还允许使用级联值和参数向子组件提供数据,如下面的演示所示。
Shared/ListDisplay3.razor
:
@typeparam TExample
@if (ExampleList is not null)
<ul style="color:blue">
@foreach (var item in ExampleList)
<li>@item</li>
@code {
[CascadingParameter]
protected IEnumerable<TExample>? ExampleList { get; set; }
Shared/ListDisplay4.razor
:
@typeparam TExample
@if (ExampleList is not null)
<ul style="color:red">
@foreach (var item in ExampleList)
<li>@item</li>
@code {
[CascadingParameter]
protected IEnumerable<TExample>? ExampleList { get; set; }
Shared/ListGenericTypeItems3.razor
:
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample
<h2>List Generic Type Items 3</h2>
@ChildContent
@if (ExampleList is not null)
<ul style="color:green">
@foreach(var item in ExampleList)
<li>@item</li>
Type of <code>TExample</code>: @typeof(TExample)
@code {
[CascadingParameter]
protected IEnumerable<TExample>? ExampleList { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
在下面的示例中对数据进行级联时,必须向 ListGenericTypeItems3
组件提供类型。
Pages/GenericTypeExample3.razor
:
@page "/generic-type-example-3"
<h1>Generic Type Example 3</h1>
<CascadingValue Value="@stringData">
<ListGenericTypeItems3 TExample="string">
<ListDisplay3 />
<ListDisplay4 />
</ListGenericTypeItems3>
</CascadingValue>
<CascadingValue Value="@integerData">
<ListGenericTypeItems3 TExample="int">
<ListDisplay3 />
<ListDisplay4 />
</ListGenericTypeItems3>
</CascadingValue>
@code {
private List<string> stringData = new() { "Item 1", "Item 2" };
private List<int> integerData = new() { 1, 2, 3 };
当级联多个泛型类型时,必须传递集中所有泛型类型的值。 在下面的示例中,TItem
、TValue
和 TEdit
是 GridColumn
泛型类型,但放置 GridColumn
的父组件并不指定 TItem
类型:
<GridColumn TValue="string" TEdit="@TextEdit" />
前面的示例会生成一个编译时错误,即 GridColumn
组件缺少 TItem
类型参数。 有效的代码将指定所有类型:
<GridColumn TValue="string" TEdit="@TextEdit" TItem="@User" />
基于上级组件推断泛型类型
本部分中的演示为 TExample
级联了一个推断的类型。
本部分使用级联的泛型类型支持部分中的两个 ListDisplay
组件。
Shared/ListGenericTypeItems4.razor
:
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample
<h2>List Generic Type Items 4</h2>
@ChildContent
@if (ExampleList is not null)
<ul style="color:green">
@foreach(var item in ExampleList)
<li>@item</li>
Type of <code>TExample</code>: @typeof(TExample)
@code {
[Parameter]
public IEnumerable<TExample>? ExampleList { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
具有所推断的级联类型的以下 GenericTypeExample4
组件提供了不同的显示数据。
Pages/GenericTypeExample4.razor
:
@page "/generic-type-example-4"
<h1>Generic Type Example 4</h1>
<ListGenericTypeItems4 ExampleList="@(new List<string> { "Item 5", "Item 6" })">
<ListDisplay1 ExampleList="@(new List<string> { "Item 1", "Item 2" })" />
<ListDisplay2 ExampleList="@(new List<string> { "Item 3", "Item 4" })" />
</ListGenericTypeItems4>
<ListGenericTypeItems4 ExampleList="@(new List<int> { 7, 8, 9 })">
<ListDisplay1 ExampleList="@(new List<int> { 1, 2, 3 })" />
<ListDisplay2 ExampleList="@(new List<int> { 4, 5, 6 })" />
</ListGenericTypeItems4>
具有所推断的级联类型的以下 GenericTypeExample5
组件提供了相同的显示数据。 下面的示例将数据直接分配给组件。
Pages/GenericTypeExample5.razor
:
@page "/generic-type-example-5"
<h1>Generic Type Example 5</h1>
<ListGenericTypeItems4 ExampleList="@stringData">
<ListDisplay1 ExampleList="@stringData" />
<ListDisplay2 ExampleList="@stringData" />
</ListGenericTypeItems4>
<ListGenericTypeItems4 ExampleList="@integerData">
<ListDisplay1 ExampleList="@integerData" />
<ListDisplay2 ExampleList="@integerData" />
</ListGenericTypeItems4>
@code {
private List<string> stringData = new() { "Item 1", "Item 2" };
private List<int> integerData = new() { 1, 2, 3 };
从 JavaScript 呈现 Razor 组件
可以从 JavaScript (JS) 为现有 JS 应用动态呈现 Razor 组件。
若要从 JS 呈现 Razor 组件,请将该组件注册为 JS 呈现的根组件并为该组件分配一个标识符:
在 Blazor Server 应用中,在 Program.cs
中修改对 AddServerSideBlazor 的调用:
builder.Services.AddServerSideBlazor(options =>
options.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
前面的代码示例要求在 Program.cs
文件中为应用的组件(例如 using BlazorSample.Pages;
)提供一个命名空间。
在 Blazor WebAssembly 应用中,在 Program.cs
中对 RootComponents 调用 RegisterForJavaScript:
builder.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
前面的代码示例要求在 Program.cs
文件中为应用的组件(例如 using BlazorSample.Pages;
)提供一个命名空间。
将 Blazor 加载到 JS 应用(blazor.server.js
或 blazor.webassembly.js
)。 使用注册的标识符将组件从 JS 呈现到容器元素,并根据需要传递组件参数:
let containerElement = document.getElementById('my-counter');
await Blazor.rootComponents.add(containerElement, 'counter', { incrementAmount: 10 });
Blazor 自定义元素
可通过实验性支持使用 Microsoft.AspNetCore.Components.CustomElements
NuGet 包来生成自定义元素。 自定义元素使用标准 HTML 接口来实现自定义 HTML 元素。
提供实验性功能是为了探索功能的可用性,此类功能可能不会以稳定版本提供。
将根组件注册为自定义元素:
在 Blazor Server 应用中,在 Program.cs
中修改对 AddServerSideBlazor 的调用:
builder.Services.AddServerSideBlazor(options =>
options.RootComponents.RegisterAsCustomElement<Counter>("my-counter");
前面的代码示例要求在 Program.cs
文件中为应用的组件(例如 using BlazorSample.Pages;
)提供一个命名空间。
在 Blazor WebAssembly 应用中,在 Program.cs
中对 RootComponents 调用 RegisterAsCustomElement
:
builder.RootComponents.RegisterAsCustomElement<Counter>("my-counter");
前面的代码示例要求在 Program.cs
文件中为应用的组件(例如 using BlazorSample.Pages;
)提供一个命名空间。
在应用的 HTML 中在Blazor 脚本标记之前添加以下 <script>
标记:
<script src="/_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js"></script>
将自定义元素与任何 Web 框架结合使用。 例如,前面的 counter 自定义元素在带有以下标记的 React 应用中使用:
<my-counter increment-amount={incrementAmount}></my-counter>
有关如何使用 Blazor 创建自定义元素的完整示例,请参阅 Blazor 自定义元素示例项目。
自定义元素功能目前处于实验阶段,不受支持,并且随时可能更改或删除。 欢迎你就这种特定方法能否满足你的要求提出反馈意见。
生成 Angular 和 React 组件
通过 Razor 组件为 Web 框架生成特定于框架的 JavaScript (JS) 组件,例如 Angular 或 React。 此功能未包含在 .NET 6 中,但通过新支持(即,支持从 JS 呈现 Razor 组件)得以实现。 GitHub 上的JS 组件生成示例演示了如何通过 Razor 组件生成 Angular 和 React 组件。 有关其他信息,请参阅 GitHub 示例应用的 README.md
文件。
Angular 和 React 组件功能目前处于实验阶段,不受支持,并且随时可能更改或删除。 欢迎你就这种特定方法能否满足你的要求提出反馈意见。
Blazor 应用是使用 Razor 组件(非正式地称为 Blazor 组件)构建的。 组件是用户界面 (UI) 的自包含部分,具有用于启用动态行为的处理逻辑。 组件可以嵌套、重复使用、在项目间共享,并可在 MVC 和 Razor Pages 应用中使用。
组件是使用 C# 和 HTML 标记的组合在 Razor 组件文件(文件扩展名为 .razor
)中实现的。
Razor 语法
组件使用 Razor 语法。 组件广泛使用了两个 Razor 功能,即指令和指令特性 。 这两个功能是前缀为 @
的保留关键字,出现在 Razor 标记中:
- 指令:更改组件标记的分析或运行方式。 例如,
@page
指令使用路由模板指定可路由组件,可以由用户请求在浏览器中按特定 URL 直接访问。
- 指令特性:更改组件元素的分析方式或运行方式。 例如,
<input>
元素的 @bind
指令特性会将数据绑定到元素的值。
本文和 Blazor 文档集的其他文章中进一步说明了在组件中使用的指令和指令特性。 有关 Razor 语法的一般信息,请参阅 ASP.NET Core 的 Razor 语法参考。
组件的名称必须以大写字符开头:
ProductDetail.razor
有效。
productDetail.razor
无效。
整个 Blazor 文档中使用的常见 Blazor 命名约定包括:
- 组件文件路径使用 Pascal 大小写†,在显示组件代码示例之前出现。 路径指示典型文件夹位置。 例如,
Pages/ProductDetail.razor
指示 ProductDetail
组件具有文件名 ProductDetail.razor
,并位于应用的 Pages
文件夹中。
- 可路由组件的组件文件路径与其 URL 匹配,对于组件路由模板中各单词之间的空格会显示连字符。 例如,在浏览器中,通过相对 URL
/product-detail
请求具有路由模板 /product-detail
(@page "/product-detail"
) 的 ProductDetail
组件。
†Pascal 大小写(大写 camel 形式)是不带空格和标点符号的命名约定,其中每个单词的首字母大写(包括第一个单词)。
可以通过使用 @page
指令为应用中的每个可访问组件提供路由模板来实现 Blazor 中的路由。 编译具有 @page
指令的 Razor 文件时,将为生成的类提供指定路由模板的 RouteAttribute。 在运行时,路由器将使用 RouteAttribute 搜索组件类,并呈现具有与请求的 URL 匹配的路由模板的任何组件。
以下 HelloWorld
组件使用路由模板 /hello-world
。 可通过相对 URL /hello-world
访问组件呈现的网页。 当使用默认协议、主机和端口在本地运行 Blazor 应用时,会在浏览器中通过 https://localhost:5001/hello-world
请求 HelloWorld
组件。 生成网页的组件通常位于 Pages
文件夹中,但可以使用任何文件夹保存组件(包括在嵌套文件夹中)。
Pages/HelloWorld.razor
:
@page "/hello-world"
<h1>Hello World!</h1>
前面的组件在浏览器中通过 /hello-world
进行加载,无论是否将组件添加到应用的 UI 导航。 (可选)组件可以添加到 NavMenu
组件,以便在应用基于 UI 的导航中显示组件链接。
对于前面的 HelloWorld
组件,可以将 NavLink
组件添加到 Shared
文件夹中的 NavMenu
组件。 有关详细信息(包括对 NavLink
和 NavMenu
组件的描述),请参阅 ASP.NET Core Blazor 路由和导航。
组件的 UI 使用由 Razor 标记、C# 和 HTML 组成的 Razor 语法进行定义。 在编译应用时,HTML 标记和 C# 呈现逻辑转换为组件类。 生成的类的名称与文件名匹配。
组件类的成员在一个或多个 @code
块中定义。 在 @code
块中,组件状态使用 C# 进行指定和处理:
- 属性和字段初始化表达式。
- 由父组件和路由参数传递的自变量的参数值。
- 用于用户事件处理、生命周期事件和自定义组件逻辑的方法。
组件成员使用以 @
符号开头的 C# 表达式在呈现逻辑中进行使用。 例如,通过为字段名称添加 @
前缀来呈现 C# 字段。 下面 Markup
示例计算并呈现:
headingFontStyle
,表示标题元素的 CSS 属性值 font-style
。
headingText
,表示标题元素的内容。
Pages/Markup.razor
:
@page "/markup"
<h1 style="font-style:@headingFontStyle">@headingText</h1>
@code {
private string headingFontStyle = "italic";
private string headingText = "Put on your new Blazor!";
Blazor 文档中的示例会为私有成员指定 private
访问修饰符。 私有成员的范围限定为组件的类。 但是,C# 会在没有访问修饰符存在时采用 private
访问修饰符,因此在自己的代码中将成员显式标记为“private
”是可选的。 有关访问修饰符的详细信息,请参阅访问修饰符(C# 编程指南)。
Blazor 框架在内部将组件作为呈现树进行处理,该树是组件的文档对象模型 (DOM) 和级联样式表对象模型 (CSSOM) 的组合。 最初呈现组件后,会重新生成组件的呈现树以响应事件。 Blazor 会将新呈现树与以前的呈现树进行比较,并将所有修改应用于浏览器的 DOM 以进行显示。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现。
组件是普通 C# 类,可以放置在项目中的任何位置。 生成网页的组件通常位于 Pages
文件夹中。 非页面组件通常放置在 Shared
文件夹或添加到项目的自定义文件夹中。
C# 控件结构、指令和指令属性的 Razor 语法需要小写(示例:@if
、@code
、@bind
)。 属性名称需要大写(示例:LayoutComponentBase.Body 的 @Body
)。
异步方法 (async
) 不支持返回 void
Blazor 框架不跟踪返回 void
的异步方法 (async
)。 因此,如果 void
返回,则不会捕获异常。 始终从异步方法返回 Task。
通过使用 HTML 语法声明组件,组件可以包含其他组件。 使用组件的标记类似于 HTML 标记,其中标记的名称是组件类型。
请考虑以下 Heading
组件,其他组件可以使用该组件显示标题。
Shared/Heading.razor
:
<h1 style="font-style:@headingFontStyle">Heading Example</h1>
@code {
private string headingFontStyle = "italic";
HeadingExample
组件中的以下标记会在 <Heading />
标记出现的位置呈现前面的 Heading
组件。
Pages/HeadingExample.razor
:
@page "/heading-example"
<Heading />
如果某个组件包含一个 HTML 元素,该元素的大写首字母与相同命名空间中的组件名称不匹配,则会发出警告,指示该元素名称异常。 为组件的命名空间添加 @using
指令使组件可用,这可解决此警告。 有关详细信息,请参阅命名空间部分。
此部分中显示的 Heading
组件示例没有 @page
指令,因此用户无法在浏览器中通过直接请求直接访问 Heading
组件。 但是,具有 @page
指令的任何组件都可以嵌套在另一个组件中。 如果通过在 Razor 文件顶部包含 @page "/heading"
可直接访问 Heading
组件,则会在 /heading
和 /heading-example
处为浏览器请求呈现该组件。
通常,组件的命名空间是从应用的根命名空间和该组件在应用内的位置(文件夹)派生而来的。 如果应用的根命名空间是 BlazorSample
,并且 Counter
组件位于 Pages
文件夹中:
Counter
组件的命名空间为 BlazorSample.Pages
。
- 组件的完全限定类型名称为
BlazorSample.Pages.Counter
。
对于保存组件的自定义文件夹,将 @using
指令添加到父组件或应用的 _Imports.razor
文件。 下面的示例提供 Components
文件夹中的组件:
@using BlazorSample.Components
_Imports.razor
文件中的 @using
指令仅适用于 Razor 文件 (.razor
),而不适用于 C# 文件 (.cs
)。
还可以使用其完全限定的名称来引用组件,而不需要 @using
指令。 以下示例直接引用应用的 Components
文件夹中的 ProductDetail
组件:
<BlazorSample.Components.ProductDetail />
使用 Razor 创建的组件的命名空间基于以下内容(按优先级顺序):
- Razor 文件标记中的
@namespace
指令(例如 @namespace BlazorSample.CustomNamespace
)。
- 项目文件中项目的
RootNamespace
(例如 <RootNamespace>BlazorSample</RootNamespace>
)。
- 项目名称,取自项目文件的文件名 (
.csproj
),以及从项目根到组件的路径。 例如,框架将具有项目命名空间 BlazorSample
(BlazorSample.csproj
) 的 {PROJECT ROOT}/Pages/Index.razor
解析到 Index
组件的命名空间 BlazorSample.Pages
。 {PROJECT ROOT}
是项目根路径。 组件遵循 C# 名称绑定规则。 对于本示例中的 Index
组件,范围内的组件是所有组件:
- 在同一文件夹
Pages
中。
- 未显式指定其他命名空间的项目根中的组件。
不支持以下项目:
global::
限定。
- 导入具有别名
using
语句的组件。 例如,@using Foo = Bar
不受支持。
- 部分限定的名称。 例如,无法将
@using BlazorSample
添加到组件中,然后使用 <Shared.NavMenu></Shared.NavMenu>
在应用的 Shared
文件夹中引用 NavMenu
组件 (Shared/NavMenu.razor
)。
分部类支持
组件以 C# 分部类的形式生成,使用以下任一方法进行创作:
- 单个文件包含在一个或多个
@code
块、HTML 标记和 Razor 标记中定义的 C# 代码。 Blazor 项目模板使用此单文件方法来定义其组件。
- HTML 和 Razor 标记位于 Razor 文件 (
.razor
) 中。 C# 代码位于定义为分部类的代码隐藏文件 (.cs
) 中。
定义特定于组件的样式的组件样式表是单独的文件 (.css
)。 Blazor CSS 隔离稍后在 ASP.NET Core Blazor CSS 隔离 中进行介绍。
下面的示例显示了从 Blazor 项目模板生成的应用中具有 @code
块的默认 Counter
组件。 标记和 C# 代码位于同一个文件中。 这是在创作组件时采用的最常见方法。
Pages/Counter.razor
:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
currentCount++;
以下 Counter
组件使用带有分部类的代码隐藏文件从 C# 代码中拆分 HTML 和 Razor 标记:
Pages/CounterPartialClass.razor
:
@page "/counter-partial-class"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
Pages/CounterPartialClass.razor.cs
:
namespace BlazorSample.Pages
public partial class CounterPartialClass
private int currentCount = 0;
void IncrementCount()
currentCount++;
_Imports.razor
文件中的 @using
指令仅适用于 Razor 文件 (.razor
),而不适用于 C# 文件 (.cs
)。 根据需要将命名空间添加到分部类文件中。
组件使用的典型命名空间:
using System.Net.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.JSInterop;
典型命名空间还包含应用的命名空间以及与应用的 Shared
文件夹对应的命名空间:
using BlazorSample;
using BlazorSample.Shared;
@inherits
指令用于指定组件的基类。 下面的示例演示组件如何继承基类以提供组件的属性和方法。 BlazorRocksBase
基类派生自 ComponentBase。
Pages/BlazorRocks.razor
:
@page "/blazor-rocks"
@inherits BlazorRocksBase
<h1>@BlazorRocksText</h1>
BlazorRocksBase.cs
:
using Microsoft.AspNetCore.Components;
namespace BlazorSample
public class BlazorRocksBase : ComponentBase
public string BlazorRocksText { get; set; } =
"Blazor rocks the browser!";
组件参数将数据传递给组件,使用组件类中包含 [Parameter]
特性的公共 C# 属性进行定义。 在下面的示例中,内置引用类型 (System.String) 和用户定义的引用类型 (PanelBody
) 作为组件参数进行传递。
PanelBody.cs
:
public class PanelBody
public string Text { get; set; }
public string Style { get; set; }
Shared/ParameterChild.razor
:
<div class="card w-25" style="margin-bottom:15px">
<div class="card-header font-weight-bold">@Title</div>
<div class="card-body" style="font-style:@Body.Style">
@Body.Text
@code {
[Parameter]
public string Title { get; set; } = "Set By Child";
[Parameter]
public PanelBody Body { get; set; } =
new()
Text = "Set by child.",
Style = "normal"
支持为组件参数提供初始值,但不会创建在首次呈现后向自身参数写入的组件。 有关详细信息,请参阅本文的重写参数部分。
ParameterChild
组件的 Title
和 Body
组件参数通过自变量在呈现组件实例的 HTML 标记中进行设置。 以下 ParameterParent
组件会呈现两个 ParameterChild
组件:
- 第一个
ParameterChild
组件在呈现时不提供参数自变量。
- 第二个
ParameterChild
组件从 ParameterParent
组件接收 Title
和 Body
的值,后者使用显式 C# 表达式设置 PanelBody
的属性值。
Pages/ParameterParent.razor
:
@page "/parameter-parent"
<h1>Child component (without attribute values)</h1>
<ParameterChild />
<h1>Child component (with attribute values)</h1>
<ParameterChild Title="Set by Parent"
Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
当 ParameterParent
组件未提供组件参数值时,来自 ParameterParent
组件的以下呈现 HTML 标记会显示 ParameterChild
组件默认值。 当 ParameterParent
组件提供组件参数值时,它们会替换 ParameterChild
组件的默认值。
为清楚起见,呈现 CSS 样式类未显示在以下呈现 HTML 标记中。
<h1>Child component (without attribute values)</h1>
<div>Set By Child</div>
<div>Set by child.</div>
<h1>Child component (with attribute values)</h1>
<div>Set by Parent</div>
<div>Set by parent.</div>
使用 Razor 的保留 @
符号,将方法的 C# 字段、属性或结果作为 HTML 特性值分配给组件参数。 以下 ParameterParent2
组件显示前面 ParameterChild
组件的四个实例,并将其 Title
参数值设置为:
title
字段的值。
GetTitle
C# 方法的结果。
- 带有ToLongDateString 的长格式当前本地日期,使用隐式 C# 表达式。
panelData
对象的 Title
属性。
字符串参数需要 @
前缀。 否则,框架假定设置了字符串字面量。
在字符串参数之外,我们建议对非文本使用 @
前缀,即使它们不是绝对必需的。
我们不建议对文本(例如,布尔值)、关键字(例如,this
)或 null
使用 @
前缀,但可以根据需要选择使用它们。 例如,IsFixed="@true"
不常见,但受支持。
在大多数情况下,根据 HTML5 规范,参数属性值的引号是可选的。 例如,支持 Value=this
,而不是 Value="this"
。 但是,我们建议使用引号,因为它更易于记住,并且在基于 Web 的技术中被广泛采用。
在整个文档中,代码示例:
- 始终使用引号。 示例:
Value="this"
。
- 非文本总是使用
@
前缀,即使它是可选的。 示例:Title="@title"
,其中 title
是字符串类型的变量。 Count="@ct"
,其中 ct
是数字类型的变量。
- Razor 表达式之外的文本始终避免使用
@
。 示例:IsFixed="true"
。
Pages/ParameterParent2.razor
:
@page "/parameter-parent-2"
<ParameterChild Title="@title" />
<ParameterChild Title="@GetTitle()" />
<ParameterChild Title="@DateTime.Now.ToLongDateString()" />
<ParameterChild Title="@panelData.Title" />
@code {
private string title = "From Parent field";
private PanelData panelData = new();
private string GetTitle()
return "From Parent method";
private class PanelData
public string Title { get; set; } = "From Parent object";
将 C# 成员分配给组件参数时,请使用符号 @
为成员添加前缀,绝不要为参数的 HTML 特性添加前缀。
<ParameterChild Title="@title" />
<ParameterChild @Title="title" />
与 Razor 页面 (.cshtml
) 不同,在呈现组件时,Blazor 不能在 Razor 表达式中执行异步工作。 这是因为 Blazor 是为呈现交互式 UI 而设计的。 在交互式 UI 中,屏幕必须始终显示某些内容,因此阻止呈现流是没有意义的。 相反,异步工作是在一个异步生命周期事件期间执行的。 在每个异步生命周期事件之后,组件可能会再次呈现。 不支持以下 Razor 语法:
<ParameterChild Title="@await ..." />
生成应用时,前面示例中的代码会生成编译器错误:
“await”运算符只能用于异步方法中。 请考虑用“async”修饰符标记此方法,并将其返回类型更改为“Task”。
若要在前面的示例中异步获取 Title
参数的值,组件可以使用 OnInitializedAsync
生命周期事件,如以下示例所示:
<ParameterChild Title="@title" />
@code {
private string title;
protected override async Task OnInitializedAsync()
title = await ...;
有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
不支持使用显式 Razor 表达式连接文本和表达式结果以赋值给参数。 下面的示例尝试将文本“Set by
”与对象属性值连接在一起。 尽管 Razor 页面 (.cshtml
) 支持此语法,但对在组件中赋值给子级的 Title
参数无效。 不支持以下 Razor 语法:
<ParameterChild Title="Set by @(panelData.Title)" />
生成应用时,前面示例中的代码会生成编译器错误:
组件属性不支持复杂内容(混合 C# 和标记)。
若要支持组合值赋值,请使用方法、字段或属性。 下面的示例在 C# 方法 GetTitle
中将“Set by
”与对象属性值连接在一起:
Pages/ParameterParent3.razor
:
@page "/parameter-parent-3"
<ParameterChild Title="@GetTitle()" />
@code {
private PanelData panelData = new();
private string GetTitle() => $"Set by {panelData.Title}";
private class PanelData
public string Title { get; set; } = "Parent";
有关详细信息,请参阅 ASP.NET Core 的 Razor 语法参考。
支持为组件参数提供初始值,但不会创建在首次呈现后向自身参数写入的组件。 有关详细信息,请参阅本文的重写参数部分。
应将组件参数声明为自动属性,这意味着它们不应在其 get
或 set
访问器中包含自定义逻辑。 例如,下面的 StartData
属性是自动属性:
[Parameter]
public DateTime StartData { get; set; }
不要在 get
或 set
访问器中放置自定义逻辑,因为组件参数专门用作父组件向子组件传送信息的通道。 如果子组件属性的 set
访问器包含导致父组件重新呈现的逻辑,则会导致一个无限的呈现循环。
若要转换已接收的参数值,请执行以下操作:
- 将参数属性保留为自动属性,以表示所提供的原始数据。
- 创建另一个属性或方法,用于基于参数属性提供转换后的数据。
重写 OnParametersSetAsync
以在每次收到新数据时转换接收到的参数。
支持将初始值写入组件参数,因为初始值赋值不会干扰 Blazor 的自动组件呈现。 在组件中,使用 DateTime.Now 将当前本地 DateTime 赋予 StartData
是有效语法:
[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;
进行 DateTime.Now 的初始赋值之后,请勿在开发人员代码中向 StartData
赋值。 有关详细信息,请参阅本文的重写参数部分。
组件可以在 @page
指令的路由模板中指定路由参数。 Blazor 路由器使用路由参数来填充相应的组件参数。
支持可选路由参数。 在下面的示例中,text
可选参数将 route 段的值赋给组件的 Text
属性。 如果该段不存在,则在 OnInitialized
生命周期方法中将 Text
的值设置为 fantastic
。
Pages/RouteParameter.razor
:
@page "/route-parameter/{text?}"
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string Text { get; set; }
protected override void OnInitialized()
Text = Text ?? "fantastic";
有关捕获跨越多个文件夹边界的路径的 catch-all 路由参数 ({*pageRoute}
) 的信息,请参阅 ASP.NET Core Blazor 路由和导航。
子内容呈现片段
组件可以设置另一个组件的内容。 分配组件提供子组件的开始标记与结束标记之间的内容。
在下面的示例中,RenderFragmentChild
组件具有一个 ChildContent
组件参数,它将要呈现的 UI 段表示为 RenderFragment。 ChildContent
在组件 Razor 标记中的位置是在最终 HTML 输出中呈现内容的位置。
Shared/RenderFragmentChild.razor
:
<div class="card w-25" style="margin-bottom:15px">
<div class="card-header font-weight-bold">Child content</div>
<div class="card-body">@ChildContent</div>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
必须按约定将接收 RenderFragment 内容的属性命名为 ChildContent
。
RenderFragment 不支持事件回叫。
以下 RenderFragmentParent
组件通过将内容置于子组件的开始标记和结束标记内,来提供用于呈现 RenderFragmentChild
的内容。
Pages/RenderFragmentParent.razor
:
@page "/render-fragment-parent"
<h1>Render child content</h1>
<RenderFragmentChild>
Content of the child component is supplied
by the parent component.
</RenderFragmentChild>
由于 Blazor 呈现子内容的方式,如果在 RenderFragmentChild
组件的内容中使用递增循环变量,则在 for
循环内呈现组件需要本地索引变量。 下面的示例可以添加到前面的 RenderFragmentParent
组件:
<h1>Three children with an index variable</h1>
@for (int c = 0; c < 3; c++)
var current = c;
<RenderFragmentChild>
Count: @current
</RenderFragmentChild>
或者,将 foreach
循环与 Enumerable.Range 结合使用,而不是使用 for
循环。 下面的示例可以添加到前面的 RenderFragmentParent
组件:
<h1>Second example of three children with an index variable</h1>
@foreach (var c in Enumerable.Range(0,3))
<RenderFragmentChild>
Count: @c
</RenderFragmentChild>
呈现片段用于在整个 Blazor 应用中呈现子内容,在下面的文章和文章部分中有示例介绍:
- Blazor 布局
- 跨组件层次结构传递数据
- 模板化组件
- 全局异常处理
Blazor 框架的内置 Razor 组件使用相同的 ChildContent
组件参数约定来设置其内容。 可以通过在 API 文档(使用搜索词“ChildContent”筛选 API)中搜索组件参数属性名称 ChildContent
来查看设置子内容的组件。
可重用呈现逻辑的呈现片段
你可以分解出子组件,纯粹作为重复使用呈现逻辑的方法。 在任何组件的 @code
块中,根据需要定义 RenderFragment 并呈现任意位置的片段:
<h1>Hello, world!</h1>
@RenderWelcomeInfo
<p>Render the welcome info a second time:</p>
@RenderWelcomeInfo
@code {
private RenderFragment RenderWelcomeInfo = __builder =>
<p>Welcome to your new app!</p>
有关详细信息,请参阅重复使用呈现逻辑。
Blazor 框架通常会施加安全的父级到子级参数的赋值:
- 不会意外覆盖参数。
- 最大程度地减少副作用。 例如,可避免附加的呈现,因为它们可能会创建无限的呈现循环。
当父组件重新呈现时,子组件会接收可能覆盖现有值的新参数值。 当开发带有一个或多个数据绑定参数的组件,并且开发人员直接写入子组件中的参数时,经常会发生意外覆盖子组件中的参数值:
- 子组件通过父组件中的一个或多个参数值呈现。
- 子级直接写入参数的值。
- 父组件重新呈现并覆盖子参数的值。
覆盖参数值的可能性也会延伸到子组件的属性 set
访问器中。
我们的通用指南不是创建在首次呈现后直接向自身参数写入的组件。
请考虑使用以下 Expander
组件,它们会:
- 呈现子内容。
- 切换以使用组件参数 (
Expanded
) 显示子内容。
在下面的 Expander
组件演示了已覆盖参数之后,会显示修改后的 Expander
组件以演示此方案的正确方法。 下面的示例可以放置在本地示例应中,以体验所述的行为。
Shared/Expander.razor
:
<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
<div class="card-body">
<h2 class="card-title">Toggle (<code>Expanded</code> = @Expanded)</h2>
@if (Expanded)
<p class="card-text">@ChildContent</p>
@code {
[Parameter]
public bool Expanded { private get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private void Toggle()
Expanded = !Expanded;
Expander
组件会添加到可调用 StateHasChanged 的以下 ExpanderExample
父组件中:
- 在开发人员代码中调用 StateHasChanged 会向组件通知其状态已更改,并且通常会触发组件呈现以更新 UI。 StateHasChanged 稍后将在 ASP.NET Core Razor 组件生命周期和 ASP.NET Core Razor 组件呈现中详细介绍。
- 按钮的
@onclick
指令特性会将事件处理程序附加到按钮的 onclick
事件。 稍后在 ASP.NET Core Blazor 事件处理中更详细地介绍事件处理。
Pages/ExpanderExample.razor
:
@page "/expander-example"
<Expander Expanded="true">
Expander 1 content
</Expander>
<Expander Expanded="true" />
<button @onclick="StateHasChanged">
Call StateHasChanged
</button>
最初,在切换 Expanded
属性时,Expander
组件独立地作出行为。 子组件会按预期方式维护其状态。
如果在父组件中调用 StateHasChanged,则 Blazor 框架会重新呈现子组件(如果其参数可能已更改):
- 对于 Blazor 显式检查的一组参数类型,如果 Blazor 检测到有任何参数发生更改,则会重新呈现子组件。
- 对于未选中的参数类型,无论参数是否发生更改,Blazor 都会呈现子组件。 子内容属于此类参数类型,因为子内容属于 RenderFragment 类型,它是引用其他可变对象的委托。
对于 ExpanderExample
组件:
- 第一个
Expander
组件在可能可变的 RenderFragment 中设置子内容,因此在父组件中调用 StateHasChanged 会自动重新呈现该组件,并可能将 Expanded
的值覆盖为其初始值 true
。
- 第二个
Expander
组件未设置子内容。 因此,不存在可能可变的 RenderFragment。 在父组件中调用 StateHasChanged 不会自动呈现子内容,因此组件的 Expanded
值不会被覆盖。
要维持在前述情况中的状态,请在 Expander
组件中使用私有字段来保留它的切换状态。
以下经修定的 Expander
组件:
- 接受父项中的
Expanded
组件参数值。
- 将组件参数值分配给
OnInitialized
事件中的私有字段 (expanded
)。
- 使用私有字段来维护其内部切换状态,该状态演示如何避免直接写入参数。
此部分中的建议可扩展到组件参数 set
访问器中的类似逻辑,这可能会产生类似的不良副作用。
Shared/Expander.razor
:
<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
<div class="card-body">
<h2 class="card-title">Toggle (<code>expanded</code> = @expanded)</h2>
@if (expanded)
<p class="card-text">@ChildContent</p>
@code {
private bool expanded;
[Parameter]
public bool Expanded { private get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
protected override void OnInitialized()
expanded = Expanded;
private void Toggle()
expanded = !expanded;
有关其他信息,请参阅 Blazor 双向绑定错误 (dotnet/aspnetcore #24599)。
有关更改检测的详细信息,包括有关 Blazor 检查的完全匹配类型的信息,请参阅 ASP.NET Core Razor 组件呈现。
属性展开和任意参数
除了组件的声明参数外,组件还可以捕获和呈现其他属性。 其他特性可以在字典中捕获,然后在使用 @attributes
Razor 指令特性呈现组件时,将其展开到元素上。 对于定义生成支持各种自定义项的标记元素的组件,此方案非常有用。 例如,为支持多个参数的 <input>
单独定义属性可能比较繁琐。
在下面的 Splat
组件中:
- 第一个
<input>
元素 (id="useIndividualParams"
) 使用单个组件参数。
- 第二个
<input>
元素 (id="useAttributesDict"
) 使用特性展开。
Pages/Splat.razor
:
@page "/splat"
<input id="useIndividualParams"
maxlength="@maxlength"
placeholder="@placeholder"
required="@required"
size="@size" />
<input id="useAttributesDict"
@attributes="InputAttributes" />
@code {
private string maxlength = "10";
private string placeholder = "Input placeholder text";
private string required = "required";
private string size = "50";
private Dictionary<string, object> InputAttributes { get; set; } =
new()
{ "maxlength", "10" },
{ "placeholder", "Input placeholder text" },
{ "required", "required" },
{ "size", "50" }
网页中呈现的 <input>
元素是相同的:
<input id="useIndividualParams"
maxlength="10"
placeholder="Input placeholder text"
required="required"
size="50">
<input id="useAttributesDict"
maxlength="10"
placeholder="Input placeholder text"
required="required"
size="50">
若要接受任意特性,请定义组件参数,并将 CaptureUnmatchedValues 属性设置为 true
:
@code {
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> InputAttributes { get; set; }
[Parameter]
上的 CaptureUnmatchedValues 属性允许参数匹配所有不匹配任何其他参数的特性。 组件只能使用 CaptureUnmatchedValues 定义单个参数。 与 CaptureUnmatchedValues 一起使用的属性类型必须可以使用字符串键从 Dictionary<string, object>
中分配。 使用 IEnumerable<KeyValuePair<string, object>>
或 IReadOnlyDictionary<string, object>
也是此方案中的选项。
相对于元素特性位置的 @attributes
位置很重要。 在元素上展开 @attributes
时,将从右到左(从最后一个到第一个)处理特性。 请考虑以下使用子组件的父组件示例:
Shared/AttributeOrderChild1.razor
:
<div @attributes="AdditionalAttributes" extra="5" />
@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object> AdditionalAttributes { get; set; }
Pages/AttributeOrderParent1.razor
:
@page "/attribute-order-parent-1"
<AttributeOrderChild1 extra="10" />
AttributeOrderChild1
组件的 extra
属性设置为 @attributes
右侧。 通过附加特性传递时,AttributeOrderParent1
组件的呈现的 <div>
包含 extra="5"
,因为特性是从右到左(从最后一个到第一个)处理的:
<div extra="5" />
在下面的示例中,extra
和 @attributes
的顺序在子组件的 <div>
中反转:
Shared/AttributeOrderChild2.razor
:
<div extra="5" @attributes="AdditionalAttributes" />
@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object> AdditionalAttributes { get; set; }
Pages/AttributeOrderParent2.razor
:
@page "/attribute-order-parent-2"
<AttributeOrderChild2 extra="10" />
通过附加特性传递时,父组件呈现的网页中的 <div>
包含 extra="10"
:
<div extra="10" />
捕获对组件的引用
组件引用提供了一种引用组件实例以便发出命令的方法。 若要捕获组件引用,请执行以下操作:
- 向子组件添加
@ref
特性。
- 定义与子组件类型相同的字段。
呈现组件时,将用组件实例填充字段。 然后,可以在实例上调用 .NET 方法。
请考虑以下 ReferenceChild
组件,它会在调用其 ChildMethod
时记录消息。
Shared/ReferenceChild.razor
:
@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> logger
@code {
public void ChildMethod(int value)
logger.LogInformation("Received {Value} in ChildMethod", value);
组件引用仅在呈现组件后才进行填充,其输出包含 ReferenceChild
的元素。 在呈现组件之前,没有任何可引用的内容。
若要在组件完成呈现后操作组件引用,请使用 OnAfterRender
或 OnAfterRenderAsync
方法。
若要结合使用事件处理程序和引用变量,请使用 Lambda 表达式,或在 OnAfterRender
或 OnAfterRenderAsync
方法中分配事件处理程序委托。 这可确保在分配事件处理程序之前先分配引用变量。
以下 lambda 方法使用前面的 ReferenceChild
组件。
Pages/ReferenceParent1.razor
:
@page "/reference-parent-1"
<button @onclick="@(() => childComponent.ChildMethod(5))">
Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>
<ReferenceChild @ref="childComponent" />
@code {
private ReferenceChild childComponent;
以下委托方法使用前面的 ReferenceChild
组件。
Pages/ReferenceParent2.razor
:
@page "/reference-parent-2"
<button @onclick="callChildMethod">
Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>
<ReferenceChild @ref="childComponent" />
@code {
private ReferenceChild childComponent;
private Action callChildMethod;
protected override void OnAfterRender(bool firstRender)
if (firstRender)
callChildMethod = CallChildMethod;
private void CallChildMethod()
childComponent.ChildMethod(5);
尽管捕获组件引用使用与捕获元素引用类似的语法,但捕获组件引用不是 JavaScript 互操作功能。 组件引用不会传递给 JavaScript 代码。 组件引用只在 .NET 代码中使用。
不要使用组件引用来改变子组件的状态。 请改用常规声明性组件参数将数据传递给子组件。 使用组件参数使子组件在正确的时间自动重新呈现。 有关详细信息,请参阅组件参数部分和 ASP.NET Core Blazor 数据绑定一文。
同步上下文
Blazor 使用同步上下文 (SynchronizationContext) 来强制执行单个逻辑线程。 组件的生命周期方法和 Blazor 引发的事件回调都在此同步上下文上执行。
Blazor Server的同步上下文尝试模拟单线程环境,使其与浏览器中的单线程 WebAssembly 模型紧密匹配。 在任意给定的时间点,工作只在一个线程上执行,这会造成单个逻辑线程的印象。 不会同时执行两个操作。
避免阻止线程的调用
通常,不要在组件中调用以下方法。 以下方法阻止执行线程,进而阻止应用继续工作,直到基础 Task 完成:
- Result
- WaitAny
- WaitAll
- Sleep
- GetResult
使用此部分中所述的线程阻止方法的 Blazor 文档示例只是使用方法进行演示,而不是用作建议编码指导。 例如,一些组件代码演示通过调用 Thread.Sleep 来模拟长时间运行的进程。
在外部调用组件方法以更新状态
如果组件必须根据外部事件(如计时器或其他通知)进行更新,请使用 InvokeAsync
方法,它将代码执行调度到 Blazor 的同步上下文。 例如,请考虑以下通告程序服务,它可向任何侦听组件通知更新的状态。 可以从应用中的任何位置调用 Update
方法。
TimerService.cs
:
using System;
using System.Timers;
using Microsoft.Extensions.Logging;
public class TimerService : IDisposable
private int elapsedCount;
private readonly ILogger<TimerService> logger;
private readonly NotifierService notifier;
private Timer timer;
public TimerService(NotifierService notifier, ILogger<TimerService> logger)
this.notifier = notifier;
this.logger = logger;
public void Start()
if (timer is null)
timer = new();
timer.AutoReset = true;
timer.Interval = 10000;
timer.Elapsed += HandleTimer;
timer.Enabled = true;
logger.LogInformation("Started");
private async void HandleTimer(object source, ElapsedEventArgs e)
elapsedCount += 1;
await notifier.Update("elapsedCount", elapsedCount);
logger.LogInformation($"elapsedCount: {elapsedCount}");
public void Dispose()
timer?.Dispose();
NotifierService.cs
:
using System;
using System.Threading.Tasks;
public class NotifierService
public async Task Update(string key, int value)
if (Notify != null)
await Notify.Invoke(key, value);
public event Func<string, int, Task> Notify;
注册服务:
在 Blazor WebAssembly 应用中,在 Program.cs
中将服务注册为单一实例:
builder.Services.AddSingleton<NotifierService>();
builder.Services.AddSingleton<TimerService>();
在 Blazor Server 应用中,在 Startup.ConfigureServices
中将服务注册为有作用域:
services.AddScoped<NotifierService>();
services.AddScoped<TimerService>();
使用 NotifierService
更新组件。
Pages/ReceiveNotifications.razor
:
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer
<h1>Receive Notifications</h1>
<h2>Timer Service</h2>
<button @onclick="StartTimer">Start Timer</button>
<h2>Notifications</h2>
Status:
@if (lastNotification.key is not null)
<span>@lastNotification.key = @lastNotification.value</span>
<span>Awaiting first notification</span>
@code {
private (string key, int value) lastNotification;
protected override void OnInitialized()
Notifier.Notify += OnNotify;
public async Task OnNotify(string key, int value)
await InvokeAsync(() =>
lastNotification = (key, value);
StateHasChanged();
private void StartTimer()
Timer.Start();
public void Dispose()
Notifier.Notify -= OnNotify;
在上面的示例中:
NotifierService
在 Blazor 的同步上下文之外调用组件的 OnNotify
方法。 InvokeAsync
用于切换到正确的上下文,并将呈现排入队列。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现。
- 组件会实现 IDisposable。
OnNotify
委托在 Dispose
方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
使用 @key
控制是否保留元素和组件
在呈现元素或组件的列表并且元素或组件随后发生更改时,Blazor 必须决定之前的元素或组件中有哪些可以保留,以及模型对象应如何映射到这些元素或组件。 通常,此过程是自动的,可以忽略,但在某些情况下,可能需要控制该过程。
请考虑使用以下 Details
和 People
组件:
Details
组件从 People
父组件接收数据 Data
,这些数据会显示在 <input>
元素中。 当用户选择一个 <input>
元素时,显示的任何给定 <input>
元素都可以从用户接收页面焦点。
People
组件使用 Details
组件创建人员对象列表以进行显示 。 每三秒向集合中添加一个新人员。
此演示使你可以:
- 从多个呈现的
Details
组件中选择一个 <input>
。
- 随着人员集合自动增长,研究页面焦点的行为。
Shared/Details.razor
:
<input value="@Data" />
@code {
[Parameter]
public string Data { get; set; }
在以下