Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Примечание.
Это не последняя версия этой статьи. Актуальная версия — см. версию этой статьи для .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 из .NET, как описано в вызовах функций JavaScript из методов .NET в ASP.NET CoreBlazor, удаляйте все созданные IJSObjectReference/IJSInProcessObjectReference/JSObjectReference, либо из .NET, либо из JS, чтобы избежать утечки памяти JS.
При вызове .NET из JS, как описано в разделе Вызов методов .NET из функций JavaScript в ASP.NET Core Blazor, удалите все созданные DotNetObjectReference из .NET или из JS, чтобы избежать утечки памяти .NET.
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 Реализуется с помощью директивы
@implements
Razor. - Удаляет
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 Реализуется с помощью директивы
@implements
Razor. - удаляет объект
obj
, который является неуправляемым типом, реализующим IAsyncDisposable; - Выполняется проверка на NULL, так как объект
obj
создается в методе жизненного цикла (не показан).
@implements IAsyncDisposable
...
@code {
...
public async ValueTask DisposeAsync()
{
if (obj is not null)
{
await obj.DisposeAsync();
}
}
}
Дополнительные сведения можно найти здесь
- контекст синхронизации ASP.NET Core Blazor
- Очистка неуправляемых ресурсов (документация .NET)
- null-условные операторы ?. и ?[]
Назначение 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 вызовы интероперабельности и приводит к необработанному исключению.
Дополнительные сведения см. в следующих ресурсах:
ASP.NET Core