Поделиться через


утилизация компонентов ASP.NET Core Razor

Примечание.

Это не последняя версия этой статьи. Актуальная версия — см. версию этой статьи для .NET 9.

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

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. Актуальная версия — см. версию этой статьи для .NET 9.

Это важно

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

Актуальная версия — см. версию этой статьи для .NET 9.

В этой статье описывается удаление компонентов Razor в ASP.NET Core с помощью IDisposable и IAsyncDisposable.

Если компонент реализует IDisposable или IAsyncDisposable, платформа вызывает удаление ресурсов при удалении компонента из пользовательского интерфейса. Не полагаться на точное время выполнения этих методов. Например, IAsyncDisposable можно активировать до или после того, как асинхронный вызов Task в OnInitalizedAsync или OnParametersSetAsync ожидается или завершается. Кроме того, код удаления объектов не должен предполагать, что объекты, созданные во время инициализации или других методов жизненного цикла, существуют.

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

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

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

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

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

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

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

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

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

Рекомендации по работе с JSDisconnectedException при отключении контура см. в ASP.NET Core Blazor интероперабельность с JavaScript (JSinterop). Для получения общих рекомендаций по обработке ошибок межплатформенного взаимодействия JavaScript, см. раздел JavaScript interop в обработке ошибок в приложениях ASP.NET Core Blazor.

Синхронный 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();
}

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

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

Дополнительные сведения о компоненте и формах см. в обзоре EditFormBlazor основных форм ASP.NET и других статьях форм на узле 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 Core Blazor.

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

Уничтожение во время JS взаимодействия

Ловушка JSDisconnectedException в потенциальных случаях, когда потеря цепи SignalRBlazor предотвращает JS вызовы интероперабельности и приводит к необработанному исключению.

Дополнительные сведения см. в следующих ресурсах: