В процессе разработки приложения MAUI 7 (.NET Multi-Platform App UI + dotnet 7) с использованием шаблона проектирования MVVM (Model–view–ViewModel) у меня возникла необходимость очищать ресурсы занимаемые моделями представления (View Model). Проблема вроде бы несложная, достаточно реализовать интерфейс IDisposable
в моделях. Но все оказалось не так просто. Все модели внедряются на страницы с помощью стандартного механизма Dependency Injection. При использовании временных зависимостей (Transient
) нет четкого понимания, когда ресурсы будут освобождены. В итоге, в программе создаются новые экземпляры моделей для каждого запроса, но старые продолжают висеть в памяти и занимать ресурсы.
В моем случае, ресурсы моделей должны освобождаться вместе с завершением жизненного цикла страницы (ContentPage
), в которую внедряются модели. Для решения проблемы, я решил использовать обработчик события Unloaded
, который вызывается после выгрузки страницы (компонента).
В итоге, модель представления реализует IDisposable
, при инициализации экземпляра страницы, модель помещается в BindingContext
и к странице добавляется обработчик Unloaded
, в котором вызывается метод IDisposable.Dispose
:
public SomePage(ISomePageViewModel model)
{
BindingContext = model;
InitializeComponent();
Unloaded += (object sender, EventArgs e) =>
{
(((ContentPage)sender).BindingContext as IDisposable)?.Dispose();
};
}
Приведение BindingContext
в IDisposable
с помощью ключевого слова as
позволяет получить значение null
, если объект в BindingContext
не реализует интерфейс IDisposable
. А использование условного оператора ?.
позволяет избежать ошибок, если значение будет null
. Таким образом, метод Dispose
будет вызван только если BindingContext
реализует IDisposable
.
Чтобы не писать одинаковый код на каждой странице, я сделал небольшой метод расширения IServiceCollection
, который создает экземпляр страницы и добавляет к нему обработчик события Unloaded
:
internal static class ServiceCollectionExtensions
{
public static void AddPage<T>(this IServiceCollection serviceCollection) where T : ContentPage
{
serviceCollection.AddTransient(PageWithDisposableContext<T>);
}
private static T PageWithDisposableContext<T>(IServiceProvider serviceProvider) where T : ContentPage
{
var page = ActivatorUtilities.CreateInstance<T>(serviceProvider);
page.Unloaded += (object sender, EventArgs e) =>
{
(((ContentPage)sender).BindingContext as IDisposable)?.Dispose();
};
return page;
}
}
Если для получения экземпляра страницы использовать метод IServiceProvider.GetRequiredService
или IServiceProvider.GetService
, то это может привести к циклическим вызовам метода создания экземпляра страницы. Для решения этой проблемы, можно использовать метод ActivatorUtilities.CreateInstance
.
В результате, добавление страниц в контейнер выглядит следующим образом:
builder.Services.AddPage<MainPage>();
builder.Services.AddPage<SomePage>();
builder.Services.AddPage<EtcPage>();
Комментарии (7)
yarosroman
23.04.2023 20:40А зачем реализовывать IDisposable для управляемых ресурсов? IDisposable придуман, чтобы освобождать неуправляемые ресурсы. Вызов Dispose не означает, что объект будет уничтожен сборщиком мусора. https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose
У вас объекты в памяти висят, значит их держит ссылка какая-то, профилировщик памяти в помощь.
mayorovp
23.04.2023 20:40+1Самое простое — чтобы вызвать Dispose у неуправляемого ресурса по цепочке. Также какой-либо внутренний объект может при освобождении возвращаться в пул.
aslepov78
23.04.2023 20:40+1>IDisposable придуман, чтобы освобождать неуправляемые ресурсы
Ошибаетесь, например отписку от события удобно делать в Dispose. Кроме того может быть зависимость от другого IDisposable, и совершенно без разницы что там делается в Dispose: если тип требует вызова Dispose он должен быть вызван.
Vasjen
Что-то я не понял или туплю, но вроде Transient должен автоматически уничтожаться GC при окончании вызова согласно документации.
mayorovp
Что-то там в документации фигня написана.
Transient-то объектами никто не владеет, а потому они вообще не освобождаются.
raptor
Transient уничтожается только когда scope, в рамках которого он был создан, будет уничтожен. В документации почти все верно написано, только не конкретизировано. На каждый request создается scope, поэтому transient зависимости будут нормально уничтожаться после завершения запроса.
Но если объект будет создан в рамках глобального скоупа самого контейнера, то и уничтожен он будет только когда контейнер уничтожат.
mayorovp
Хм, и правда: