使用 Blazor 生成可重用的 UI 组件

提示

此内容摘自电子书《面向 Azure ASP.NET Web Forms 开发人员的 Blazor》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

Blazor-for-ASP-NET-Web-Forms-Developers eBook cover thumbnail.

ASP.NET Web Forms 的优势之一在于其能够将可重用的用户界面 (UI) 代码片段封装到可重用的 UI 控件中。 可以使用 .ascx 文件在标记中定义自定义用户控件。 还可以在代码中生成具有全面设计器支持的复杂服务器控件。

Blazor 还支持通过组件进行 UI 封装。 组件具有以下特点:

  • 是自包含的 UI 块。
  • 维护自己的状态和呈现逻辑。
  • 可以定义 UI 事件处理程序、绑定到输入数据以及管理其自己的生命周期。
  • 通常使用 Razor 语法在 .razor 文件中定义。

Razor 简介

Razor 是基于 HTML 和 C# 的轻量级标记模板化语言。 借助 Razor,可以在标记和 C# 代码之间无缝转换,以定义组件呈现逻辑。 编译 .razor 文件时,将在 .NET 类中以结构化方式捕获呈现逻辑。 编译类的名称从 .razor 文件名中获取。 命名空间从项目的默认命名空间和文件夹路径中获取,也可以使用 @namespace 指令(在下面的 Razor 指令中详细介绍)显式指定命名空间。

组件的呈现逻辑使用常规 HTML 标记创作,并使用 C# 添加动态逻辑。 @ 字符用于转换为 C#。 Razor 通常很聪明,可猜出你何时切换回 HTML。 例如,以下组件使用当前时间呈现 <p> 标记:

<p>@DateTime.Now</p>

若要显式指定 C# 表达式的开头和结尾,请使用括号:

<p>@(DateTime.Now)</p>

使用 Razor 还可以在呈现逻辑中轻松地使用 C# 控制流。 例如,可以有条件地呈现某些 HTML,如下所示:

@if (value % 2 == 0)
{
    <p>The value was even.</p>
}

或者,可以使用常规 C# foreach 循环生成项目列表,如下所示:

<ul>
@foreach (var item in items)
{
    <li>@item.Text</li>
}
</ul>

像 ASP.NET Web Forms 中的指令一样,Razor 指令可控制 Razor 组件编译的许多方面。 示例包括组件的以下项:

  • 命名空间
  • 基类
  • 实现的接口
  • 泛型参数
  • 导入的命名空间
  • 路由

Razor 指令以 @ 字符开头,通常用于文件开头的新行的开头。 例如,@namespace 指令定义组件的命名空间:

@namespace MyComponentNamespace

下表总结 Blazor 中使用的各种 Razor 指令及其 ASP.NET Web Forms 等效项(如果存在)。

指令 描述 示例 Web Forms 等效项
@attribute 将类级别属性添加到组件 @attribute [Authorize] None
@code 将类成员添加到组件 @code { ... } <script runat="server">...</script>
@implements 实现指定的接口 @implements IDisposable 使用代码隐藏
@inherits 从指定的基类继承 @inherits MyComponentBase <%@ Control Inherits="MyUserControlBase" %>
@inject 将服务注入组件 @inject IJSRuntime JS None
@layout 指定组件的布局组件 @layout MainLayout <%@ Page MasterPageFile="~/Site.Master" %>
@namespace 设置组件的命名空间 @namespace MyNamespace None
@page 指定组件的路由 @page "/product/{id}" <%@ Page %>
@typeparam 指定组件的泛型类型参数 @typeparam TItem 使用代码隐藏
@using 指定要纳入范围的命名空间 @using MyComponentNamespace 在 web.config 中添加命名空间

Razor 组件还广泛使用元素上的指令属性来控制组件编译的各个方面(事件处理、数据绑定、组件和元素引用等)。 指令属性均遵循常用泛型语法,其中括号中的值是可选的:

