Жизненный цикл компонента Razor ASP.NET Core

Примечание.

Это не последняя версия этой статьи. Сведения о текущем выпуске см. в ASP.NET версии Core 8.0 этой статьи.

В этой статье приведены сведения о жизненном цикле компонента Razor ASP.NET Core, а также о том, как использовать события жизненного цикла.

События жизненного цикла

Компонент Razor обрабатывает события жизненного цикла компонента Razor в наборе синхронных и асинхронных методов жизненного цикла. Методы жизненного цикла можно переопределить для выполнения дополнительных операций с компонентами во время инициализации и отрисовки компонента.

В этой статье упрощается обработка событий жизненного цикла компонентов для уточнения сложной логики платформы. Возможно, вам потребуется получить доступ к источнику ссылки для интеграции пользовательской ComponentBase обработки событий с Blazorобработкой событий жизненного цикла. Комментарии кода в справочном источнике включают дополнительные замечания по обработке событий жизненного цикла, которые не отображаются в этой статье или в документации по API. BlazorОбработка событий жизненного цикла изменилась со временем и подлежит изменению без уведомления о каждом выпуске.

Примечание.

По ссылкам в документации на справочные материалы по .NET обычно загружается ветвь репозитория по умолчанию, которая представляет текущую разработку для следующего выпуска .NET. Чтобы выбрать тег для определенного выпуска, используйте раскрывающийся список Switch branches or tags (Переключение ветвей или тегов). Дополнительные сведения см. в статье Выбор тега версии исходного кода ASP.NET Core (dotnet/AspNetCore.Docs #26205).

На следующих упрощенных схемах показана Razor обработка событий жизненного цикла компонентов. Методы C#, связанные с событиями жизненного цикла, с примерами приводятся в следующих разделах этой статьи.

События жизненного цикла компонента

  1. Если компонент выполняет отрисовку в первый раз по запросу:
    • Создайте экземпляр компонента.
    • Выполните внедрение свойств. Запустите SetParametersAsync.
    • Вызовите процедуру OnInitialized{Async}. Если метод Task возвращает неполное значение, ожидается Task, а затем компонент отрисовывается. Синхронный метод вызывается до асихронного метода.
  2. Вызовите процедуру OnParametersSet{Async}. Если метод Task возвращает неполное значение, ожидается Task, а затем компонент отрисовывается. Синхронный метод вызывается до асихронного метода.
  3. Рендеринг для всей синхронной работы и выполнение операций класса Task.

Примечание.

Асинхронные действия, выполняемые в событиях жизненного цикла, могут не завершиться до отрисовки компонента. Дополнительные сведения см. в разделе Обработка незавершенных асинхронных действий при отрисовке далее в этой статье.

Родительский компонент отображается перед дочерними компонентами, так как отрисовка определяет, какие дочерние элементы присутствуют. Если используется синхронная инициализация родительского компонента, то родительская инициализация гарантированно завершится первым. Если используется асинхронная инициализация родительского компонента, порядок завершения инициализации родительского и дочернего компонента не может быть определен, так как он зависит от выполняемого кода инициализации.

Component lifecycle events of a Razor component in Blazor

Обработка событий DOM:

  1. Выполняется обработчик событий.
  2. Если метод Task возвращает неполное значение, ожидается Task, а затем компонент отрисовывается.
  3. Рендеринг для всей синхронной работы и выполнение операций класса Task.

DOM event processing

Жизненный цикл Render

  1. Избегайте дальнейших операций отрисовки компонента:
    • После первой отрисовки.
    • Если ShouldRender имеет значение false.
  2. Выполните сборку отличий дерева отрисовки и отрисуйте компонент.
  3. Подождите, пока модель DOM обновится.
  4. Вызовите процедуру OnAfterRender{Async}. Синхронный метод вызывается до асихронного метода.

Render lifecycle

Вызовы StateHasChanged разработчиком приводят к отрисовке. Дополнительные сведения см. в статье Отрисовка компонентов Razor ASP.NET Core.

После указания параметров (SetParametersAsync)

SetParametersAsync задает параметры, предоставляемые родительским элементом компонента в дереве отрисовки или из параметров маршрута.

Параметр ParameterView метода содержит набор значений параметров для компонента при каждом вызове SetParametersAsync. Переопределяя метод SetParametersAsync, код разработчика может напрямую взаимодействовать с параметрами ParameterView.

Реализация SetParametersAsync по умолчанию задает значение каждого свойства с атрибутом [Parameter] либо атрибутом [CascadingParameter], имеющим соответствующее значение в ParameterView. Параметры, у которых нет соответствующего значения в ParameterView, остаются неизменными.

Если base.SetParametersAsync не вызывается, код разработчика может интерпретировать значения входящего параметра любым необходимым образом. Например, назначать входящие параметры свойствам класса не обязательно.

Если обработчики событий предоставляются в коде разработчика, отсоедините их при удалении. Дополнительные сведения см. в разделе Удаление компонента с использованием IDisposableIAsyncDisposable.

В следующем примере ParameterView.TryGetValue присваивает value значение параметраParam, если анализ параметра маршрута для Param выполнен успешно. Если value не равно null, значение отображается компонентом.

Хотя сопоставление параметров маршрута не учитывает регистр, в шаблоне маршрута совпадают только имена параметров с TryGetValue учетом регистра. В следующем примере, чтобы получить значение с помощью метода TryGetValue, вам потребуется использовать в шаблоне маршрута /{Param?}, а не /{param?}. Если в этом сценарии используется /{param?}, функция TryGetValue возвращает значение false, а message не задается в качестве строкового параметра message.

SetParamsAsync.razor:

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

<PageTitle>Set Parameters Async</PageTitle>

<h1>Set Parameters Async Example</h1>

<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);
    }
}
@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);
    }
}
@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);
    }
}
@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);
    }
}
@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})

OnInitialized и OnInitializedAsync вызываются, когда компонент инициализируется после получения начальных параметров в SetParametersAsync. Синхронный метод вызывается до асихронного метода.

Если используется синхронная инициализация родительского компонента, родительская инициализация гарантированно завершится до инициализации дочернего компонента. Если используется асинхронная инициализация родительского компонента, порядок завершения инициализации родительского и дочернего компонента не может быть определен, так как он зависит от выполняемого кода инициализации.

Для синхронной операции переопределите OnInitialized:

OnInit.razor:

@page "/on-init"

<PageTitle>On Initialized</PageTitle>

<h1>On Initialized Example</h1>

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
@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 ...
}

Если пользовательский базовый класс используется с пользовательской логикой инициализации, вызовите OnInitializedAsync базовый класс:

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

    await base.OnInitializedAsync();
}

Не обязательно ComponentBase.OnInitializedAsync вызывать, если настраиваемый базовый класс не используется с пользовательской логикой. Дополнительные сведения см. в разделе "Методы жизненного цикла базового класса".

Приложения Blazor, которые предварительно отрисовывают свое содержимое на сервере, вызывают OnInitializedAsyncдважды:

  • Один раз, когда компонент изначально отрисовывается статически как часть страницы.
  • И второй раз, когда браузер отрисовывает компонент.

Чтобы узнать, как предотвратить двойное выполнение кода разработчика в OnInitializedAsync при выполнении предварительной отрисовки, см. раздел Повторное подключение с отслеживанием состояния после предварительной отрисовки. Содержимое раздела посвящено Blazor веб-приложения и повторному подключению с отслеживанием SignalRсостояния. Сведения о сохранении состояния во время выполнения кода инициализации во время предварительной подготовки см. в разделе prerender ASP.NET Core Razor components.

Чтобы узнать, как предотвратить двойное выполнение кода разработчика в OnInitializedAsync при выполнении предварительной отрисовки, см. раздел Повторное подключение с отслеживанием состояния после предварительной отрисовки. Хотя содержимое в разделе посвящено Blazor Server повторному подключению с отслеживаниемSignalR состояния, сценарий предварительной подготовки в размещенных Blazor WebAssembly решениях (WebAssemblyPrerendered) включает аналогичные условия и подходы, чтобы предотвратить выполнение кода разработчика дважды. Сведения о сохранении состояния во время выполнения кода инициализации на этапе предварительной отрисовка см. в статье Компоненты Razor для предварительной визуализации и интеграции ASP.NET Core.

Когда приложение Blazor выполняет предварительную отрисовку, некоторые действия, такие как вызов в JavaScript (взаимодействие с JS), невозможны. При предварительной отрисовке компоненты могут отрисовываться иначе. Дополнительные сведения см. в разделе Предварительная отрисовка с помощью взаимодействия с JavaScript.

Если обработчики событий предоставляются в коде разработчика, отсоедините их при удалении. Дополнительные сведения см. в разделе Удаление компонента с использованием IDisposableIAsyncDisposable.

Используйте потоковую отрисовку с компонентами интерактивного сервера, чтобы улучшить взаимодействие с пользователями для компонентов, выполняющих длительные асинхронные задачи для OnInitializedAsync полной отрисовки. Дополнительные сведения см. в статье Отрисовка компонентов Razor ASP.NET Core.

После указания параметров (OnParametersSet{Async})

OnParametersSet или OnParametersSetAsync вызываются:

  • После инициализации компонента в OnInitialized или OnInitializedAsync.

  • Когда родительский компонент повторно отрисовывается и предоставляет:

    • Известные или примитивные неизменяемые типы, в которых изменился по крайней мере один параметр.
    • Параметры сложных типов. Платформа не может определить, изменились ли значения параметров сложного типа на внутреннем уровне, поэтому при наличии одного или нескольких параметров сложных типов она рассматривает набор параметров как измененный.

    Дополнительные сведения о соглашениях об отрисовке см. в статье Отрисовка компонентов Razor ASP.NET Core.

Синхронный метод вызывается до асихронного метода.

В следующем примере компонента перейдите на страницу компонента по URL-адресу:

  • В котором указана дата начала, полученная StartDate: /on-parameters-set/2021-03-19
  • В котором не указана дата начала, и где StartDate присваивается значение текущего местного времени: /on-parameters-set

Примечание.

В маршруте компонента невозможно одновременно ограничить параметр DateTime и применить ограничение маршрута datetime, а также сделать параметр необязательным. Поэтому следующий компонент OnParamsSet для управления маршрутизацией в URL-адресе с заданным сегментом даты и без него использует две директивы @page.

OnParamsSet.razor:

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

<PageTitle>On Parameters Set</PageTitle>

<h1>On Parameters Set Example</h1>

<p>
    Pass a datetime in the URI of the browser's address bar. 
    For example, add <code>/1-1-2024</code> to the address.
</p>

<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}).";
        }
    }
}
@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}).";
        }
    }
}
@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}).";
        }
    }
}
@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}).";
        }
    }
}
@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 ...
}

Если пользовательский базовый класс используется с пользовательской логикой инициализации, вызовите OnParametersSetAsync базовый класс:

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

    await base.OnParametersSetAsync();
}

Не обязательно ComponentBase.OnParametersSetAsync вызывать, если настраиваемый базовый класс не используется с пользовательской логикой. Дополнительные сведения см. в разделе "Методы жизненного цикла базового класса".

Если обработчики событий предоставляются в коде разработчика, отсоедините их при удалении. Дополнительные сведения см. в разделе Удаление компонента с использованием IDisposableIAsyncDisposable.

Дополнительные сведения о параметрах маршрута и ограничениях см. в статье Маршрутизация ASP.NET Core Blazor и навигация.

Пример реализации SetParametersAsync вручную для повышения производительности в некоторых сценариях см. в ASP.NET рекомендациях по производительности CoreBlazor.

После отрисовки компонента (OnAfterRender{Async})

OnAfterRender и OnAfterRenderAsync вызываются после интерактивной отрисовки компонента, а пользовательский интерфейс завершил обновление (например, после добавления элементов в браузер DOM). В этот момент указываются ссылки на элементы и компоненты. Используйте этот этап, чтобы выполнить дополнительные шаги инициализации отрисованного содержимого, такого как вызовы взаимодействия с JS, которые взаимодействуют с отрисованными элементами модели DOM. Синхронный метод вызывается до асихронного метода.

Эти методы не вызываются во время предварительной отрисовки или отрисовки на сервере, так как эти процессы не подключены к DOM в динамическом браузере и уже завершены до обновления DOM.

Для OnAfterRenderAsyncэтого компонент не выполняет автоматическую отрисовку после завершения любого возвращаемого Task , чтобы избежать бесконечного цикла отрисовки.

OnAfterRender и OnAfterRenderAsync вызываются после завершения отрисовки компонента. В этот момент указываются ссылки на элементы и компоненты. Используйте этот этап, чтобы выполнить дополнительные шаги инициализации отрисованного содержимого, такого как вызовы взаимодействия с JS, которые взаимодействуют с отрисованными элементами модели DOM. Синхронный метод вызывается до асихронного метода.

Эти методы не вызываются во время предварительной отрисовки, так как предварительная отрисовка не присоединена к DOM в динамическом браузере и уже завершена до обновления DOM.

Для OnAfterRenderAsyncэтого компонент не выполняет автоматическую отрисовку после завершения любого возвращаемого Task , чтобы избежать бесконечного цикла отрисовки.

Параметр firstRender для OnAfterRender и OnAfterRenderAsync:

  • Устанавливается в значение true при первой отрисовке экземпляра компонента.
  • Может использоваться, чтобы гарантировать однократное выполнение инициализации.

AfterRender.razor:

@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

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

<p>Study logged messages in the console.</p>

@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");
    }
}
@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");
    }
}
@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");
    }
}
@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");
    }
}
@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 базовый класс:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...

    await base.OnAfterRenderAsync(firstRender);
}

Не обязательно ComponentBase.OnAfterRenderAsync вызывать, если настраиваемый базовый класс не используется с пользовательской логикой. Дополнительные сведения см. в разделе "Методы жизненного цикла базового класса".

Даже если вы возвращаете Task из OnAfterRenderAsync, платформа не планирует дальнейший цикл отрисовки компонента после завершения задачи. Это позволяет избежать бесконечного цикла отрисовки. Это поведение отличается от других методов жизненного цикла, которые планируют дальнейший цикл отрисовки после завершения возвращаемой операции Task.

OnAfterRender и OnAfterRenderAsyncне вызываются при предварительной отрисовке на сервере. Методы вызываются, когда компонент отрисовывается в интерактивном режиме после предварительной отрисовки. При предварительной отрисовке приложения происходит следующее.

  1. Компонент выполняется на сервере для создания статической HTML-разметки в HTTP-ответе. На этом этапе OnAfterRender и OnAfterRenderAsync не вызываются.
  2. Blazor При запуске скрипта (blazor.{server|webassembly|web}.js) в браузере компонент перезапускается в интерактивном режиме отрисовки. После перезапуска компонента вызываются методы OnAfterRender и OnAfterRenderAsync, так как приложение больше не находится на этапе предварительной отрисовки.

Если обработчики событий предоставляются в коде разработчика, отсоедините их при удалении. Дополнительные сведения см. в разделе Удаление компонента с использованием IDisposableIAsyncDisposable.

Методы жизненного цикла базового класса

При переопределении Blazorметодов жизненного цикла не требуется вызывать методы жизненного цикла базового класса.ComponentBase Однако компонент должен вызывать переопределенный метод жизненного цикла базового класса, если метод базового класса содержит логику, которую необходимо выполнить. Потребители библиотек обычно вызывают методы жизненного цикла базового класса при наследовании базового класса, так как базовые классы библиотек часто имеют пользовательскую логику жизненного цикла для выполнения. Если приложение использует базовый класс из библиотеки, ознакомьтесь с документацией по библиотеке.

В следующем примере base.OnInitialized(); вызывается для обеспечения выполнения метода базового класса OnInitialized . Без вызова BlazorRocksBase2.OnInitialized не выполняется.

BlazorRocks2.razor:

@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}

BlazorRocksBase2.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}

Изменения состояния (StateHasChanged)

StateHasChanged уведомляет компонент о том, что его состояние изменилось. В соответствующих случаях вызов StateHasChanged приводит к повторной отрисовке компонента.

StateHasChanged вызывается автоматически для методов EventCallback. Дополнительные сведения об обратных вызовах событий см. в разделе Обработка событий Blazor в ASP.NET Core.

Дополнительные сведения об отрисовке компонентов и о том, когда лучше вызывать StateHasChanged и когда для этого следует использовать ComponentBase.InvokeAsync, см. в статье Отрисовка компонента Razor ASP.NET Core.

Обработка незавершенных асинхронных действий при отрисовке

Асинхронные действия, выполняемые в событиях жизненного цикла, могут не завершиться до отрисовки компонента. Во время выполнения метода жизненного цикла объекты могут быть null или заполнены данными не полностью. Предоставьте логику отрисовки для подтверждения инициализации объектов. Отрисуйте элементы пользовательского интерфейса заполнителя (например, сообщение загрузки), пока объекты имеют значение null.

В следующем компоненте OnInitializedAsync переопределяется для асинхронного предоставления данных о рейтинге фильмов (movies). Если movies имеет значение null, пользователю выводится сообщение о загрузке. Когда элемент Task, возвращенный OnInitializedAsync, завершается, компонент отрисовывается повторно с обновленным состоянием.

<h1>Sci-Fi Movie Ratings</h1>

@if (movies == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <ul>
        @foreach (var movie in movies)
        {
            <li>@movie.Title &mdash; @movie.Rating</li>
        }
    </ul>
}

@code {
    private Movies[]? movies;

    protected override async Task OnInitializedAsync()
    {
        movies = await GetMovieRatings(DateTime.Now);
    }
}

Обработка ошибок

Сведения об обработке ошибок во время выполнения метода жизненного цикла см. в статье Обработка ошибок в приложениях ASP.NET Core Blazor.

Повторное подключение с отслеживанием состояния после предварительной отрисовки

При предварительной подготовке на сервере компонент изначально отображается статически как часть страницы. После того как браузер установит соединение SignalR с сервером, компонент будет отрисован снова и станет интерактивным. Если метод жизненного цикла OnInitialized{Async} для инициализации компонента присутствует, он выполняется дважды:

  • Когда компонент предварительно отрисовывается статически.
  • После установления соединения с сервером.

Это может привести к заметному изменению данных, отображаемых в пользовательском интерфейсе, когда компонент отрисовывается окончательно. Чтобы избежать этого поведения, передайте идентификатор в кэш состояния во время предварительной отрисовки и получите состояние после предварительной отрисовки.

В следующем коде показано WeatherForecastService , как избежать изменения отображения данных из-за предварительной отрисовки. Ожидается Delay (await Task.Delay(...)) имитирует короткую задержку перед возвратом данных из GetForecastAsync метода.

Добавьте IMemoryCache службы в AddMemoryCache коллекцию служб в файле приложения Program :

builder.Services.AddMemoryCache();

WeatherForecastService.cs:

using Microsoft.Extensions.Caching.Memory;

namespace BlazorSample;

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

    public IMemoryCache MemoryCache { get; } = memoryCache;

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

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

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
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)
            });

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

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
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)
            });

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

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
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();
        });
    }
}
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 веб-приложения и повторному подключению с отслеживанием SignalRсостояния. Сведения о сохранении состояния во время выполнения кода инициализации во время предварительной подготовки см. в разделе prerender ASP.NET Core Razor components.

Хотя содержимое этого раздела посвящено Blazor Server повторному подключению и повторному подключению с отслеживаниемSignalR состояния, сценарий предварительной подготовки в размещенных Blazor WebAssembly решениях (WebAssemblyPrerendered) включает аналогичные условия и подходы, чтобы предотвратить выполнение кода разработчика дважды. Сведения о сохранении состояния во время выполнения кода инициализации на этапе предварительной отрисовка см. в статье Компоненты Razor для предварительной визуализации и интеграции ASP.NET Core.

Предварительная отрисовка с помощью взаимодействия с JavaScript

Этот раздел относится к серверным приложениям, которые предопределили Razor компоненты. Предварительная отрисовка рассматривается в компонентах Prerender ASP.NET CoreRazor.

Примечание.

Внутренняя навигация для интерактивной маршрутизации в Blazor веб-приложения не включает запрос нового содержимого страницы с сервера. Поэтому предварительное отображение не выполняется для внутренних запросов страниц. Если приложение принимает интерактивную маршрутизацию, выполните полную перезагрузку страницы для примеров компонентов, демонстрирующих поведение предварительной подготовки. Дополнительные сведения см. в разделе "Предварительная ASP.NET Основные Razor компоненты".

Этот раздел относится к серверным приложениям и размещенным приложениям, которые предустановили Blazor WebAssemblyRazor компоненты. Предварительная отрисовка рассматривается в статье Предварительная отрисовка и интеграция компонентов Razor в ASP.NET Core.

Когда приложение выполняет предварительную отрисовку, некоторые действия, такие как вызов в JavaScript (JS), невозможны.

В следующем примере setElementText1 функция вызывается и JSRuntimeExtensions.InvokeVoidAsync не возвращает значение.

Примечание.

Общие рекомендации по расположению JS и наши рекомендации для приложений в рабочей среде см. в статье Взаимодействие ASP.NET Core Blazor с JavaScript (JS-взаимодействие).

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

Предупреждение

Предыдущий пример изменяет только DOM для демонстрационных целей. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JS не рекомендуется, так как JS может повлиять на отслеживание изменений Blazor. Дополнительные сведения см. в разделе Взаимодействие JavaScript приложения Blazor ASP.NET Core (взаимодействие JS).

Событие жизненного цикла OnAfterRender{Async} не вызывается при предварительной отрисовке на сервере. Переопределите метод OnAfterRender{Async}, чтобы отложить вызовы взаимодействия с JS до тех пор, пока компонент не будет отрисован и не будет находиться в интерактивном режиме в клиенте после предварительной отрисовки.

PrerenderedInterop1.razor:

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

<PageTitle>Prerendered Interop 1</PageTitle>

<h1>Prerendered Interop Example 1</h1>

<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 функция вызывается и IJSRuntime.InvokeAsync возвращает значение.

Примечание.

Общие рекомендации по расположению JS и наши рекомендации для приложений в рабочей среде см. в статье Взаимодействие ASP.NET Core Blazor с JavaScript (JS-взаимодействие).

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

Предупреждение

Предыдущий пример изменяет только DOM для демонстрационных целей. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JS не рекомендуется, так как JS может повлиять на отслеживание изменений Blazor. Дополнительные сведения см. в разделе Взаимодействие JavaScript приложения Blazor ASP.NET Core (взаимодействие JS).

При вызове JSRuntime.InvokeAsync структура ElementReference используется только в методе OnAfterRenderAsync, а не в предыдущем методе жизненного цикла, так как элемент JS появляется только после отрисовки компонента.

Метод StateHasChanged вызывается для повторной отрисовки компонента с новым состоянием, полученным из вызова взаимодействия с JS (дополнительные сведения см. в статье Отрисовка компонента приложения Razor в ASP.NET Core). Код не создает бесконечный цикл, так как метод StateHasChanged вызывается, только если data имеет значение null.

PrerenderedInterop2.razor:

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

<PageTitle>Prerendered Interop 2</PageTitle>

<h1>Prerendered Interop Example 2</h1>

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

<p>
    Set value via JS interop call: 
    <strong id="val-set-by-interop" @ref="divElement"></strong>
</p>



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

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

            StateHasChanged();
        }
    }
}

Примечание.

В предыдущем примере клиент загрязняет глобальные функции. Более эффективный подход для приложений в рабочей среде приведен в разделе Изоляция JavaScript в модулях JavaScript.

Пример:

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

Удаление компонента с использованием IDisposable и IAsyncDisposable

Если компонент реализует интерфейс IDisposable, IAsyncDisposable (или и тот и другой), при удалении компонента из пользовательского интерфейса платформа вызывает неуправляемое удаление ресурсов. Удаление можно выполнить в любое время, в том числе при инициализации компонента.

Компоненты не должны реализовывать IDisposable и IAsyncDisposable одновременно. Если реализуются оба, платформа выполняет только асинхронную перегрузку.

