ASP.NET Core Razor 组件生命周期

注意

此版本不是本文的最新版本。 若要切换到最新版本,请使用目录顶部的 ASP.NET Core 版本选择器。

版本选择器

如果选择器在较窄的浏览器窗口中不可见,请扩大窗口或选择垂直省略号 (⋮) >“目录”。

目录选择器

本文介绍 ASP.NET Core Razor 组件生命周期以及如何使用生命周期事件。

Razor 组件处理一组同步和异步生命周期方法中的 Razor 组件生命周期事件。 可以替代生命周期方法,以在组件初始化和呈现期间对组件执行其他操作。

本文简化了组件生命周期事件处理,以阐明复杂的框架逻辑。 可能需要访问 ComponentBase 引用源,以将自定义事件处理与 Blazor 的生命周期事件处理集成。 引用源中的代码注释包括有关未出现在本文或 API 文档中的生命周期事件处理的其他注释。 Blazor 的生命周期事件处理随时间推移而变化,并且可能会在每次发布时发生变化,恕不另行通知。

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

生命周期事件

以下简化图展示了 Razor 组件生命周期事件处理。 本文以下部分中的示例定义了与生命周期事件关联的 C# 方法。

组件生命周期事件:

  1. 如果组件是第一次呈现在请求上:
  2. 调用 OnParametersSet{Async}。 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

注意

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 有关详细信息,请参阅本文后面的处理呈现时的不完整异步操作部分。

父组件在其子组件之前呈现,因为呈现决定存在哪些子组件。 如果使用同步父组件初始化,则保证先完成父组件初始化。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

Blazor 中的 Razor 组件的组件生命周期事件

文档对象模型 (DOM) 事件处理:

  1. 运行事件处理程序。
  2. 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

文档对象模型 (DOM) 事件处理

Render 生命周期:

  1. 避免对组件进行进一步的呈现操作:
    • 在第一次呈现后。
    • ShouldRenderfalse 时。
  2. 生成呈现树差异并呈现组件。
  3. 等待 DOM 更新。
  4. 调用 OnAfterRender{Async}

呈现生命周期

开发人员调用 StateHasChanged 会产生呈现。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

设置参数时 (SetParametersAsync)

SetParametersAsync 设置由组件的父组件在呈现树或路由参数中提供的参数。

每次调用 SetParametersAsync 时,方法的 ParameterView 参数都包含该组件的组件参数值集。 通过重写 SetParametersAsync 方法,开发人员代码可以直接与 ParameterView 参数交互。

SetParametersAsync 的默认实现使用 [Parameter][CascadingParameter] 特性(在 ParameterView 中具有对应的值)设置每个属性的值。 在 ParameterView 中没有对应值的参数保持不变。

如果未调用 base.SetParametersAsync,则开发人员代码可使用任何需要的方式解释传入的参数值。 例如,不要求将传入参数分配给类的属性。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

在下面的示例中,如果分析 Param 路由参数成功,则 ParameterView.TryGetValue 会将 Param 参数值分配给 value。 如果 value 不是 null,则由组件显示值。

尽管路由参数匹配不区分大小写,但 TryGetValue 只匹配路由模板中区分大小写的参数名称。 以下示例需要使用路由模板中的 /{Param?} 来获取具有 TryGetValue(而不是 /{param?})的值。 如果在此方案中使用 /{param?},则 TryGetValue 返回 false,并且 message 未设置为任一 message 字符串。

Pages/SetParamsAsync.razor:

@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}

组件初始化 (OnInitialized{Async})

组件在接收 SetParametersAsync 中的初始参数后初始化,此时,将调用 OnInitializedOnInitializedAsync

如果使用同步父组件初始化,则保证父组件初始化在子组件初始化之前完成。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

对于同步操作,替代 OnInitialized

Pages/OnInit.razor:

@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}

若要执行异步操作,请替代 OnInitializedAsync 并使用 await 运算符:

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

在服务器上预呈现其内容的 Blazor 应用调用 OnInitializedAsync 两次:

  • 在组件最初作为页面的一部分静态呈现时调用一次。
  • 浏览器第二次呈现组件时。

为了防止 OnInitializedAsync 中的开发人员代码在预呈现时运行两次,请参阅预呈现后的有状态重新连接部分。 尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

在 Blazor 应用进行预呈现时,无法执行调用 JavaScript(JS 互操作)等特定操作。 预呈现时,组件可能需要进行不同的呈现。 有关详细信息,请参阅使用 JavaScript 互操作预呈现部分。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

设置参数之后 (OnParametersSet{Async})

OnParametersSetOnParametersSetAsync 在以下情况下调用:

  • OnInitializedOnInitializedAsync 中初始化组件后。

  • 当父组件重新呈现并提供以下内容时:

    • 至少一个参数已更改时的已知或基元不可变类型。
    • 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

    有关呈现约定的详细信息,请参阅 ASP.NET Core Razor 组件呈现

对于以下示例组件,请导航到 URL 中的组件页面:

  • StartDate 收到的开始日期:/on-parameters-set/2021-03-19
  • 没有开始日期,其中 StartDate 分配有当前本地时间的值:/on-parameters-set

Pages/OnParamsSet.razor:

注意

在组件路由中,无法约束具有路由约束 datetimeDateTime 参数,也无法使参数成为可选参数。 因此,以下 OnParamsSet 组件使用两个 @page 指令来处理具有和没有 URL 中提供的日期段的路由。

@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

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

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}

应用参数和属性值时,异步操作必须在 OnParametersSetAsync 生命周期事件期间发生:

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

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

有关路由参数和约束的详细信息,请参阅 ASP.NET Core Blazor 路由和导航

组件呈现之后 (OnAfterRender{Async})

OnAfterRenderOnAfterRenderAsync 在组件完成呈现后调用。 此时会填充元素和组件引用。 在此阶段中,可使用呈现的内容执行其他初始化步骤,例如与呈现的 DOM 元素交互的 JS 互操作调用。

OnAfterRenderOnAfterRenderAsyncfirstRender 参数:

  • 在第一次呈现组件实例时设置为 true
  • 可用于确保初始化操作仅执行一次。

Pages/AfterRender.razor:

@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger 

<button @onclick="LogInformation">Log information (and trigger a render)</button>

@code {
    private string message = "Initial assigned message.";

    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender(1): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);

        if (firstRender)
        {
            message = "Executed for the first render.";
        }
        else
        {
            message = "Executed after the first render.";
        }

        Logger.LogInformation("OnAfterRender(2): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);
    }

    private void LogInformation()
    {
        Logger.LogInformation("LogInformation called");
    }
}

呈现后立即进行的异步操作必须在 OnAfterRenderAsync 生命周期事件期间发生:

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

即使从 OnAfterRenderAsync 返回 Task,框架也不会在任务完成后为组件再安排一个呈现循环。 这是为了避免无限呈现循环。 这与其他生命周期方法不同,后者在返回的Task 完成后会再安排呈现循环。

在服务器上的预呈现过程中,不会调用 OnAfterRenderOnAfterRenderAsync。 在预呈现后以交互方式呈现组件时,将调用这些方法。 当应用预呈现时:

  1. 组件将在服务器上执行,以在 HTTP 响应中生成一些静态 HTML 标记。 在此阶段,不会调用 OnAfterRenderOnAfterRenderAsync
  2. 当 Blazor 脚本(blazor.webassembly.jsblazor.server.js)在浏览器中启动时,组件将以交互呈现模式重新启动。 组件重新启动后,将调用 OnAfterRenderOnAfterRenderAsync,因为应用不再处于预呈现阶段。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

状态更改 (StateHasChanged)

StateHasChanged 通知组件其状态已更改。 如果适用,调用 StateHasChanged 会导致组件重新呈现。

将自动为 EventCallback 方法调用 StateHasChanged。 有关事件回调的详细信息,请参阅 ASP.NET Core Blazor 事件处理

有关组件呈现以及何时调用 StateHasChanged(包括何时通过 ComponentBase.InvokeAsync 调用它)的详细信息,请参阅 ASP.NET Core Razor 组件呈现

处理呈现时的不完整异步操作

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 执行生命周期方法时,对象可能为 null 或未完全填充数据。 提供呈现逻辑以确认对象已初始化。 对象为 null 时,呈现占位符 UI 元素(例如,加载消息)。

在 Blazor 模板的 FetchData 组件中,替代 OnInitializedAsync 以异步接收预测数据 (forecasts)。 当 forecastsnull 时,将向用户显示加载消息。 OnInitializedAsync 返回的 Task 完成后,该组件以更新后的状态重新呈现。

Blazor Server 模板中的 Pages/FetchData.razor

@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <!-- forecast data in table element content -->
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
    }
}

处理错误

有关在生命周期方法执行期间处理错误的信息,请参阅处理 ASP.NET Core Blazor 应用中的错误

预呈现后的有状态重新连接

在 Blazor Server 应用中,当 RenderModeServerPrerendered 时,组件最初作为页面的一部分静态呈现。 浏览器重新建立与服务器的 SignalR 连接后,将再次呈现组件,并且该组件为交互式。 如果存在用于初始化组件的 OnInitialized{Async} 生命周期方法,则该方法执行两次:

  • 在静态预呈现组件时执行一次。
  • 在建立服务器连接后执行一次。

在最终呈现组件时,这可能导致 UI 中显示的数据发生明显变化。 若要避免在 Blazor Server 应用中出现此双重呈现行为,请传递一个标识符以在预呈现期间缓存状态并在预呈现后检索状态。

以下代码演示基于模板的 Blazor Server 应用中更新后的 WeatherForecastService,其避免了双重呈现。 在以下示例中,等待的 Delay (await Task.Delay(...)) 模拟先短暂延迟,然后再从 GetForecastAsync 方法返回数据。

WeatherForecastService.cs:

using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}

有关 RenderMode 的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南

尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

使用 JavaScript 互操作预呈现

本部分适用于预呈现 Razor 组件的 Blazor Server 和托管 Blazor WebAssembly 应用。 预呈现在预呈现和集成 ASP.NET Core 组件中进行了介绍。

在应用进行预呈现时,无法执行调用 JavaScript (JS) 等特定操作。

对于以下示例,setElementText1 函数置于 <head> 元素内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

在服务器上的预呈现过程中,不会调用 OnAfterRender{Async} 生命周期事件。 替代 OnAfterRender{Async} 方法以延迟 JS 互操作调用,直到预呈现后在客户端上呈现组件并进行交互之后。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText1 = (element, text) => element.innerText = text;

以下组件展示了如何以一种与预呈现兼容的方式将 JS 互操作用作组件初始化逻辑的一部分。 该组件显示可从 OnAfterRenderAsync 内部触发呈现更新。 开发人员必须小心避免在此场景中创建无限循环。

对于以下示例,setElementText2 函数置于 <head> 元素内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,并返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

如果调用 JSRuntime.InvokeAsync,则 ElementReference 仅在 OnAfterRenderAsync 中使用,而不在任何更早的生命周期方法中使用,因为呈现组件后才会有 JS 元素。

通过调用 StateHasChanged,可使用从 JS 互操作调用获得的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Razor 组件呈现)。 此代码不会创建无限循环,因为仅在 datanull 时才调用 StateHasChanged

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string? data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

使用 IDisposableIAsyncDisposable 释放组件

如果某个组件实现了 IDisposable 和/或 IAsyncDisposable,则当从 UI 中删除该组件时,框架会调用非托管资源释放。 可随时进行处置,包括在组件初始化期间。

组件不需要同时实现 IDisposableIAsyncDisposable。 如果两者均已实现,则框架仅执行异步重载。

开发人员代码必须确保 IAsyncDisposable 实现不会花很长时间才能完成。

JavaScript 互操作对象引用的释放

JavaScript (JS) 互操作文章中的示例演示了典型的对象释放模式:

JS 互操作对象引用作为映射实现,该映射按创建引用的 JS 互操作调用端的标识符键控。 从 .NET 或 JS 端启动对象释放时,Blazor 会从映射中删除该条目,只要不存在对对象的其他强引用,就可以对对象进行垃圾回收。

至少应始终释放在 .NET 端创建的对象,避免泄漏 .NET 托管内存。

组件处置期间的文档对象模型 (DOM) 清理任务

请勿在组件处置期间执行 DOM 清理任务的 JS 互操作代码。 相反,在客户端上使用采用 JavaScript 的 MutationObserver 模式,原因如下:

  • Dispose{Async} 中执行清理代码时,组件可能已从 DOM 中删除。
  • 在 Blazor Server 应用中,Blazor 呈现器可能已在 Dispose{Async} 中执行清理代码时由框架处置。

使用 MutationObserver 模式可以在从 DOM 中删除元素时运行函数。

有关断开线路时 Blazor Server 应用中 JSDisconnectedException 的指导,请参阅从 ASP.NET Core Blazor 中的 .NET 方法调用 JavaScript 函数从 ASP.NET Core Blazor 中的 JavaScript 函数调用 .NET 方法。 有关常规 JavaScript 互操作错误处理指南,请参阅处理 ASP.NET Core Blazor 应用中的错误中的“JavaScript 互操作”部分。

同步 IDisposable

对于同步释放任务,可以使用 IDisposable.Dispose

以下组件:

  • @implementsRazor 指令实现 IDisposable
  • 释放 obj,它是实现 IDisposable 的非托管类型。
  • 执行 null 检查是因为 obj 是在生命周期方法中创建的(不显示)。
@implements IDisposable

...

@code {
    ...

    public void Dispose()
    {
        obj?.Dispose();
    }
}

如果需要释放单个对象,则在调用 Dispose 时,可使用 Lambda 来释放对象。 以下示例显示在 ASP.NET Core Razor 组件呈现一文中,并演示如何使用 Lambda 表达式来释放 Timer

Pages/CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

注意

在前面的示例中,调用 StateHasChanged 通过调用 ComponentBase.InvokeAsync 包装,因为回调是在 Blazor 的同步上下文外部调用的。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

如果对象是在生命周期方法中创建的(如 OnInitialized/OnInitializedAsync),则在调用 Dispose 前检查是否为 null

Pages/CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer? timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}

有关详细信息,请参阅:

异步 IAsyncDisposable

对于异步释放任务,可以使用 IAsyncDisposable.DisposeAsync

以下组件:

@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}

有关详细信息,请参阅:

null 分配到已释放的对象

通常,在调用 Dispose/DisposeAsync 后无需将 null 分配到已释放的对象。 分配 null 的罕见情况包括:

  • 如果对象的类型未正确实现并且不允许重复调用 Dispose/DisposeAsync,则在释放后分配 null 以巧妙跳过对 Dispose/DisposeAsync 的进一步调用。
  • 如果一个长时间运行的进程继续引用已释放的对象,则分配 null 将允许垃圾回收器释放该对象,即使长时间运行的进程持续引用它也是如此。

这是一种不常见的场景。 对于正确实现并正常运行的对象,没有必要将 null 分配给已释放的对象。 在必须为对象分配 null 的罕见情况下,建议记录原因,并寻求一个防止需要分配 null 的解决方案。

StateHasChanged

注意

不支持在 Dispose 中调用 StateHasChangedStateHasChanged 可能在拆除呈现器时调用,因此不支持在此时请求 UI 更新。

事件处理程序

始终取消订阅 .NET 事件中的事件处理程序。 下面的 Blazor 窗体示例演示如何取消订阅 Dispose 方法中的事件处理程序:

  • 专用字段和 Lambda 方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        private EventHandler<FieldChangedEventArgs>? fieldChanged;
    
        protected override void OnInitialized()
        {
            editContext = new(model);
    
            fieldChanged = (_, __) =>
            {
                ...
            };
    
            editContext.OnFieldChanged += fieldChanged;
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= fieldChanged;
        }
    }
    
  • 专用方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        protected override void OnInitialized()
        {
            editContext = new(model);
            editContext.OnFieldChanged += HandleFieldChanged;
        }
    
        private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
        {
            ...
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
    

有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处置组件部分。

匿名函数、方法和表达式

使用匿名函数、方法或表达式时,无需实现 IDisposable 和取消订阅委托。 但是,当公开事件的对象的生存期长于注册委托的组件的生存期时,不能取消订阅委托是一个问题。 发生这种情况时,会导致内存泄漏,因为已注册的委托使原始对象保持活动状态。 因此,仅当你知道事件委托可快速释放时,才使用以下方法。 当不确定需要释放的对象的生存期时,请订阅委托方法并正确地释放委托,如前面的示例所示。

  • 匿名 Lambda 方法(无需显式释放):

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }
    
    protected override void OnInitialized()
    {
        editContext = new(starship);
        editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
    }
    
  • 匿名 Lambda 表达式方法(无需显式释放):

    private ValidationMessageStore? messageStore;
    
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }
    
    protected override void OnInitialized()
    {
        ...
    
        messageStore = new(CurrentEditContext);
    
        CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore.Clear(e.FieldIdentifier);
    }
    

    前面带有匿名 Lambda 表达式的完整代码示例显示在 ASP.NET Core Blazor 窗体和输入组件一文中。

有关详细信息,请参阅清理非托管资源以及后续关于实现 DisposeDisposeAsync 方法的主题。

可取消的后台工作

组件通常会执行长时间运行的后台工作,如进行网络调用 (HttpClient) 以及与数据库交互。 在几种情况下,最好停止后台工作以节省系统资源。 例如,当用户离开组件时,后台异步操作不会自动停止。

后台工作项可能需要取消的其他原因包括:

  • 正在执行的后台任务由错误的输入数据或处理参数启动。
  • 正在执行的一组后台工作项必须替换为一组新的工作项。
  • 必须更改当前正在执行的任务的优先级。
  • 必须关闭应用进行服务器重新部署。
  • 服务器资源受到限制,需要重新计划后台工作项。

要在组件中实现可取消的后台工作模式:

如下示例中:

  • await Task.Delay(5000, cts.Token); 表示长时间运行的异步后台工作。
  • BackgroundResourceMethod 表示如果在调用方法之前释放 Resource,则不应启动的长时间运行的后台方法。

Pages/BackgroundWork.razor:

@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}

Blazor Server 重新连接事件

本文所述的组件生命周期事件与 Blazor Server 的重新连接事件处理程序分开运行。 当 Blazor Server 应用断开其与客户端的 SignalR 连接时,只有 UI 更新会被中断。 重新建立连接后,将恢复 UI 更新。 有关线路处理程序事件和配置的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南

Razor 组件处理一组同步和异步生命周期方法中的 Razor 组件生命周期事件。 可以替代生命周期方法,以在组件初始化和呈现期间对组件执行其他操作。

生命周期事件

以下简化图展示了 Razor 组件生命周期事件处理。 本文以下部分中的示例定义了与生命周期事件关联的 C# 方法。

组件生命周期事件:

  1. 如果组件是第一次呈现在请求上:
  2. 调用 OnParametersSet{Async}。 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

注意

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 有关详细信息,请参阅本文后面的处理呈现时的不完整异步操作部分。

父组件在其子组件之前呈现,因为呈现决定存在哪些子组件。 如果使用同步父组件初始化,则保证先完成父组件初始化。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

Blazor 中的 Razor 组件的组件生命周期事件

文档对象模型 (DOM) 事件处理:

  1. 运行事件处理程序。
  2. 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

文档对象模型 (DOM) 事件处理

Render 生命周期:

  1. 避免对组件进行进一步的呈现操作:
    • 在第一次呈现后。
    • ShouldRenderfalse 时。
  2. 生成呈现树差异并呈现组件。
  3. 等待 DOM 更新。
  4. 调用 OnAfterRender{Async}

呈现生命周期

开发人员调用 StateHasChanged 会产生呈现。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

本文简化了组件生命周期事件处理的某些方面,以阐明复杂的框架逻辑。 可能需要访问 ComponentBase 引用源,以将自定义事件处理与 Blazor 的生命周期事件处理集成。 引用源中的代码注释包括有关未出现在本文或 API 文档中的生命周期事件处理的其他注释。 请注意,Blazor 的生命周期事件处理随时间推移而变化,并且可能会在每次发布时发生变化,恕不另行通知。

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

设置参数时 (SetParametersAsync)

SetParametersAsync 设置由组件的父组件在呈现树或路由参数中提供的参数。

每次调用 SetParametersAsync 时,方法的 ParameterView 参数都包含该组件的组件参数值集。 通过重写 SetParametersAsync 方法,开发人员代码可以直接与 ParameterView 参数交互。

SetParametersAsync 的默认实现使用 [Parameter][CascadingParameter] 特性(在 ParameterView 中具有对应的值)设置每个属性的值。 在 ParameterView 中没有对应值的参数保持不变。

如果未调用 base.SetParametersAsync,则开发人员代码可使用任何需要的方式解释传入的参数值。 例如,不要求将传入参数分配给类的属性。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

在下面的示例中,如果分析 Param 路由参数成功,则 ParameterView.TryGetValue 会将 Param 参数值分配给 value。 如果 value 不是 null,则由组件显示值。

尽管路由参数匹配不区分大小写,但 TryGetValue 只匹配路由模板中区分大小写的参数名称。 以下示例需要使用路由模板中的 /{Param?} 来获取具有 TryGetValue(而不是 /{param?})的值。 如果在此方案中使用 /{param?},则 TryGetValue 返回 false,并且 message 未设置为任一 message 字符串。

Pages/SetParamsAsync.razor:

@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}

组件初始化 (OnInitialized{Async})

组件在接收 SetParametersAsync 中的初始参数后初始化,此时,将调用 OnInitializedOnInitializedAsync

如果使用同步父组件初始化,则保证父组件初始化在子组件初始化之前完成。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

对于同步操作,替代 OnInitialized

Pages/OnInit.razor:

@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}

若要执行异步操作,请替代 OnInitializedAsync 并使用 await 运算符:

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

在服务器上预呈现其内容的 Blazor 应用调用 OnInitializedAsync 两次:

  • 在组件最初作为页面的一部分静态呈现时调用一次。
  • 浏览器第二次呈现组件时。

为了防止 OnInitializedAsync 中的开发人员代码在预呈现时运行两次,请参阅预呈现后的有状态重新连接部分。 尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

在 Blazor 应用进行预呈现时,无法执行调用 JavaScript(JS 互操作)等特定操作。 预呈现时,组件可能需要进行不同的呈现。 有关详细信息,请参阅使用 JavaScript 互操作预呈现部分。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

设置参数之后 (OnParametersSet{Async})

OnParametersSetOnParametersSetAsync 在以下情况下调用:

  • OnInitializedOnInitializedAsync 中初始化组件后。

  • 当父组件重新呈现并提供以下内容时:

    • 至少一个参数已更改时的已知或基元不可变类型。
    • 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

    有关呈现约定的详细信息,请参阅 ASP.NET Core Razor 组件呈现

对于以下示例组件,请导航到 URL 中的组件页面:

  • StartDate 收到的开始日期:/on-parameters-set/2021-03-19
  • 没有开始日期,其中 StartDate 分配有当前本地时间的值:/on-parameters-set

Pages/OnParamsSet.razor:

注意

在组件路由中,无法约束具有路由约束 datetimeDateTime 参数,也无法使参数成为可选参数。 因此,以下 OnParamsSet 组件使用两个 @page 指令来处理具有和没有 URL 中提供的日期段的路由。

@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

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

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}

应用参数和属性值时,异步操作必须在 OnParametersSetAsync 生命周期事件期间发生:

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

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

有关路由参数和约束的详细信息,请参阅 ASP.NET Core Blazor 路由和导航

组件呈现之后 (OnAfterRender{Async})

OnAfterRenderOnAfterRenderAsync 在组件完成呈现后调用。 此时会填充元素和组件引用。 在此阶段中,可使用呈现的内容执行其他初始化步骤,例如与呈现的 DOM 元素交互的 JS 互操作调用。

OnAfterRenderOnAfterRenderAsyncfirstRender 参数:

  • 在第一次呈现组件实例时设置为 true
  • 可用于确保初始化操作仅执行一次。

Pages/AfterRender.razor:

@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger 

<button @onclick="LogInformation">Log information (and trigger a render)</button>

@code {
    private string message = "Initial assigned message.";

    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender(1): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);

        if (firstRender)
        {
            message = "Executed for the first render.";
        }
        else
        {
            message = "Executed after the first render.";
        }

        Logger.LogInformation("OnAfterRender(2): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);
    }

    private void LogInformation()
    {
        Logger.LogInformation("LogInformation called");
    }
}

呈现后立即进行的异步操作必须在 OnAfterRenderAsync 生命周期事件期间发生:

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

即使从 OnAfterRenderAsync 返回 Task,框架也不会在任务完成后为组件再安排一个呈现循环。 这是为了避免无限呈现循环。 这与其他生命周期方法不同,后者在返回的Task 完成后会再安排呈现循环。

在服务器上的预呈现过程中,不会调用 OnAfterRenderOnAfterRenderAsync。 在预呈现后呈现组件并进行交互时,将调用这些方法。 当应用预呈现时:

  1. 组件将在服务器上执行,以在 HTTP 响应中生成一些静态 HTML 标记。 在此阶段,不会调用 OnAfterRenderOnAfterRenderAsync
  2. 当 Blazor 脚本(blazor.webassembly.jsblazor.server.js)在浏览器中启动时,组件将以交互呈现模式重新启动。 组件重新启动后,将调用 OnAfterRenderOnAfterRenderAsync,因为应用不再处于预呈现阶段。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

状态更改 (StateHasChanged)

StateHasChanged 通知组件其状态已更改。 如果适用,调用 StateHasChanged 会导致组件重新呈现。

将自动为 EventCallback 方法调用 StateHasChanged。 有关事件回调的详细信息,请参阅 ASP.NET Core Blazor 事件处理

有关组件呈现以及何时调用 StateHasChanged(包括何时通过 ComponentBase.InvokeAsync 调用它)的详细信息,请参阅 ASP.NET Core Razor 组件呈现

处理呈现时的不完整异步操作

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 执行生命周期方法时,对象可能为 null 或未完全填充数据。 提供呈现逻辑以确认对象已初始化。 对象为 null 时,呈现占位符 UI 元素(例如,加载消息)。

在 Blazor 模板的 FetchData 组件中,替代 OnInitializedAsync 以异步接收预测数据 (forecasts)。 当 forecastsnull 时,将向用户显示加载消息。 OnInitializedAsync 返回的 Task 完成后,该组件以更新后的状态重新呈现。

Blazor Server 模板中的 Pages/FetchData.razor

@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <!-- forecast data in table element content -->
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

处理错误

有关在生命周期方法执行期间处理错误的信息,请参阅处理 ASP.NET Core Blazor 应用中的错误

预呈现后的有状态重新连接

在 Blazor Server 应用中,当 RenderModeServerPrerendered 时,组件最初作为页面的一部分静态呈现。 浏览器重新建立与服务器的 SignalR 连接后,将再次呈现组件,并且该组件为交互式。 如果存在用于初始化组件的 OnInitialized{Async} 生命周期方法,则该方法执行两次:

  • 在静态预呈现组件时执行一次。
  • 在建立服务器连接后执行一次。