@directive(-suffix(:name))(="value")

下表总结 Blazor 中使用的 Razor 指令的各种属性。

属性 说明 示例
@attributes 呈现属性字典 <input @attributes="ExtraAttributes" />
@bind 创建双向数据绑定 <input @bind="username" @bind:event="oninput" />
@on{event} 为指定事件添加事件处理程序 <button @onclick="IncrementCount">Click me!</button>
@key 指定比较算法要用于保存集合中元素的键 <DetailsEditor @key="person" Details="person.Details" />
@ref 捕获对组件或 HTML 元素的引用 <MyDialog @ref="myDialog" />

Blazor 使用的各种指令属性(@onclick@bind@ref 等)将在下面的部分和后续章节中介绍。

.aspx 和 .ascx 文件中使用的许多语法在 Razor 中具有并行语法。 下面是 ASP.NET Web Forms 语法和 Razor 语法的简单比较。

功能 Web Forms — Web 窗体 语法 Razor 语法
指令 <%@ [directive] %> <%@ Page %> @[directive] @page
代码块 <% %> <% int x = 123; %> @{ } @{ int x = 123; }
表达式
(HTML 编码)
<%: %> <%:DateTime.Now %> 隐式:@
显式:@()
@DateTime.Now
@(DateTime.Now)
注释 <%-- --%> <%-- Commented --%> @* *@ @* Commented *@
数据绑定 <%# %> <%# Bind("Name") %> @bind <input @bind="username" />

若要将成员添加到 Razor 组件类,请使用 @code 指令。 此方法类似于在 ASP.NET Web Forms 用户控件或页中使用 <script runat="server">...</script> 块。

@code {
    int count = 0;

    void IncrementCount()
    {
        count++;
    }
}

因为 Razor 基于 C#,所以必须从 C# 项目 (.csproj) 中进行编译。 无法从 Visual Basic 项目 (.vbproj) 编译 .razor 文件。 仍然可以从 Blazor 项目引用 Visual Basic 项目。 反之亦然。

有关完整的 Razor 语法参考,请参阅 ASP.NET Core 的 Razor 语法参考

使用组件

除常规 HTML 外,组件还可以将其他组件用作其呈现逻辑的一部分。 用于在 Razor 中使用组件的语法类似于在 ASP.NET Web Forms 应用中使用用户控件。 使用与组件的类型名称匹配的元素标记指定组件。 例如,可以添加 Counter 组件,如下所示:

<Counter />

与 ASP.NET Web Forms 不同,Blazor 中的组件:

  • 不使用元素前缀(例如,asp:)。
  • 不要求在页上或在 web.config 中进行注册。

将 Razor 组件视为 .NET 类型,因为这正是其本质。 如果引用了包含组件的程序集,则该组件可供使用。 若要将组件的命名空间纳入范围,请应用 @using 指令:

@using MyComponentLib

<Counter />

如默认的 Blazor 项目所示,通常将 @using 指令放入 _Imports.razor 文件中,以便将它们导入同一目录中和子目录中的所有 .razor 文件。

如果组件的命名空间不在范围内,则可以使用其完整类型名称来指定组件,就像在 C# 中一样:

<MyComponentLib.Counter />

修改组件的页面标题

在生成 SPA 样式的应用时,通常可以在不重新加载整个页面的情况下重新加载页面的某些部分。 尽管如此,根据当前加载的组件,更改页面标题可能会很有用。 这可以通过在组件的 Razor 页面中包含 <PageTitle> 标记来实现:

@page "/"
<PageTitle>Home</PageTitle>

此元素的内容可以是动态的,例如显示消息的当前计数:

<PageTitle>@MessageCount messages</PageTitle>

请注意,如果特定页面上的多个组件包含 <PageTitle> 标记,则只会显示最后一个(因为每个组件都将覆盖前一个)。

组件参数

