В процессе разработки приложения 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)


  1. Vasjen
    23.04.2023 20:40
    +1

    При использовании временных зависимостей (Transient) нет четкого понимания, когда ресурсы будут освобождены

    Что-то я не понял или туплю, но вроде Transient должен автоматически уничтожаться GC при окончании вызова согласно документации.


    1. mayorovp
      23.04.2023 20:40
      +1

      Что-то там в документации фигня написана.


      Transient-то объектами никто не владеет, а потому они вообще не освобождаются.


    1. raptor
      23.04.2023 20:40

      Transient уничтожается только когда scope, в рамках которого он был создан, будет уничтожен. В документации почти все верно написано, только не конкретизировано. На каждый request создается scope, поэтому transient зависимости будут нормально уничтожаться после завершения запроса.

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


      1. mayorovp
        23.04.2023 20:40

        Хм, и правда:


        Проверочный код
        using Microsoft.Extensions.DependencyInjection;
        
        var services = new ServiceCollection();
        services.AddTransient<Tracker>();
        
        var sp = services.BuildServiceProvider();
        
        var scope = sp.CreateScope();
        Console.WriteLine("Scope created");
        scope.ServiceProvider.GetRequiredService<Tracker>();
        
        scope.Dispose();
        Console.WriteLine("Scope disposed");
        
        sp.Dispose();
        Console.WriteLine("Service provider disposed");
        
        class Tracker : IDisposable
        {
            public Tracker()
            {
                Console.WriteLine("Created");
            }
        
            public void Dispose()
            {
                Console.WriteLine("Disposed!");
            }
        }


  1. yarosroman
    23.04.2023 20:40

    А зачем реализовывать IDisposable для управляемых ресурсов? IDisposable придуман, чтобы освобождать неуправляемые ресурсы. Вызов Dispose не означает, что объект будет уничтожен сборщиком мусора. https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose

    У вас объекты в памяти висят, значит их держит ссылка какая-то, профилировщик памяти в помощь.


    1. mayorovp
      23.04.2023 20:40
      +1

      Самое простое — чтобы вызвать Dispose у неуправляемого ресурса по цепочке. Также какой-либо внутренний объект может при освобождении возвращаться в пул.


    1. aslepov78
      23.04.2023 20:40
      +1

      >IDisposable придуман, чтобы освобождать неуправляемые ресурсы

      Ошибаетесь, например отписку от события удобно делать в Dispose. Кроме того может быть зависимость от другого IDisposable, и совершенно без разницы что там делается в Dispose: если тип требует вызова Dispose он должен быть вызван.