在最终呈现组件时,这可能导致 UI 中显示的数据发生明显变化。 若要避免在 Blazor Server 应用中出现此双重呈现行为,请传递一个标识符以在预呈现期间缓存状态并在预呈现后检索状态。

以下代码演示基于模板的 Blazor Server 应用中更新后的 WeatherForecastService,其避免了双重呈现。 在以下示例中,等待的 Delay (await Task.Delay(...)) 模拟先短暂延迟,然后再从 GetForecastAsync 方法返回数据。

WeatherForecastService.cs:

using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}

有关 RenderMode 的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南

尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

使用 JavaScript 互操作预呈现

本部分适用于预呈现 Razor 组件的 Blazor Server 和托管 Blazor WebAssembly 应用。 预呈现在预呈现和集成 ASP.NET Core 组件中进行了介绍。

在应用进行预呈现时,无法执行调用 JavaScript (JS) 等特定操作。

对于以下示例,setElementText1 函数置于 <head> 元素内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

在服务器上的预呈现过程中,不会调用 OnAfterRender{Async} 生命周期事件。 替代 OnAfterRender{Async} 方法以延迟 JS 互操作调用,直到预呈现后在客户端上呈现组件并进行交互之后。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText1 = (element, text) => element.innerText = text;

以下组件展示了如何以一种与预呈现兼容的方式将 JS 互操作用作组件初始化逻辑的一部分。 该组件显示可从 OnAfterRenderAsync 内部触发呈现更新。 开发人员必须小心避免在此场景中创建无限循环。

对于以下示例,setElementText2 函数置于 <head> 元素内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,并返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

如果调用 JSRuntime.InvokeAsync,则 ElementReference 仅在 OnAfterRenderAsync 中使用,而不在任何更早的生命周期方法中使用,因为呈现组件后才会有 JS 元素。

通过调用 StateHasChanged,可使用从 JS 互操作调用获得的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Razor 组件呈现)。 此代码不会创建无限循环,因为仅在 datanull 时才调用 StateHasChanged

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string? data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

使用 IDisposableIAsyncDisposable 释放组件

如果某个组件实现了 IDisposable 和/或 IAsyncDisposable,则当从 UI 中删除该组件时,框架会调用非托管资源释放。 可随时进行处置,包括在组件初始化期间。

组件不需要同时实现 IDisposableIAsyncDisposable。 如果两者均已实现,则框架仅执行异步重载。

开发人员代码必须确保 IAsyncDisposable 实现不会花很长时间才能完成。

组件处置期间的文档对象模型 (DOM) 清理任务

请勿在组件处置期间执行 DOM 清理任务的 JS 互操作代码。 相反,在客户端上使用采用 JavaScript 的 MutationObserver 模式,原因如下:

  • Dispose{Async} 中执行清理代码时,组件可能已从 DOM 中删除。
  • 在 Blazor Server 应用中,Blazor 呈现器可能已在 Dispose{Async} 中执行清理代码时由框架处置。

使用 MutationObserver 模式可以在从 DOM 中删除元素时运行函数。

有关断开线路时 Blazor Server 应用中 JSDisconnectedException 的指导,请参阅从 ASP.NET Core Blazor 中的 .NET 方法调用 JavaScript 函数从 ASP.NET Core Blazor 中的 JavaScript 函数调用 .NET 方法。 有关常规 JavaScript 互操作错误处理指南,请参阅处理 ASP.NET Core Blazor 应用中的错误中的“JavaScript 互操作”部分。

同步 IDisposable

对于同步释放任务,可以使用 IDisposable.Dispose

以下组件:

  • @implementsRazor 指令实现 IDisposable
  • 释放 obj,它是实现 IDisposable 的非托管类型。
  • 执行 null 检查是因为 obj 是在生命周期方法中创建的(不显示)。
@implements IDisposable

...

@code {
    ...

    public void Dispose()
    {
        obj?.Dispose();
    }
}

如果需要释放单个对象,则在调用 Dispose 时,可使用 Lambda 来释放对象。 以下示例显示在 ASP.NET Core Razor 组件呈现一文中,并演示如何使用 Lambda 表达式来释放 Timer

Pages/CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

注意

在前面的示例中,调用 StateHasChanged 通过调用 ComponentBase.InvokeAsync 包装,因为回调是在 Blazor 的同步上下文外部调用的。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

如果对象是在生命周期方法中创建的(如 OnInitialized/OnInitializedAsync),则在调用 Dispose 前检查是否为 null

Pages/CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer? timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}

有关详细信息,请参阅:

异步 IAsyncDisposable

对于异步释放任务,可以使用 IAsyncDisposable.DisposeAsync

以下组件:

@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}

有关详细信息,请参阅:

null 分配到已释放的对象

通常,在调用 Dispose/DisposeAsync 后无需将 null 分配到已释放的对象。 分配 null 的罕见情况包括:

  • 如果对象的类型未正确实现并且不允许重复调用 Dispose/DisposeAsync,则在释放后分配 null 以巧妙跳过对 Dispose/DisposeAsync 的进一步调用。
  • 如果一个长时间运行的进程继续引用已释放的对象,则分配 null 将允许垃圾回收器释放该对象,即使长时间运行的进程持续引用它也是如此。

这是一种不常见的场景。 对于正确实现并正常运行的对象,没有必要将 null 分配给已释放的对象。 在必须为对象分配 null 的罕见情况下,建议记录原因,并寻求一个防止需要分配 null 的解决方案。

StateHasChanged

注意

不支持在 Dispose 中调用 StateHasChangedStateHasChanged 可能在拆除呈现器时调用,因此不支持在此时请求 UI 更新。

事件处理程序

始终取消订阅 .NET 事件中的事件处理程序。 下面的 Blazor 窗体示例演示如何取消订阅 Dispose 方法中的事件处理程序:

  • 专用字段和 Lambda 方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        private EventHandler<FieldChangedEventArgs>? fieldChanged;
    
        protected override void OnInitialized()
        {
            editContext = new(model);
    
            fieldChanged = (_, __) =>
            {
                ...
            };
    
            editContext.OnFieldChanged += fieldChanged;
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= fieldChanged;
        }
    }
    
  • 专用方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        ...
    
        protected override void OnInitialized()
        {
            editContext = new(model);
            editContext.OnFieldChanged += HandleFieldChanged;
        }
    
        private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
        {
            ...
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
    

有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处置组件部分。

匿名函数、方法和表达式

使用匿名函数、方法或表达式时,无需实现 IDisposable 和取消订阅委托。 但是,当公开事件的对象的生存期长于注册委托的组件的生存期时,不能取消订阅委托是一个问题。 发生这种情况时,会导致内存泄漏,因为已注册的委托使原始对象保持活动状态。 因此,仅当你知道事件委托可快速释放时,才使用以下方法。 当不确定需要释放的对象的生存期时,请订阅委托方法并正确地释放委托,如前面的示例所示。

  • 匿名 Lambda 方法(无需显式释放):

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }
    
    protected override void OnInitialized()
    {
        editContext = new(starship);
        editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
    }
    
  • 匿名 Lambda 表达式方法(无需显式释放):

    private ValidationMessageStore? messageStore;
    
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }
    
    protected override void OnInitialized()
    {
        ...
    
        messageStore = new(CurrentEditContext);
    
        CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore.Clear(e.FieldIdentifier);
    }
    

    前面带有匿名 Lambda 表达式的完整代码示例显示在 ASP.NET Core Blazor 窗体和输入组件一文中。

有关详细信息,请参阅清理非托管资源以及后续关于实现 DisposeDisposeAsync 方法的主题。

可取消的后台工作

组件通常会执行长时间运行的后台工作,如进行网络调用 (HttpClient) 以及与数据库交互。 在几种情况下,最好停止后台工作以节省系统资源。 例如,当用户离开组件时,后台异步操作不会自动停止。

后台工作项可能需要取消的其他原因包括:

  • 正在执行的后台任务由错误的输入数据或处理参数启动。
  • 正在执行的一组后台工作项必须替换为一组新的工作项。
  • 必须更改当前正在执行的任务的优先级。
  • 必须关闭应用进行服务器重新部署。
  • 服务器资源受到限制,需要重新计划后台工作项。

要在组件中实现可取消的后台工作模式:

如下示例中:

  • await Task.Delay(5000, cts.Token); 表示长时间运行的异步后台工作。
  • BackgroundResourceMethod 表示如果在调用方法之前释放 Resource,则不应启动的长时间运行的后台方法。

Pages/BackgroundWork.razor:

@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}

Blazor Server 重新连接事件

本文所述的组件生命周期事件与 Blazor Server 的重新连接事件处理程序分开运行。 当 Blazor Server 应用断开其与客户端的 SignalR 连接时,只有 UI 更新会被中断。 重新建立连接后,将恢复 UI 更新。 有关线路处理程序事件和配置的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南

Razor 组件处理一组同步和异步生命周期方法中的 Razor 组件生命周期事件。 可以替代生命周期方法,以在组件初始化和呈现期间对组件执行其他操作。

生命周期事件

以下简化图展示了 Razor 组件生命周期事件处理。 本文以下部分中的示例定义了与生命周期事件关联的 C# 方法。

组件生命周期事件:

  1. 如果组件是第一次呈现在请求上:
  2. 调用 OnParametersSet{Async}。 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