Код разработчика должен гарантировать, что реализации IAsyncDisposable не займут много времени.

Удаление ссылок на объекты взаимодействия JavaScript

Примеры в статьях взаимодействия JavaScript (JS) демонстрируют типичные шаблоны удаления объектов:

JS Ссылки на объекты взаимодействия реализуются в виде карты с ключом идентификатора на стороне JS вызова взаимодействия, создающего ссылку. Если удаление объектов инициируется из .NET или JS на стороне, удаляет запись из карты, Blazor а объект может быть собран мусором до тех пор, пока нет другой строгой ссылки на объект.

Как минимум, всегда удалять объекты, созданные на стороне .NET, чтобы избежать утечки управляемой памяти .NET.

Задачи очистки DOM во время удаления компонентов

Дополнительные сведения см. в разделе Взаимодействие JavaScript приложения Blazor ASP.NET Core (взаимодействие JS).

Инструкции по отключению канала см. в JSDisconnectedException разделе ASP.NET взаимодействие JavaScript Core Blazor (JSвзаимодействие). Общие рекомендации по обработке ошибок взаимодействия JavaScript см. в разделе "Обработка ошибок взаимодействия JavaScript" в приложениях ASP.NET CoreBlazor.

Синхронный интерфейс IDisposable

Для задач синхронного удаления используйте IDisposable.Dispose.

Приведенный ниже компонент делает следующее.

  • IDisposable Реализуется с помощью директивы@implementsRazor.
  • удаляет объект obj, который является неуправляемым типом, реализующим IDisposable;
  • выполняет проверку значений NULL, так как объект obj создается в методе жизненного цикла (не показан).
@implements IDisposable

...

@code {
    ...

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

При необходимости один объект можно удалить, используя при вызове Dispose лямбда-выражение. Следующий пример можно найти в статье Отрисовка компонента приложения Razor в ASP.NET Core. Он демонстрирует использование лямбда-выражения для удаления объекта Timer.

TimerDisposal1.razor:

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

<PageTitle>Timer Disposal 1</PageTitle>

<h1>Timer Disposal Example 1</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();
}

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();
}

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();
}

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();
}

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. Дополнительные сведения см. в статье Отрисовка компонентов Razor ASP.NET Core.

Если объект создан в методе жизненного цикла (например, OnInitialized{Async}), проверьте его на наличие значений null перед вызовом Dispose.

TimerDisposal2.razor:

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

<PageTitle>Timer Disposal 2</PageTitle>

<h1>Timer Disposal Example 2</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();
}

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();
}

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();
}

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();
}

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.

Приведенный ниже компонент делает следующее.

  • IAsyncDisposable Реализуется с помощью директивы@implementsRazor.
  • удаляет объект obj, который является неуправляемым типом, реализующим IAsyncDisposable;
  • выполняет проверку значений NULL, так как объект obj создается в методе жизненного цикла (не показан).
@implements IAsyncDisposable

...