在 ASP.NET Web Forms 中,可以使用公共属性将参数和数据传递到控件。 这些属性可以使用特性在标记中进行设置,也可以直接在代码中设置。 Razor 组件以类似的方式工作,尽管组件属性还必须使用 [Parameter] 特性进行标记才能被视为组件参数。

以下 Counter 组件定义名为 IncrementAmount 的组件参数,该参数可用于指定每次单击按钮时 Counter 应该递增的数量。

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    int currentCount = 0;

    [Parameter]
    public int IncrementAmount { get; set; } = 1;

    void IncrementCount()
    {
        currentCount+=IncrementAmount;
    }
}

若要在 Blazor 中指定组件参数,请像在 ASP.NET Web Forms 中一样使用特性:

<Counter IncrementAmount="10" />

查询字符串参数

Razor 组件也可以利用它们所呈现的页面的查询字符串的值作为参数源。 若要启用此参数,请将 [SupplyParameterFromQuery] 属性添加到参数中。 例如,下面的参数定义将以 ?IncBy=2 格式从请求中获取其值:

[Parameter]
[SupplyParameterFromQuery(Name = "IncBy")]
public int IncrementAmount { get; set; } = 1;

如果未在 [SupplyParameterFromQuery] 属性中提供自定义 Name,则默认情况下它将匹配属性名称(在这种情况下为 IncrementAmount)。

组件和错误边界

默认情况下,Blazor 应用将检测未处理的异常,并在页面底部显示错误消息,无其他详细信息。 若要限制受未处理错误影响的应用的某些部分,例如将影响限制在单个组件,可以将 <ErrorBoundary> 标记包装在组件声明周围。

例如,若要防止从 Counter 组件引发的可能异常,请在 <ErrorBoundary> 中声明它,并且可以选择指定在出现异常时显示的消息:

<ErrorBoundary>
    <ChildContent>
        <Counter />
    </ChildContent>
    <ErrorContent>
        Oops! The counter isn't working right now; please try again later.
    </ErrorContent>
</ErrorBoundary>

如果不需要指定自定义错误内容,则只需直接包装组件:

<ErrorBoundary>
  <Counter />
</ErrorBoundary>

如果包装组件中发生未处理的异常,将显示指示“出现错误”的默认消息。

事件处理程序

ASP.NET Web Forms 和 Blazor 均提供用于处理 UI 事件的基于事件的编程模型。 此类事件的示例包括按钮单击和文本输入。 在 ASP.NET Web Forms 中,可以使用 HTML 服务器控件处理 DOM 公开的 UI 事件,也可以处理 Web 服务器控件公开的事件。 这些事件通过表单回发请求在服务器上显示。 考虑以下 Web Forms 按钮单击示例:

Counter.ascx

<asp:Button ID="ClickMeButton" runat="server" Text="Click me!" OnClick="ClickMeButton_Click" />

Counter.ascx.cs

public partial class Counter : System.Web.UI.UserControl
{
    protected void ClickMeButton_Click(object sender, EventArgs e)
    {
        Console.WriteLine("The button was clicked!");
    }
}

在 Blazor 中,可以直接使用 @on{event} 形式的指令属性为 DOM UI 事件注册处理程序。 {event} 占位符表示事件的名称。 例如,可侦听按钮单击,如下所示:

<button @onclick="OnClick">Click me!</button>

@code {
    void OnClick()
    {
        Console.WriteLine("The button was clicked!");
    }
}

事件处理程序可以接受可选的事件特定参数,以提供有关事件的详细信息。 例如,鼠标事件可以接受 MouseEventArgs 参数,但这不是必需的。

<button @onclick="OnClick">Click me!</button>

@code {
    void OnClick(MouseEventArgs e)
    {
        Console.WriteLine($"Mouse clicked at {e.ScreenX}, {e.ScreenY}.");
    }
}

可以使用 lambda 表达式来代替引用事件处理程序的方法组。 借助 lambda 表达式,可以覆盖其他范围内的值。