注意

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 有关详细信息,请参阅本文后面的处理呈现时的不完整异步操作部分。

父组件在其子组件之前呈现,因为呈现决定存在哪些子组件。 如果使用同步父组件初始化,则保证先完成父组件初始化。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

Blazor 中的 Razor 组件的组件生命周期事件

文档对象模型 (DOM) 事件处理:

  1. 运行事件处理程序。
  2. 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

文档对象模型 (DOM) 事件处理

Render 生命周期:

  1. 避免对组件进行进一步的呈现操作:
    • 在第一次呈现后。
    • ShouldRenderfalse 时。
  2. 生成呈现树差异并呈现组件。
  3. 等待 DOM 更新。
  4. 调用 OnAfterRender{Async}

呈现生命周期

开发人员调用 StateHasChanged 会产生呈现。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

本文简化了组件生命周期事件处理的某些方面,以阐明复杂的框架逻辑。 可能需要访问 ComponentBase 引用源,以将自定义事件处理与 Blazor 的生命周期事件处理集成。 引用源中的代码注释包括有关未出现在本文或 API 文档中的生命周期事件处理的其他注释。 请注意,Blazor 的生命周期事件处理随时间推移而变化,并且可能会在每次发布时发生变化,恕不另行通知。

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

设置参数时 (SetParametersAsync)

SetParametersAsync 设置由组件的父组件在呈现树或路由参数中提供的参数。

每次调用 SetParametersAsync 时,方法的 ParameterView 参数都包含该组件的组件参数值集。 通过重写 SetParametersAsync 方法,开发人员代码可以直接与 ParameterView 参数交互。

SetParametersAsync 的默认实现使用 [Parameter][CascadingParameter] 特性(在 ParameterView 中具有对应的值)设置每个属性的值。 在 ParameterView 中没有对应值的参数保持不变。

如果未调用 base.SetParametersAsync,则开发人员代码可使用任何需要的方式解释传入的参数值。 例如,不要求将传入参数分配给类的属性。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

在下面的示例中,如果分析 Param 路由参数成功,则 ParameterView.TryGetValue 会将 Param 参数值分配给 value。 如果 value 不是 null,则由组件显示值。

尽管路由参数匹配不区分大小写,但 TryGetValue 只匹配路由模板中区分大小写的参数名称。 以下示例需要使用路由模板中的 /{Param?} 来获取具有 TryGetValue(而不是 /{param?})的值。 如果在此方案中使用 /{param?},则 TryGetValue 返回 false,并且 message 未设置为任一 message 字符串。

Pages/SetParamsAsync.razor:

@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}

组件初始化 (OnInitialized{Async})

组件在接收 SetParametersAsync 中的初始参数后初始化,此时,将调用 OnInitializedOnInitializedAsync

如果使用同步父组件初始化,则保证父组件初始化在子组件初始化之前完成。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

对于同步操作,替代 OnInitialized

Pages/OnInit.razor:

@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}

若要执行异步操作,请替代 OnInitializedAsync 并使用 await 运算符:

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

在服务器上预呈现其内容的 Blazor 应用调用 OnInitializedAsync 两次:

  • 在组件最初作为页面的一部分静态呈现时调用一次。
  • 浏览器第二次呈现组件时。

为了防止 OnInitializedAsync 中的开发人员代码在预呈现时运行两次,请参阅预呈现后的有状态重新连接部分。 尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

在 Blazor 应用进行预呈现时,无法执行调用 JavaScript(JS 互操作)等特定操作。 预呈现时,组件可能需要进行不同的呈现。 有关详细信息,请参阅使用 JavaScript 互操作预呈现部分。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

设置参数之后 (OnParametersSet{Async})

OnParametersSetOnParametersSetAsync 在以下情况下调用:

  • OnInitializedOnInitializedAsync 中初始化组件后。

  • 当父组件重新呈现并提供以下内容时:

    • 至少一个参数已更改时的已知或基元不可变类型。
    • 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

    有关呈现约定的详细信息,请参阅 ASP.NET Core Razor 组件呈现

对于以下示例组件,请导航到 URL 中的组件页面:

  • StartDate 收到的开始日期:/on-parameters-set/2021-03-19
  • 没有开始日期,其中 StartDate 分配有当前本地时间的值:/on-parameters-set

Pages/OnParamsSet.razor:

注意

在组件路由中,无法约束具有路由约束 datetimeDateTime 参数,也无法使参数成为可选参数。 因此,以下 OnParamsSet 组件使用两个 @page 指令来处理具有和没有 URL 中提供的日期段的路由。

@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

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

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}

应用参数和属性值时,异步操作必须在 OnParametersSetAsync 生命周期事件期间发生:

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

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

有关路由参数和约束的详细信息,请参阅 ASP.NET Core Blazor 路由和导航

组件呈现之后 (OnAfterRender{Async})

OnAfterRenderOnAfterRenderAsync 在组件完成呈现后调用。 此时会填充元素和组件引用。 在此阶段中,可使用呈现的内容执行其他初始化步骤,例如与呈现的 DOM 元素交互的 JS 互操作调用。

OnAfterRenderOnAfterRenderAsyncfirstRender 参数:

  • 在第一次呈现组件实例时设置为 true
  • 可用于确保初始化操作仅执行一次。

Pages/AfterRender.razor:

@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger 

<button @onclick="LogInformation">Log information (and trigger a render)</button>

@code {
    private string message = "Initial assigned message.";

    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender(1): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);

        if (firstRender)
        {
            message = "Executed for the first render.";
        }
        else
        {
            message = "Executed after the first render.";
        }

        Logger.LogInformation("OnAfterRender(2): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);
    }

    private void LogInformation()
    {
        Logger.LogInformation("LogInformation called");
    }
}

呈现后立即进行的异步操作必须在 OnAfterRenderAsync 生命周期事件期间发生:

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

即使从 OnAfterRenderAsync 返回 Task,框架也不会在任务完成后为组件再安排一个呈现循环。 这是为了避免无限呈现循环。 这与其他生命周期方法不同,后者在返回的Task 完成后会再安排呈现循环。

在服务器上的预呈现过程中,不会调用 OnAfterRenderOnAfterRenderAsync。 在预呈现后呈现组件并进行交互时,将调用这些方法。 当应用预呈现时:

  1. 组件将在服务器上执行,以在 HTTP 响应中生成一些静态 HTML 标记。 在此阶段,不会调用 OnAfterRenderOnAfterRenderAsync
  2. 当 Blazor 脚本(blazor.webassembly.jsblazor.server.js)在浏览器中启动时,组件将以交互呈现模式重新启动。 组件重新启动后,将调用 OnAfterRenderOnAfterRenderAsync,因为应用不再处于预呈现阶段。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

状态更改 (StateHasChanged)

StateHasChanged 通知组件其状态已更改。 如果适用,调用 StateHasChanged 会导致组件重新呈现。

将自动为 EventCallback 方法调用 StateHasChanged。 有关事件回调的详细信息,请参阅 ASP.NET Core Blazor 事件处理

有关组件呈现以及何时调用 StateHasChanged(包括何时通过 ComponentBase.InvokeAsync 调用它)的详细信息,请参阅 ASP.NET Core Razor 组件呈现

处理呈现时的不完整异步操作

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 执行生命周期方法时,对象可能为 null 或未完全填充数据。 提供呈现逻辑以确认对象已初始化。 对象为 null 时,呈现占位符 UI 元素(例如,加载消息)。

在 Blazor 模板的 FetchData 组件中,替代 OnInitializedAsync 以异步接收预测数据 (forecasts)。 当 forecastsnull 时,将向用户显示加载消息。 OnInitializedAsync 返回的 Task 完成后,该组件以更新后的状态重新呈现。

Blazor Server 模板中的 Pages/FetchData.razor

@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <!-- forecast data in table element content -->
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

处理错误

有关在生命周期方法执行期间处理错误的信息,请参阅处理 ASP.NET Core Blazor 应用中的错误

预呈现后的有状态重新连接

在 Blazor Server 应用中,当 RenderModeServerPrerendered 时,组件最初作为页面的一部分静态呈现。 浏览器重新建立与服务器的 SignalR 连接后,将再次呈现组件,并且该组件为交互式。 如果存在用于初始化组件的 OnInitialized{Async} 生命周期方法,则该方法执行两次:

  • 在静态预呈现组件时执行一次。
  • 在建立服务器连接后执行一次。

在最终呈现组件时,这可能导致 UI 中显示的数据发生明显变化。 若要避免在 Blazor Server 应用中出现此双重呈现行为,请传递一个标识符以在预呈现期间缓存状态并在预呈现后检索状态。

以下代码演示基于模板的 Blazor Server 应用中更新后的 WeatherForecastService,其避免了双重呈现。 在以下示例中,等待的 Delay (await Task.Delay(...)) 模拟先短暂延迟,然后再从 GetForecastAsync 方法返回数据。

WeatherForecastService.cs:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}

有关 RenderMode 的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南

尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

使用 JavaScript 互操作预呈现

本部分适用于预呈现 Razor 组件的 Blazor Server 和托管 Blazor WebAssembly 应用。 预呈现在预呈现和集成 ASP.NET Core 组件中进行了介绍。