@code {
    ...

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

Дополнительные сведения см. в разделе:

Назначение null для удаленных объектов

Обычно нет необходимости назначать null удаленным объектам после вызова Dispose/DisposeAsync. К редким случаям назначения null можно отнести приведенные ниже.

  • Если тип объекта плохо реализован и не допускает повторных вызовов Dispose/DisposeAsync, назначьте null после удаления, чтобы корректно пропустить дальнейшие вызовы Dispose/DisposeAsync.
  • Если долгосрочный процесс продолжает сохранять ссылку на удаленный объект, назначение null позволяет сборщику мусора освободить объект, несмотря на эту ссылку.

Это нетипичные сценарии. При использовании объектов, которые реализованы правильно и ведут себя нормально, нет смысла назначать удаленным объектам значение null. В редких случаях, когда объекту должно быть назначено значение null, рекомендуется задокументировать причину и найти решение, которое устраняет необходимость назначения null.

StateHasChanged

Примечание.

Вызов StateHasChanged в Dispose не поддерживается. StateHasChanged может вызываться в процессе уничтожения отрисовщика, поэтому запрос обновлений пользовательского интерфейса на этом этапе не поддерживается.

Обработчики событий

Всегда отменяйте подписки на обработчики событий из событий .NET. В следующих примерах формы Blazor показано, как отсоединить обработчик событий в методе Dispose:

  • Подход с использованием частного поля и лямбда-выражения

    @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 и IAsyncDisposable.

Дополнительные сведения о компоненте и формах см. в обзоре EditForm основных форм ASP.NET Blazor и других статьях форм на узле Forms.

Анонимные функции, методы и выражения

При использовании анонимных функций, методов или выражений реализовывать IDisposable и отсоединять делегаты не обязательно. Однако отсоединение делегата может стать проблемой, когда объект, предоставляющий событие, переживает время существования компонента, регистрирующего делегат. В этом случае возникает утечка памяти, поскольку зарегистрированный делегат сохраняет исходный объект в активном состоянии. Поэтому следует использовать только следующие подходы, если известно, что делегат события быстро удаляется. Если точное время существования объектов, требующих удаления, неизвестно, подключите метод делегата и правильно удалите делегат, как показано в предыдущих примерах.

  • Подход с использованием анонимного лямбда-метода (явное удаление не требуется):

    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);
    }
    
  • Подход с использованием анонимного лямбда-выражения (явное удаление не требуется):

    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);
    }
    

    Полный пример предыдущего кода с анонимными лямбда-выражениями отображается в статье проверки ASP.NET Основных Blazor форм .

Дополнительные сведения см. в статье Очистка неуправляемых ресурсов и следующих разделах, в которых описана реализация методов Dispose и DisposeAsync.

Отменяемая фоновая операция

Компоненты часто выполняют длительные фоновые операции, например осуществление сетевых вызовов (HttpClient) и взаимодействие с базами данных. В целях экономии системных ресурсов в ряде ситуаций желательно отключить выполнение фоновых операций. Например, фоновые асинхронные операции не останавливаются автоматически, когда пользователь выходит из компонента.

Ниже перечислены другие причины, по которым может потребоваться отмена фоновых рабочих элементов.

  • Выполнение фоновой задачи было начато с ошибочными входными данными или параметрами обработки.
  • Текущий набор выполняемых фоновых рабочих элементов должен быть заменен новым набором.
  • Необходимо изменить приоритет выполняемых в данный момент задач.
  • Чтобы выполнить повторное развертывание на сервере, завершите работу приложения.
  • Ресурсы сервера становятся ограниченными, в связи с чем возникает необходимость в пересмотре графика выполнения фоновых рабочих элементов.

Чтобы реализовать механизм отменяемой фоновой операции в компоненте, выполните следующие действия.

В следующем примере :

  • await Task.Delay(5000, cts.Token); представляет длительную асинхронную фоновую операцию.
  • BackgroundResourceMethod представляет длительный фоновый метод, который не должен запускаться, если Resource удаляется перед вызовом метода.

BackgroundWork.razor:

@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<PageTitle>Background Work</PageTitle>

<h1>Background Work Example</h1>

<p>
    <button @onclick="LongRunningWork">Trigger long running work</button>
    <button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
    If you trigger disposal within 10 seconds of page load, the 
    <code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
    If disposal occurs after <code>BackgroundResourceMethod</code> is called but before action
    is taken on the resource, an <code>ObjectDisposedException</code> is thrown by 
    <code>BackgroundResourceMethod</code>, and the resource isn't processed.
</p>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();
    private IList<string> messages = [];

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

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

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

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");

        if (!cts.IsCancellationRequested)
        {
            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;
    }
}
@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;
        }
    }
}
@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;
        }
    }
}
@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;
        }
    }
}
@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

События жизненного цикла компонентов, описанные в этой статье, работают отдельно от обработчиков событий повторного подключения на стороне сервера. SignalR Когда подключение к клиенту потеряно, прерваны только обновления пользовательского интерфейса. Они возобновляются при восстановлении подключения. Дополнительные сведения о конфигурации и событиях обработчика канала см. в статье Руководство по ASP.NET Core BlazorSignalR.

Дополнительные ресурсы

Обработка перехвата исключений за пределами жизненного Razor цикла компонента