@foreach (var buttonLabel in buttonLabels)
{
    <button @onclick="() => Console.WriteLine($"The {buttonLabel} button was clicked!")">@buttonLabel</button>
}

事件处理程序可以同步或异步执行。 例如,以下 OnClick 事件处理程序异步执行:

<button @onclick="OnClick">Click me!</button>

@code {
    async Task OnClick()
    {
        var result = await Http.GetAsync("api/values");
    }
}

处理事件后,将呈现组件以解释任何组件状态更改。 使用异步事件处理程序时,组件将在处理程序执行完成后立即呈现。 异步 Task 完成后,将再次呈现组件。 在异步 Task 仍在执行时,此异步执行模式提供呈现部分相应 UI 的机会。

<button @onclick="ShowMessage">Get message</button>

@if (showMessage)
{
    @if (message == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <p>The message is: @message</p>
    }
}

@code
{
    bool showMessage = false;
    string message;

    public async Task ShowMessage()
    {
        showMessage = true;
        message = await MessageService.GetMessageAsync();
    }
}

组件还可以通过定义类型为 EventCallback<TValue> 的组件参数来定义自己的事件。 事件回叫支持 DOM UI 事件处理程序的所有变体:可选参数、同步或异步、方法组或 lambda 表达式。

<button class="btn btn-primary" @onclick="OnClick">Click me!</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClick { get; set; }
}

数据绑定

Blazor 提供将 UI 组件中的数据绑定到组件状态的简单机制。 此方法与 ASP.NET Web Forms 中用于将数据源中的数据绑定到 UI 控件的功能不同。 我们将在处理数据部分中介绍如何处理来自不同数据源的数据。

若要创建从 UI 组件到组件状态的双向数据绑定,请使用 @bind 指令属性。 在下面的示例中,复选框的值绑定到 isChecked 字段。

<input type="checkbox" @bind="isChecked" />

@code {
    bool isChecked;
}

呈现组件时,复选框的值将设置为 isChecked 字段的值。 当用户切换复选框时,将触发 onchange 事件,且 isChecked 字段将设置为新值。 在这种情况下,@bind 语法等效于以下标记:

<input value="@isChecked" @onchange="(UIChangeEventArgs e) => isChecked = e.Value" />

若要更改用于绑定的事件,请使用 @bind:event 属性。

<input @bind="text" @bind:event="oninput" />
<p>@text</p>

@code {
    string text;
}

组件还可以支持将数据绑定到其参数。 若要进行数据绑定,请定义与可绑定参数同名的事件回叫参数。 名称会添加“Changed”后缀。

PasswordBox.razor

Password: <input
    value="@Password"
    @oninput="OnPasswordChanged"
    type="@(showPassword ? "text" : "password")" />

<label><input type="checkbox" @bind="showPassword" />Show password</label>

@code {
    private bool showPassword;

    [Parameter]
    public string Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        Password = e.Value.ToString();
        return PasswordChanged.InvokeAsync(Password);
    }
}

若要将数据绑定链接到基础 UI 元素,请设置值并直接在 UI 元素上处理事件,而不要使用 @bind 属性。

若要绑定到组件参数,请使用 @bind-{Parameter} 属性指定要绑定到的参数。

<PasswordBox @bind-Password="password" />

@code {
    string password;
}

状态更改

如果组件的状态已在常规 UI 事件或事件回叫之外更改,则组件必须手动发出需要再次呈现的信号。 若要发出组件状态已更改的信号,请在组件上调用 StateHasChanged 方法。

在下面的示例中,组件显示来自 AppState 服务的消息(可由应用的其他部分更新)。 组件向 AppState.OnChange 事件注册其 StateHasChanged 方法,以便每次消息更新时呈现该组件。

public class AppState
{
    public string Message { get; }

    // Lets components receive change notifications
    public event Action OnChange;