在应用进行预呈现时,无法执行调用 JavaScript (JS) 等特定操作。

对于以下示例,setElementText1 函数置于 <head> 元素内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

在服务器上的预呈现过程中,不会调用 OnAfterRender{Async} 生命周期事件。 替代 OnAfterRender{Async} 方法以延迟 JS 互操作调用,直到预呈现后在客户端上呈现组件并进行交互之后。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText1 = (element, text) => element.innerText = text;

以下组件展示了如何以一种与预呈现兼容的方式将 JS 互操作用作组件初始化逻辑的一部分。 该组件显示可从 OnAfterRenderAsync 内部触发呈现更新。 开发人员必须小心避免在此场景中创建无限循环。

对于以下示例,setElementText2 函数置于 <head> 元素内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,并返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

如果调用 JSRuntime.InvokeAsync,则 ElementReference 仅在 OnAfterRenderAsync 中使用,而不在任何更早的生命周期方法中使用,因为呈现组件后才会有 JS 元素。

通过调用 StateHasChanged,可使用从 JS 互操作调用获得的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Razor 组件呈现)。 此代码不会创建无限循环,因为仅在 datanull 时才调用 StateHasChanged

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string? data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

使用 IDisposableIAsyncDisposable 释放组件

如果某个组件实现了 IDisposable 和/或 IAsyncDisposable,则当从 UI 中删除该组件时,框架会调用非托管资源释放。 可随时进行处置,包括在组件初始化期间。

组件不需要同时实现 IDisposableIAsyncDisposable。 如果两者均已实现,则框架仅执行异步重载。

开发人员代码必须确保 IAsyncDisposable 实现不会花很长时间才能完成。

组件处置期间的文档对象模型 (DOM) 清理任务

请勿在组件处置期间执行 DOM 清理任务的 JS 互操作代码。 相反,在客户端上使用采用 JavaScript 的 MutationObserver 模式,原因如下:

  • Dispose{Async} 中执行清理代码时,组件可能已从 DOM 中删除。
  • 在 Blazor Server 应用中,Blazor 呈现器可能已在 Dispose{Async} 中执行清理代码时由框架处置。

使用 MutationObserver 模式可以在从 DOM 中删除元素时运行函数。

有关断开线路时 Blazor Server 应用中 JSDisconnectedException 的指导,请参阅从 ASP.NET Core Blazor 中的 .NET 方法调用 JavaScript 函数从 ASP.NET Core Blazor 中的 JavaScript 函数调用 .NET 方法。 有关常规 JavaScript 互操作错误处理指南,请参阅处理 ASP.NET Core Blazor 应用中的错误中的“JavaScript 互操作”部分。

同步 IDisposable

对于同步释放任务,可以使用 IDisposable.Dispose

以下组件:

  • @implementsRazor 指令实现 IDisposable
  • 释放 obj,它是实现 IDisposable 的非托管类型。
  • 执行 null 检查是因为 obj 是在生命周期方法中创建的(不显示)。
@implements IDisposable

...

@code {
    ...

    public void Dispose()
    {
        obj?.Dispose();
    }
}

如果需要释放单个对象,则在调用 Dispose 时,可使用 Lambda 来释放对象。 以下示例显示在 ASP.NET Core Razor 组件呈现一文中,并演示如何使用 Lambda 表达式来释放 Timer

Pages/CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

注意

在前面的示例中,调用 StateHasChanged 通过调用 ComponentBase.InvokeAsync 包装,因为回调是在 Blazor 的同步上下文外部调用的。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

如果对象是在生命周期方法中创建的(如 OnInitialized/OnInitializedAsync),则在调用 Dispose 前检查是否为 null

Pages/CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}

有关详细信息,请参阅:

异步 IAsyncDisposable

对于异步释放任务,可以使用 IAsyncDisposable.DisposeAsync

以下组件:

@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}

有关详细信息,请参阅:

null 分配到已释放的对象

通常,在调用 Dispose/DisposeAsync 后无需将 null 分配到已释放的对象。 分配 null 的罕见情况包括:

  • 如果对象的类型未正确实现并且不允许重复调用 Dispose/DisposeAsync,则在释放后分配 null 以巧妙跳过对 Dispose/DisposeAsync 的进一步调用。
  • 如果一个长时间运行的进程继续引用已释放的对象,则分配 null 将允许垃圾回收器释放该对象,即使长时间运行的进程持续引用它也是如此。

这是一种不常见的场景。 对于正确实现并正常运行的对象,没有必要将 null 分配给已释放的对象。 在必须为对象分配 null 的罕见情况下,建议记录原因,并寻求一个防止需要分配 null 的解决方案。

StateHasChanged

注意

不支持在 Dispose 中调用 StateHasChangedStateHasChanged 可能在拆除呈现器时调用,因此不支持在此时请求 UI 更新。

事件处理程序

始终取消订阅 .NET 事件中的事件处理程序。 下面的 Blazor 窗体示例演示如何取消订阅 Dispose 方法中的事件处理程序:

  • 专用字段和 Lambda 方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        // ...
        private EventHandler<FieldChangedEventArgs> fieldChanged;
    
        protected override void OnInitialized()
        {
            editContext = new(model);
    
            fieldChanged = (_, __) =>
            {
                // ...
            };
    
            editContext.OnFieldChanged += fieldChanged;
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= fieldChanged;
        }
    }
    
  • 专用方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        // ...
    
        protected override void OnInitialized()
        {
            editContext = new(model);
            editContext.OnFieldChanged += HandleFieldChanged;
        }
    
        private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
        {
            // ...
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
    

有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处置组件部分。

匿名函数、方法和表达式

使用匿名函数、方法或表达式时,无需实现 IDisposable 和取消订阅委托。 但是,当公开事件的对象的生存期长于注册委托的组件的生存期时,不能取消订阅委托是一个问题。 发生这种情况时,会导致内存泄漏,因为已注册的委托使原始对象保持活动状态。 因此,仅当你知道事件委托可快速释放时,才使用以下方法。 当不确定需要释放的对象的生存期时,请订阅委托方法并正确地释放委托,如前面的示例所示。

  • 匿名 Lambda 方法(无需显式释放):

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }
    
    protected override void OnInitialized()
    {
        editContext = new(starship);
        editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
    }
    
  • 匿名 Lambda 表达式方法(无需显式释放):

    private ValidationMessageStore messageStore;
    
    [CascadingParameter]
    private EditContext CurrentEditContext { get; set; }
    
    protected override void OnInitialized()
    {
        ...
    
        messageStore = new(CurrentEditContext);
    
        CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore.Clear(e.FieldIdentifier);
    }
    

    前面带有匿名 Lambda 表达式的完整代码示例显示在 ASP.NET Core Blazor 窗体和输入组件一文中。

有关详细信息,请参阅清理非托管资源以及后续关于实现 DisposeDisposeAsync 方法的主题。

可取消的后台工作

组件通常会执行长时间运行的后台工作,如进行网络调用 (HttpClient) 以及与数据库交互。 在几种情况下,最好停止后台工作以节省系统资源。 例如,当用户离开组件时,后台异步操作不会自动停止。

后台工作项可能需要取消的其他原因包括:

  • 正在执行的后台任务由错误的输入数据或处理参数启动。
  • 正在执行的一组后台工作项必须替换为一组新的工作项。
  • 必须更改当前正在执行的任务的优先级。
  • 必须关闭应用进行服务器重新部署。
  • 服务器资源受到限制,需要重新计划后台工作项。

要在组件中实现可取消的后台工作模式:

如下示例中:

  • await Task.Delay(5000, cts.Token); 表示长时间运行的异步后台工作。
  • BackgroundResourceMethod 表示如果在调用方法之前释放 Resource,则不应启动的长时间运行的后台方法。

Pages/BackgroundWork.razor:

@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}

Blazor Server 重新连接事件

本文所述的组件生命周期事件与 Blazor Server 的重新连接事件处理程序分开运行。 当 Blazor Server 应用断开其与客户端的 SignalR 连接时,只有 UI 更新会被中断。 重新建立连接后,将恢复 UI 更新。 有关线路处理程序事件和配置的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南

Razor 组件处理一组同步和异步生命周期方法中的 Razor 组件生命周期事件。 可以替代生命周期方法,以在组件初始化和呈现期间对组件执行其他操作。

生命周期事件

以下简化图展示了 Razor 组件生命周期事件处理。 本文以下部分中的示例定义了与生命周期事件关联的 C# 方法。

组件生命周期事件:

  1. 如果组件是第一次呈现在请求上:
  2. 调用 OnParametersSet{Async}。 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

注意

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 有关详细信息,请参阅本文后面的处理呈现时的不完整异步操作部分。

父组件在其子组件之前呈现,因为呈现决定存在哪些子组件。 如果使用同步父组件初始化,则保证先完成父组件初始化。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

Blazor 中的 Razor 组件的组件生命周期事件

文档对象模型 (DOM) 事件处理:

  1. 运行事件处理程序。
  2. 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  3. 呈现所有同步工作和完整的 Task

文档对象模型 (DOM) 事件处理

