添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

本文介绍如何在 Blazor 应用中创建和使用 Razor 组件,包括有关 Razor 语法、组件命名、命名空间和组件参数的指导。

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" <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 组件的 TitleBody 组件参数通过自变量在呈现组件实例的 HTML 标记中进行设置。 以下 ParameterParent 组件会呈现两个 ParameterChild 组件:

    • 第一个 ParameterChild 组件在呈现时不提供参数自变量。
    • 第二个 ParameterChild 组件从 ParameterParent 组件接收 TitleBody 的值,后者使用显式 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 语法参考

      支持为组件参数提供初始值,但不会创建在首次呈现后向自身参数写入的组件。 有关详细信息,请参阅本文的重写参数部分。

      应将组件参数声明为自动属性,这意味着它们不应在其 getset 访问器中包含自定义逻辑。 例如,下面的 StartData 属性是自动属性:

      [Parameter]
      public DateTime StartData { get; set; }
      

      不要在 getset 访问器中放置自定义逻辑,因为组件参数专门用作父组件向子组件传送信息的通道。 如果子组件属性的 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 类型支持 TuplesAPI 文档)。 下面的组件参数示例在 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)

      引用 ©2005 环球影业冲出宁静号内森·菲利安

      组件可以在 @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 段表示为 RenderFragmentChildContent 在组件 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 组件呈现

      属性展开和任意参数

      除了组件的声明参数外,组件还可以捕获和呈现其他属性。 其他特性可以在字典中捕获,然后在使用 @attributesRazor 指令特性呈现组件时,将其展开到元素上。 对于定义生成支持各种自定义项的标记元素的组件,此方案非常有用。 例如,为支持多个参数的 <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 的元素。 在呈现组件之前,没有任何可引用的内容。

      若要在组件完成呈现后操作组件引用,请使用 OnAfterRenderOnAfterRenderAsync 方法

      若要结合使用事件处理程序和引用变量,请使用 Lambda 表达式,或在 OnAfterRenderOnAfterRenderAsync 方法中分配事件处理程序委托。 这可确保在分配事件处理程序之前先分配引用变量。

      以下 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 组件呈现
    • 组件会实现 IDisposableOnNotify 委托在 Dispose 方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期
    • 使用 @key 控制是否保留元素和组件

      在呈现元素或组件的列表并且元素或组件随后发生更改时,Blazor 必须决定之前的元素或组件中有哪些可以保留,以及模型对象应如何映射到这些元素或组件。 通常,此过程是自动的,可以忽略,但在某些情况下,可能需要控制该过程。

      请考虑使用以下 DetailsPeople 组件:

    • 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 属性指令的范围是其父级中自己的同级。

      请考虑以下示例。 firstsecond 键在外部 <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)。 这可确保基于对象引用相等性的保留。
    • 唯一标识符。 例如,唯一标识符可以基于类型为 intstringGuid 的主键值。
    • 确保用于 @key 的值不冲突。 如果在同一父元素内检测到冲突值,则 Blazor 引发异常,因为它无法明确地将旧元素或组件映射到新元素或组件。 仅使用非重复值,例如对象实例或主键值。

      可以通过 @attribute 指令将特性应用于组件。 下面的示例将 [Authorize] 特性应用于组件的类:

      @page "/"
      @attribute [Authorize]
      

      条件 HTML 元素属性

      HTML 元素特性属性基于 .NET 值有条件地设置。 如果值为 falsenull,则属性未设置。 如果值为 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}>
      

      下面的示例演示如何在组件中指定 RenderFragmentRenderFragment<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) 设置与分配的数据类型匹配的类型 stringint
    • 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 和一个匹配名称的最接近的上级中获取参数值。 级联泛型类型参数在特定子树内被重写。

      匹配只按名称进行。 因此,建议使用通用名称(例如 TTItem)来避免使用级联泛型类型参数。 如果开发人员选择级联一个类型参数,则会隐式承诺其名称是唯一的,不会与来自无关组件的其他级联类型参数冲突。

      可以通过以下任一使用上级(父)组件的方法将泛型类型级联到子组件,如以下两个子部分中所示:

    • 显式设置级联的泛型类型。
    • 推断级联的泛型类型。
    • 以下子部分使用以下两个 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 };
      

      当级联多个泛型类型时,必须传递集中所有泛型类型的值。 在下面的示例中,TItemTValueTEditGridColumn 泛型类型,但放置 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.jsblazor.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 组件。 有关详细信息(包括对 NavLinkNavMenu 组件的描述),请参阅 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 组件的 TitleBody 组件参数通过自变量在呈现组件实例的 HTML 标记中进行设置。 以下 ParameterParent 组件会呈现两个 ParameterChild 组件:

      • 第一个 ParameterChild 组件在呈现时不提供参数自变量。
      • 第二个 ParameterChild 组件从 ParameterParent 组件接收 TitleBody 的值,后者使用显式 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 语法参考

        支持为组件参数提供初始值,但不会创建在首次呈现后向自身参数写入的组件。 有关详细信息,请参阅本文的重写参数部分。

        应将组件参数声明为自动属性,这意味着它们不应在其 getset 访问器中包含自定义逻辑。 例如,下面的 StartData 属性是自动属性:

        [Parameter]
        public DateTime StartData { get; set; }
        

        不要在 getset 访问器中放置自定义逻辑,因为组件参数专门用作父组件向子组件传送信息的通道。 如果子组件属性的 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 段表示为 RenderFragmentChildContent 在组件 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 组件呈现

        属性展开和任意参数

        除了组件的声明参数外,组件还可以捕获和呈现其他属性。 其他特性可以在字典中捕获,然后在使用 @attributesRazor 指令特性呈现组件时,将其展开到元素上。 对于定义生成支持各种自定义项的标记元素的组件,此方案非常有用。 例如,为支持多个参数的 <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 的元素。 在呈现组件之前,没有任何可引用的内容。

        若要在组件完成呈现后操作组件引用,请使用 OnAfterRenderOnAfterRenderAsync 方法

        若要结合使用事件处理程序和引用变量,请使用 Lambda 表达式,或在 OnAfterRenderOnAfterRenderAsync 方法中分配事件处理程序委托。 这可确保在分配事件处理程序之前先分配引用变量。

        以下 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 组件呈现
      • 组件会实现 IDisposableOnNotify 委托在 Dispose 方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期
      • 使用 @key 控制是否保留元素和组件

        在呈现元素或组件的列表并且元素或组件随后发生更改时,Blazor 必须决定之前的元素或组件中有哪些可以保留,以及模型对象应如何映射到这些元素或组件。 通常,此过程是自动的,可以忽略,但在某些情况下,可能需要控制该过程。

        请考虑使用以下 DetailsPeople 组件:

      • 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 *@
        

        如果

  •