    public void UpdateMessage(string message)
    {
        Message = message;
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}
@inject AppState AppState

<p>App message: @AppState.Message</p>

@code {
    protected override void OnInitialized()
    {
        AppState.OnChange += StateHasChanged
    }
}

组件生命周期

ASP.NET Web Forms 框架为模块、页和控件提供定义完善的生命周期方法。 例如,以下控件为 InitLoadUnLoad 生命周期事件实现事件处理程序:

Counter.ascx.cs

public partial class Counter : System.Web.UI.UserControl
{
    protected void Page_Init(object sender, EventArgs e) { ... }
    protected void Page_Load(object sender, EventArgs e) { ... }
    protected void Page_UnLoad(object sender, EventArgs e) { ... }
}

Razor 组件也具有定义完善的生命周期。 组件的生命周期可用于初始化组件状态及实现高级组件行为。

Blazor 的所有组件生命周期方法都具有同步和异步版本。 组件呈现是同步的。 不能在组件呈现期间运行异步逻辑。 所有异步逻辑都必须作为 async 生命周期方法的一部分执行。

OnInitialized

OnInitializedOnInitializedAsync 方法用于初始化组件。 组件通常在首次呈现后初始化。 组件初始化后,可能会在最终释放前呈现多次。 OnInitialized 方法类似于 ASP.NET Web Forms 页和控件中的 Page_Load 事件。

protected override void OnInitialized() { ... }
protected override async Task OnInitializedAsync() { await ... }

OnParametersSet

当组件已从其父级接收参数并将值分配给属性时,将调用 OnParametersSetOnParametersSetAsync 方法。 这些方法在组件初始化后以及每次呈现组件时执行。

protected override void OnParametersSet() { ... }
protected override async Task OnParametersSetAsync() { await ... }

OnAfterRender

OnAfterRenderOnAfterRenderAsync 方法在组件完成呈现后调用。 此时,将填充元素和组件引用(在下文中详细介绍这些概念)。 此时已启用与浏览器的交互性功能。 与 DOM 和 JavaScript 执行的交互可以安全地进行。

protected override void OnAfterRender(bool firstRender)
{
    if (firstRender)
    {
        ...
    }
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await ...
    }
}

在服务器上进行预呈现时,不调用 OnAfterRenderOnAfterRenderAsync

firstRender 参数在首次呈现组件时为 true;否则,其值为 false

IDisposable

从 UI 中移除 Razor 组件时,该组件可以实现 IDisposable 来释放资源。 Razor 组件可以通过使用 @implements 指令来实现 IDispose

@using System
@implements IDisposable

...

@code {
    public void Dispose()
    {
        ...
    }
}

捕获组件引用

在 ASP.NET Web Forms 中,通常通过引用控件实例 ID 直接在代码中操作控件实例。 在 Blazor 中,也可以捕获和操作对组件的引用,尽管这种做法不太常见。

若要在 Blazor 中捕获组件引用,请使用 @ref 指令属性。 属性的值应该匹配与引用的组件具有相同类型的可设置字段的名称。

<MyLoginDialog @ref="loginDialog" ... />

@code {
    MyLoginDialog loginDialog = default!;

    void OnSomething()
    {
        loginDialog.Show();
    }
}

呈现父组件时,将使用子组件实例填充该字段。 然后,可以在组件实例上调用方法,或以其他方式操作组件实例。

不建议直接使用组件引用来操作组件状态。 这样做会阻止组件在正确的时间自动呈现。

捕获元素引用

Razor 组件可以捕获对元素的引用。 与 ASP.NET Web Forms 中的 HTML 服务器控件不同,不能在 Blazor 中直接使用元素引用来操作 DOM。 Blazor 使用其 DOM 比较算法处理大部分 DOM 交互。 Blazor 中捕获的元素引用是不透明的。 但是,它们用于在 JavaScript 互操作调用中传递特定元素引用。 有关 JavaScript 互操作的详细信息,请参阅 ASP.NET Core Blazor JavaScript 互操作