Render 生命周期:

  1. 避免对组件进行进一步的呈现操作:
    • 在第一次呈现后。
    • ShouldRenderfalse 时。
  2. 生成呈现树差异并呈现组件。
  3. 等待 DOM 更新。
  4. 调用 OnAfterRender{Async}

呈现生命周期

开发人员调用 StateHasChanged 会产生呈现。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

本文简化了组件生命周期事件处理的某些方面,以阐明复杂的框架逻辑。 可能需要访问 ComponentBase 引用源,以将自定义事件处理与 Blazor 的生命周期事件处理集成。 引用源中的代码注释包括有关未出现在本文或 API 文档中的生命周期事件处理的其他注释。 请注意,Blazor 的生命周期事件处理随时间推移而变化,并且可能会在每次发布时发生变化,恕不另行通知。

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

设置参数时 (SetParametersAsync)

SetParametersAsync 设置由组件的父组件在呈现树或路由参数中提供的参数。

每次调用 SetParametersAsync 时,方法的 ParameterView 参数都包含该组件的组件参数值集。 通过重写 SetParametersAsync 方法,开发人员代码可以直接与 ParameterView 参数交互。

SetParametersAsync 的默认实现使用 [Parameter][CascadingParameter] 特性(在 ParameterView 中具有对应的值)设置每个属性的值。 在 ParameterView 中没有对应值的参数保持不变。

如果未调用 base.SetParametersAsync,则开发人员代码可使用任何需要的方式解释传入的参数值。 例如,不要求将传入参数分配给类的属性。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

在下面的示例中,如果分析 Param 路由参数成功,则 ParameterView.TryGetValue 会将 Param 参数值分配给 value。 如果 value 不是 null,则由组件显示值。

尽管路由参数匹配不区分大小写,但 TryGetValue 只匹配路由模板中区分大小写的参数名称。 以下示例需要使用路由模板中的 /{Param?} 来获取具有 TryGetValue(而不是 /{param?})的值。 如果在此方案中使用 /{param?},则 TryGetValue 返回 false,并且 message 未设置为任一 message 字符串。

Pages/SetParamsAsync.razor:

@page "/set-params-async"
@page "/set-params-async/{Param}"

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}

组件初始化 (OnInitialized{Async})

组件在接收 SetParametersAsync 中的初始参数后初始化,此时,将调用 OnInitializedOnInitializedAsync

如果使用同步父组件初始化,则保证父组件初始化在子组件初始化之前完成。 如果使用异步父组件初始化,则无法确定父组件和子组件初始化的完成顺序,因为它取决于正在运行的初始化代码。

对于同步操作,替代 OnInitialized

Pages/OnInit.razor:

@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}

若要执行异步操作,请替代 OnInitializedAsync 并使用 await 运算符:

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

在服务器上预呈现其内容的 Blazor 应用调用 OnInitializedAsync 两次:

  • 在组件最初作为页面的一部分静态呈现时调用一次。
  • 浏览器第二次呈现组件时。

为了防止 OnInitializedAsync 中的开发人员代码在预呈现时运行两次,请参阅预呈现后的有状态重新连接部分。 尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

在 Blazor 应用进行预呈现时,无法执行调用 JavaScript(JS 互操作)等特定操作。 预呈现时,组件可能需要进行不同的呈现。 有关详细信息,请参阅使用 JavaScript 互操作预呈现部分。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

设置参数之后 (OnParametersSet{Async})

OnParametersSetOnParametersSetAsync 在以下情况下调用:

  • OnInitializedOnInitializedAsync 中初始化组件后。

  • 当父组件重新呈现并提供以下内容时:

    • 至少一个参数已更改时的已知或基元不可变类型。
    • 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

    有关呈现约定的详细信息,请参阅 ASP.NET Core Razor 组件呈现

对于以下示例组件,请导航到 URL 中的组件页面:

  • StartDate 收到的开始日期:/on-parameters-set/2021-03-19
  • 没有开始日期,其中 StartDate 分配有当前本地时间的值:/on-parameters-set

Pages/OnParamsSet.razor:

@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

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

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}

应用参数和属性值时,异步操作必须在 OnParametersSetAsync 生命周期事件期间发生:

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

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

有关路由参数和约束的详细信息,请参阅 ASP.NET Core Blazor 路由和导航

组件呈现之后 (OnAfterRender{Async})

OnAfterRenderOnAfterRenderAsync 在组件完成呈现后调用。 此时会填充元素和组件引用。 在此阶段中,可使用呈现的内容执行其他初始化步骤,例如与呈现的 DOM 元素交互的 JS 互操作调用。

OnAfterRenderOnAfterRenderAsyncfirstRender 参数:

  • 在第一次呈现组件实例时设置为 true
  • 可用于确保初始化操作仅执行一次。

Pages/AfterRender.razor:

@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger 

<button @onclick="LogInformation">Log information (and trigger a render)</button>

@code {
    private string message = "Initial assigned message.";

    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender(1): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);

        if (firstRender)
        {
            message = "Executed for the first render.";
        }
        else
        {
            message = "Executed after the first render.";
        }

        Logger.LogInformation("OnAfterRender(2): firstRender: " +
            "{FirstRender}, message: {Message}", firstRender, message);
    }

    private void LogInformation()
    {
        Logger.LogInformation("LogInformation called");
    }
}

呈现后立即进行的异步操作必须在 OnAfterRenderAsync 生命周期事件期间发生:

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

即使从 OnAfterRenderAsync 返回 Task,框架也不会在任务完成后为组件再安排一个呈现循环。 这是为了避免无限呈现循环。 这与其他生命周期方法不同,后者在返回的Task 完成后会再安排呈现循环。

在服务器上的预呈现过程中,不会调用 OnAfterRenderOnAfterRenderAsync。 在预呈现后呈现组件并进行交互时,将调用这些方法。 当应用预呈现时:

  1. 组件将在服务器上执行,以在 HTTP 响应中生成一些静态 HTML 标记。 在此阶段,不会调用 OnAfterRenderOnAfterRenderAsync
  2. 当 Blazor 脚本(blazor.webassembly.jsblazor.server.js)在浏览器中启动时,组件将以交互呈现模式重新启动。 组件重新启动后,将调用 OnAfterRenderOnAfterRenderAsync,因为应用不再处于预呈现阶段。

如果在开发人员代码中提供了事件处理程序,处置时会将其解除挂接。 有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处理组件部分。

状态更改 (StateHasChanged)

StateHasChanged 通知组件其状态已更改。 如果适用,调用 StateHasChanged 会导致组件重新呈现。

将自动为 EventCallback 方法调用 StateHasChanged。 有关事件回调的详细信息,请参阅 ASP.NET Core Blazor 事件处理

有关组件呈现以及何时调用 StateHasChanged(包括何时通过 ComponentBase.InvokeAsync 调用它)的详细信息,请参阅 ASP.NET Core Razor 组件呈现

处理呈现时的不完整异步操作

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 执行生命周期方法时,对象可能为 null 或未完全填充数据。 提供呈现逻辑以确认对象已初始化。 对象为 null 时,呈现占位符 UI 元素(例如,加载消息)。

在 Blazor 模板的 FetchData 组件中,替代 OnInitializedAsync 以异步接收预测数据 (forecasts)。 当 forecastsnull 时,将向用户显示加载消息。 OnInitializedAsync 返回的 Task 完成后,该组件以更新后的状态重新呈现。

Blazor Server 模板中的 Pages/FetchData.razor

@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <!-- forecast data in table element content -->
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

处理错误

有关在生命周期方法执行期间处理错误的信息,请参阅处理 ASP.NET Core Blazor 应用中的错误

预呈现后的有状态重新连接

在 Blazor Server 应用中,当 RenderModeServerPrerendered 时,组件最初作为页面的一部分静态呈现。 浏览器重新建立与服务器的 SignalR 连接后,将再次呈现组件,并且该组件为交互式。 如果存在用于初始化组件的 OnInitialized{Async} 生命周期方法,则该方法执行两次:

  • 在静态预呈现组件时执行一次。
  • 在建立服务器连接后执行一次。

在最终呈现组件时,这可能导致 UI 中显示的数据发生明显变化。 若要避免在 Blazor Server 应用中出现此双重呈现行为,请传递一个标识符以在预呈现期间缓存状态并在预呈现后检索状态。

以下代码演示基于模板的 Blazor Server 应用中更新后的 WeatherForecastService,其避免了双重呈现。 在以下示例中,等待的 Delay (await Task.Delay(...)) 模拟先短暂延迟,然后再从 GetForecastAsync 方法返回数据。

WeatherForecastService.cs:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}

有关 RenderMode 的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南

尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管的 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 若要在预呈现时保持执行初始化代码期间的状态,请参阅预呈现和集成 ASP.NET Core Razor 组件

使用 JavaScript 互操作预呈现

本部分适用于预呈现 Razor 组件的 Blazor Server 和托管 Blazor WebAssembly 应用。 预呈现在预呈现和集成 ASP.NET Core 组件中进行了介绍。

在应用进行预呈现时,无法执行调用 JavaScript (JS) 等特定操作。

对于以下示例,setElementText1 函数置于 <head> 元素内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

