ASP.NET Core Razor 组件生命周期

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

生命周期事件

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

组件生命周期事件:

  1. 如果组件是第一次呈现在请求上:
    • 创建组件的实例。
    • 执行属性注入。 运行 SetParametersAsync
    • 调用 OnInitialized{Async}。 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  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 Blazor 组件呈现

设置参数时 (SetParametersAsync)

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

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

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

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

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

在下面的示例中,如果分析 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 6.0 版本计划的,该功能将改进在预呈现期间对初始化代码执行的管理。

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

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

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

OnParametersSetOnParametersSetAsync 在以下情况下调用:

  • OnInitializedOnInitializedAsync 中初始化组件后。
  • 当父组件重新呈现并提供以下内容时:
    • 至少一个参数已更改时的已知基元不可变类型。
    • 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

对于以下示例组件,请导航到 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 ...
}

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

有关路由参数和约束的详细信息,请参阅 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,因为应用不再处于预呈现阶段。

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

状态更改 (StateHasChanged)

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

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

有关组件呈现以及何时调用 StateHasChanged 的详细信息,请参阅 ASP.NET Core Blazor 组件呈现

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

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 执行生命周期方法时,对象可能为 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 Blazor SignalR 指南

尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 新状态保留功能是针对 ASP.NET Core 6.0 版本计划的,该功能将改进在预呈现期间对初始化代码执行的管理。

检测应用何时预呈现

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

在应用进行预呈现时,无法执行调用 JavaScript 等特定操作。 预呈现时,组件可能需要进行不同的呈现。

对于以下示例,setElementText1 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Layout.cshtml (Blazor Server) 内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值:

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

警告

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

若要将 JavaScript 互操作调用延迟到能够保证此类调用正常工作之后,请重写 OnAfterRender{Async} 生命周期事件。 仅在完全呈现应用后,才会调用此事件。

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;

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

对于以下示例,setElementText2 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Layout.cshtml (Blazor Server) 内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,会返回值:

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

警告

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

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

通过调用 StateHasChanged,可使用从 JavaScript 互操作调用中获取的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Blazor 组件呈现)。 此代码不会创建无限循环,因为仅在 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 中删除该组件时,框架会调用非托管资源释放。 可随时进行处置,包括在组件初始化期间。

同步 IDisposable

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

以下组件:

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

...

@code {
    ...

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

如果需要释放单个对象,则在调用 Dispose 时,可使用 Lambda 来释放对象。 以下示例显示在 ASP.NET Core Blazor 组件呈现 一文中,并演示如何使用 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();
}

如果对象是在生命周期方法中创建的(如 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;
        }
    }
    

匿名函数、方法和表达式

使用匿名函数、方法或表达式时,无需实现 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 Blazor SignalR 指南

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

生命周期事件

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

组件生命周期事件:

  1. 如果组件是第一次呈现在请求上:
    • 创建组件的实例。
    • 执行属性注入。 运行 SetParametersAsync
    • 调用 OnInitialized{Async}。 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  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 Blazor 组件呈现

设置参数时 (SetParametersAsync)

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

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

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

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

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

在下面的示例中,如果分析 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 6.0 版本计划的,该功能将改进在预呈现期间对初始化代码执行的管理。

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

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

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

OnParametersSetOnParametersSetAsync 在以下情况下调用:

  • OnInitializedOnInitializedAsync 中初始化组件后。
  • 当父组件重新呈现并提供以下内容时:
    • 至少一个参数已更改时的已知基元不可变类型。
    • 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

对于以下示例组件,请导航到 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 ...
}

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

有关路由参数和约束的详细信息,请参阅 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,因为应用不再处于预呈现阶段。

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

状态更改 (StateHasChanged)

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

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

有关组件呈现以及何时调用 StateHasChanged 的详细信息,请参阅 ASP.NET Core Blazor 组件呈现

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

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 执行生命周期方法时,对象可能为 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 Blazor SignalR 指南

尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 新状态保留功能是针对 ASP.NET Core 6.0 版本计划的,该功能将改进在预呈现期间对初始化代码执行的管理。

检测应用何时预呈现

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

在应用进行预呈现时,无法执行调用 JavaScript 等特定操作。 预呈现时,组件可能需要进行不同的呈现。

对于以下示例,setElementText1 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Layout.cshtml (Blazor Server) 内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值:

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

警告

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

若要将 JavaScript 互操作调用延迟到能够保证此类调用正常工作之后,请重写 OnAfterRender{Async} 生命周期事件。 仅在完全呈现应用后,才会调用此事件。

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;

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

对于以下示例,setElementText2 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Layout.cshtml (Blazor Server) 内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,会返回值:

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

警告

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

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

通过调用 StateHasChanged,可使用从 JavaScript 互操作调用中获取的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Blazor 组件呈现)。 此代码不会创建无限循环,因为仅在 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 中删除该组件时,框架会调用非托管资源释放。 可随时进行处置,包括在组件初始化期间。

同步 IDisposable

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

以下组件:

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

...

@code {
    ...

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

如果需要释放单个对象,则在调用 Dispose 时,可使用 Lambda 来释放对象。 以下示例显示在 ASP.NET Core Blazor 组件呈现 一文中,并演示如何使用 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();
}

如果对象是在生命周期方法中创建的(如 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;
        }
    }
    

匿名函数、方法和表达式

使用匿名函数、方法或表达式时,无需实现 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 Blazor SignalR 指南

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

生命周期事件

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

组件生命周期事件:

  1. 如果组件是第一次呈现在请求上:
    • 创建组件的实例。
    • 执行属性注入。 运行 SetParametersAsync
    • 调用 OnInitialized{Async}。 如果返回不完整的 Task,则将等待 Task,然后重新呈现组件。
  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 Blazor 组件呈现

设置参数时 (SetParametersAsync)

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

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

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

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

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

在下面的示例中,如果分析 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 6.0 版本计划的,该功能将改进在预呈现期间对初始化代码执行的管理。

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

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

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

OnParametersSetOnParametersSetAsync 在以下情况下调用:

  • OnInitializedOnInitializedAsync 中初始化组件后。
  • 当父组件重新呈现并提供以下内容时:
    • 至少一个参数已更改时的已知基元不可变类型。
    • 复杂类型的参数。 框架无法知道复杂类型参数的值是否在内部发生了改变,因此,如果存在一个或多个复杂类型的参数,框架始终将参数集视为已更改。

对于以下示例组件,请导航到 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 ...
}

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

有关路由参数和约束的详细信息,请参阅 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,因为应用不再处于预呈现阶段。

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

状态更改 (StateHasChanged)

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

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

有关组件呈现以及何时调用 StateHasChanged 的详细信息,请参阅 ASP.NET Core Blazor 组件呈现

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

在呈现组件之前,在生命周期事件中执行的异步操作可能尚未完成。 执行生命周期方法时,对象可能为 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 Blazor SignalR 指南

尽管本部分中的内容重点介绍 Blazor Server 和有状态 SignalR 重新连接,但在托管 Blazor WebAssembly 应用 (WebAssemblyPrerendered) 中预呈现的方案涉及相似的条件和防止执行两次开发人员代码的方法。 新状态保留功能是针对 ASP.NET Core 6.0 版本计划的,该功能将改进在预呈现期间对初始化代码执行的管理。

检测应用何时预呈现

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

在应用进行预呈现时,无法执行调用 JavaScript 等特定操作。 预呈现时,组件可能需要进行不同的呈现。

对于以下示例,setElementText1 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Layout.cshtml (Blazor Server) 内部。 该函数通过 JSRuntimeExtensions.InvokeVoidAsync 进行调用,不返回值:

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

警告

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

若要将 JavaScript 互操作调用延迟到能够保证此类调用正常工作之后,请重写 OnAfterRender{Async} 生命周期事件。 仅在完全呈现应用后,才会调用此事件。

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;

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

对于以下示例,setElementText2 函数置于 wwwroot/index.html<head> 元素 (Blazor WebAssembly) 或 Pages/_Layout.cshtml (Blazor Server) 内部。 该函数通过 IJSRuntime.InvokeAsync 进行调用,会返回值:

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

警告

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

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

通过调用 StateHasChanged,可使用从 JavaScript 互操作调用中获取的新状态重新呈现组件(有关详细信息,请参阅 ASP.NET Core Blazor 组件呈现)。 此代码不会创建无限循环,因为仅在 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 中删除该组件时,框架会调用非托管资源释放。 可随时进行处置,包括在组件初始化期间。

同步 IDisposable

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

以下组件:

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

...

@code {
    ...

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

如果需要释放单个对象,则在调用 Dispose 时,可使用 Lambda 来释放对象。 以下示例显示在 ASP.NET Core Blazor 组件呈现 一文中,并演示如何使用 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();
}

如果对象是在生命周期方法中创建的(如 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;
        }
    }
    

匿名函数、方法和表达式

使用匿名函数、方法或表达式时,无需实现 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 Blazor SignalR 指南