模板化组件

在 ASP.NET Web Forms 中,可以创建模板化控件。 模板化控件使开发人员可以指定用于呈现容器控件的一部分 HTML。 生成模板化服务器控件的机制很复杂,但它们提供了功能强大的方案,可用于以用户可自定义的方式呈现数据。 模板化控件的示例包括 RepeaterDataList

还可以通过定义类型为 RenderFragmentRenderFragment<T> 的组件参数来模板化 Razor 组件。 RenderFragment 表示可随后由组件呈现的 Razor 标记块。 RenderFragment<T> 是 Razor 标记块,其接受可在呈现片段时指定的参数。

子内容

Razor 组件可以将其子内容作为 RenderFragment 进行捕获,并将该内容作为组件呈现的一部分呈现。 若要捕获子内容,请定义类型为 RenderFragment 的组件参数并将其命名为 ChildContent

ChildContentComponent.razor

<h1>Component with child content</h1>

<div>@ChildContent</div>

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

然后,父组件可以使用常规 Razor 语法提供子内容。

<ChildContentComponent>
    <ChildContent>
        <p>The time is @DateTime.Now</p>
    </ChildContent>
</ChildContentComponent>

模板参数

模板化 Razor 组件还可以定义类型为 RenderFragmentRenderFragment<T> 的多个组件参数。 RenderFragment<T> 的参数可以在调用时指定。 若要为组件指定泛型类型参数,请使用 @typeparam Razor 指令。

SimpleListView.razor

@typeparam TItem

@Heading

<ul>
@foreach (var item in Items)
{
    <li>@ItemTemplate(item)</li>
}
</ul>

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

    [Parameter]
    public RenderFragment<TItem> ItemTemplate { get; set; }

    [Parameter]
    public IEnumerable<TItem> Items { get; set; }
}

在使用模板化组件时,可以使用与参数名称匹配的子元素来指定模板参数。 作为元素传递的类型为 RenderFragment<T> 的组件参数有一个名为 context 的隐式参数。 可以使用子元素上的 Context 属性来更改此实现参数的名称。 可以使用与类型参数名称匹配的属性来指定任何泛型类型参数。 如果可能,将推断类型参数:

<SimpleListView Items="messages" TItem="string">
    <Heading>
        <h1>My list</h1>
    </Heading>
    <ItemTemplate Context="message">
        <p>The message is: @message</p>
    </ItemTemplate>
</SimpleListView>

此组件的输出如下所示:

<h1>My list</h1>
<ul>
    <li><p>The message is: message1</p></li>
    <li><p>The message is: message2</p></li>
<ul>

代码隐藏

Razor 组件通常是在单个 .razor 文件中创作的。 但也可以使用代码隐藏文件来分隔代码和标记。 若要使用组件文件,请添加与组件文件的文件名匹配,但添加了 .cs 扩展名的 C# 文件 (Counter.razor.cs)。 使用 C# 文件定义组件的基类。 可以根据需要命名基类,但通常将基类命名为与组件类相同的名称,并添加 Base 扩展名 (CounterBase)。 基于组件的类也必须从 ComponentBase 中派生。 然后,在 Razor 组件文件中,添加 @inherits 指令以指定组件的基类 (@inherits CounterBase)。

Counter.razor

@inherits CounterBase

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button @onclick="IncrementCount">Click me</button>

Counter.razor.cs

public class CounterBase : ComponentBase
{
    protected int currentCount = 0;

    protected void IncrementCount()
    {
        currentCount++;
    }
}

组件成员在基类中的可见性必须为 protectedpublic 才能对组件类可见。

其他资源

前面的内容并不是对 Razor 组件的所有方面的详尽介绍。 有关如何创建和使用 ASP.NET Core Razor 组件的详细信息,请参阅 Blazor 文档。