在服务器上的预呈现过程中,不会调用 OnAfterRender{Async} 生命周期事件。 替代 OnAfterRender{Async} 方法以延迟 JS 互操作调用,直到预呈现后在客户端上呈现组件并进行交互之后。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText1 = (element, text) => element.innerText = text;

以下组件展示了如何以一种与预呈现兼容的方式将 JS 互操作用作组件初始化逻辑的一部分。 该组件显示可从 OnAfterRenderAsync 内部触发呈现更新。 开发人员必须小心避免在此场景中创建无限循环。

对于以下示例,setElementText2 函数置于 <head> 元素内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,并返回值。

注意

有关生产应用的 JS 位置和建议的一般指南,请参阅 ASP.NET Core Blazor JavaScript 互操作性(JS 互操作)

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。 大多数情况下,不建议使用 JS 直接修改 DOM,因为 JS 可能会干扰 Blazor 的更改跟踪。 有关详细信息,请参阅 ASP.NET Core JavaScript 互操作性(JS 互操作)。

如果调用 JSRuntime.InvokeAsync,则 ElementReference 仅在 OnAfterRenderAsync 中使用,而不在任何更早的生命周期方法中使用,因为呈现组件后才会有 JS 元素。

通过调用 StateHasChanged,可使用从 JS 互操作调用获得的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Razor 组件呈现)。 此代码不会创建无限循环,因为仅在 datanull 时才调用 StateHasChanged

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string? data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

前面的示例使用全局方法来污染客户端。 若要在生产应用中获取更好的方法,请参阅 JavaScript 模块中的 JavaScript 隔离

示例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

使用 IDisposableIAsyncDisposable 释放组件

如果某个组件实现了 IDisposable 和/或 IAsyncDisposable,则当从 UI 中删除该组件时,框架会调用非托管资源释放。 可随时进行处置,包括在组件初始化期间。

组件不需要同时实现 IDisposableIAsyncDisposable。 如果两者均已实现,则框架仅执行异步重载。

开发人员代码必须确保 IAsyncDisposable 实现不会花很长时间才能完成。

组件处置期间的文档对象模型 (DOM) 清理任务

请勿在组件处置期间执行 DOM 清理任务的 JS 互操作代码。 相反,在客户端上使用采用 JavaScript 的 MutationObserver 模式,原因如下:

  • Dispose{Async} 中执行清理代码时,组件可能已从 DOM 中删除。
  • 在 Blazor Server 应用中,Blazor 呈现器可能已在 Dispose{Async} 中执行清理代码时由框架处置。

使用 MutationObserver 模式可以在从 DOM 中删除元素时运行函数。

有关断开线路时 Blazor Server 应用中 JSDisconnectedException 的指导,请参阅从 ASP.NET Core Blazor 中的 .NET 方法调用 JavaScript 函数从 ASP.NET Core Blazor 中的 JavaScript 函数调用 .NET 方法。 有关常规 JavaScript 互操作错误处理指南,请参阅处理 ASP.NET Core Blazor 应用中的错误中的“JavaScript 互操作”部分。

同步 IDisposable

对于同步释放任务,可以使用 IDisposable.Dispose

以下组件:

  • @implementsRazor 指令实现 IDisposable
  • 释放 obj,它是实现 IDisposable 的非托管类型。
  • 执行 null 检查是因为 obj 是在生命周期方法中创建的(不显示)。
@implements IDisposable

...

@code {
    ...

    public void Dispose()
    {
        obj?.Dispose();
    }
}

如果需要释放单个对象,则在调用 Dispose 时,可使用 Lambda 来释放对象。 以下示例显示在 ASP.NET Core Razor 组件呈现一文中,并演示如何使用 Lambda 表达式来释放 Timer

Pages/CounterWithTimerDisposal1.razor:

@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

注意

在前面的示例中,调用 StateHasChanged 通过调用 ComponentBase.InvokeAsync 包装,因为回调是在 Blazor 的同步上下文外部调用的。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现

如果对象是在生命周期方法中创建的(如 OnInitialized/OnInitializedAsync),则在调用 Dispose 前检查是否为 null

Pages/CounterWithTimerDisposal2.razor:

@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

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

@code {
    private int currentCount = 0;
    private Timer timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}

有关详细信息,请参阅:

异步 IAsyncDisposable

对于异步释放任务,可以使用 IAsyncDisposable.DisposeAsync

以下组件:

@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}

有关详细信息,请参阅:

null 分配到已释放的对象

通常,在调用 Dispose/DisposeAsync 后无需将 null 分配到已释放的对象。 分配 null 的罕见情况包括:

  • 如果对象的类型未正确实现并且不允许重复调用 Dispose/DisposeAsync,则在释放后分配 null 以巧妙跳过对 Dispose/DisposeAsync 的进一步调用。
  • 如果一个长时间运行的进程继续引用已释放的对象,则分配 null 将允许垃圾回收器释放该对象,即使长时间运行的进程持续引用它也是如此。

这是一种不常见的场景。 对于正确实现并正常运行的对象,没有必要将 null 分配给已释放的对象。 在必须为对象分配 null 的罕见情况下,建议记录原因,并寻求一个防止需要分配 null 的解决方案。

StateHasChanged

注意

不支持在 Dispose 中调用 StateHasChangedStateHasChanged 可能在拆除呈现器时调用,因此不支持在此时请求 UI 更新。

事件处理程序

始终取消订阅 .NET 事件中的事件处理程序。 下面的 Blazor 窗体示例演示如何取消订阅 Dispose 方法中的事件处理程序:

  • 专用字段和 Lambda 方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        // ...
        private EventHandler<FieldChangedEventArgs> fieldChanged;
    
        protected override void OnInitialized()
        {
            editContext = new EditContext(model);
    
            fieldChanged = (_, __) =>
            {
                // ...
            };
    
            editContext.OnFieldChanged += fieldChanged;
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= fieldChanged;
        }
    }
    
  • 专用方法

    @implements IDisposable
    
    <EditForm EditContext="@editContext">
        ...
        <button type="submit" disabled="@formInvalid">Submit</button>
    </EditForm>
    
    @code {
        // ...
    
        protected override void OnInitialized()
        {
            editContext = new EditContext(model);
            editContext.OnFieldChanged += HandleFieldChanged;
        }
    
        private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
        {
            // ...
        }
    
        public void Dispose()
        {
            editContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
    

有关详细信息,请参阅使用 IDisposableIAsyncDisposable 处置组件部分。

匿名函数、方法和表达式

使用匿名函数、方法或表达式时,无需实现 IDisposable 和取消订阅委托。 但是,当公开事件的对象的生存期长于注册委托的组件的生存期时,不能取消订阅委托是一个问题。 发生这种情况时,会导致内存泄漏,因为已注册的委托使原始对象保持活动状态。 因此,仅当你知道事件委托可快速释放时,才使用以下方法。 当不确定需要释放的对象的生存期时,请订阅委托方法并正确地释放委托,如前面的示例所示。

  • 匿名 Lambda 方法(无需显式释放):

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        formInvalid = !editContext.Validate();
        StateHasChanged();
    }
    
    protected override void OnInitialized()
    {
        editContext = new EditContext(starship);
        editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
    }
    
  • 匿名 Lambda 表达式方法(无需显式释放):

    private ValidationMessageStore messageStore;
    
    [CascadingParameter]
    private EditContext CurrentEditContext { get; set; }
    
    protected override void OnInitialized()
    {
        ...
    
        messageStore = new ValidationMessageStore(CurrentEditContext);
    
        CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) => 
            messageStore.Clear(e.FieldIdentifier);
    }
    

    前面带有匿名 Lambda 表达式的完整代码示例显示在 ASP.NET Core Blazor 窗体和输入组件一文中。

有关详细信息,请参阅清理非托管资源以及后续关于实现 DisposeDisposeAsync 方法的主题。

可取消的后台工作

组件通常会执行长时间运行的后台工作,如进行网络调用 (HttpClient) 以及与数据库交互。 在几种情况下,最好停止后台工作以节省系统资源。 例如,当用户离开组件时,后台异步操作不会自动停止。

后台工作项可能需要取消的其他原因包括:

  • 正在执行的后台任务由错误的输入数据或处理参数启动。
  • 正在执行的一组后台工作项必须替换为一组新的工作项。
  • 必须更改当前正在执行的任务的优先级。
  • 必须关闭应用进行服务器重新部署。
  • 服务器资源受到限制,需要重新计划后台工作项。

要在组件中实现可取消的后台工作模式:

如下示例中:

  • await Task.Delay(5000, cts.Token); 表示长时间运行的异步后台工作。
  • BackgroundResourceMethod 表示如果在调用方法之前释放 Resource,则不应启动的长时间运行的后台方法。

Pages/BackgroundWork.razor:

@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new Resource();
    private CancellationTokenSource cts = new CancellationTokenSource();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}

Blazor Server 重新连接事件

本文所述的组件生命周期事件与 Blazor Server 的重新连接事件处理程序分开运行。 当 Blazor Server 应用断开其与客户端的 SignalR 连接时,只有 UI 更新会被中断。 重新建立连接后,将恢复 UI 更新。 有关线路处理程序事件和配置的详细信息,请参阅 ASP.NET Core BlazorSignalR 指南