maui

When to unsubscribe from events in content views?


I am making some reusable ContentView components in purely C#. In some cases, I want to subscribe to events to some of the elements I've added to the View. ie:

    public class MyEntry : ContentView
    {
        private readonly Entry _entry;
        public MyEntry()
        {
            _entry = new Entry();
            _entry.Focused += EntryFocused;
        }
        private void EntryFocused(object? sender, FocusEventArgs e)
        {
            FocusCommand?.Execute(sender);
        }
    }

But I don't really see any Dispose method to override, or any other relevant lifecycle methods for that matter. So how should I properly unsubscribe from such events to avoid creating memory leaks?

If I change my class to inherit IDisposable, will the dispose method be called by the framework automatically?


Solution

  • I ended up having to write my own logic to dispose of IDisposable views, which I manually call from my Navigation Service. If others read this and directly call the Navigation property on your content pages instead of using a NavigationService from your ViewModels, you'll have to find a way to hook into that.

    In the NavigationService, I created this method:

    private void DisposeChildElements(IView view)
    {
        if (view is Layout layout)
        {
            foreach (var child in layout.Children)
            {
                DisposeChildElements(child);
                if (child is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
        } else if (view is IContentView {Content: View viewContent})
        {
            DisposeChildElements(viewContent);
        }
    
        if (view is IDisposable disposableView)
        {
            disposableView.Dispose();
        }
    }
    

    And then I call it in the GoBack method:

    public async Task GoBackAsync()
    {
        var currentPage = Shell.Current.CurrentPage;
        await Shell.Current.GoToAsync("..");
    
        if (currentPage != Shell.Current.CurrentPage)
        {
            if (currentPage is ContentPage contentPage)
                DisposeChildElements(contentPage.Content);
            if (currentPage is IAsyncDisposable asyncDisposable)
                await asyncDisposable.DisposeAsync();
            else if (currentPage is IDisposable disposable)
                disposable.Dispose();
    
            if (currentPage.BindingContext is IAsyncDisposable vmAsyncDisposable)
                await vmAsyncDisposable.DisposeAsync();
            else if (currentPage.BindingContext is IDisposable disposable)
                disposable.Dispose();
        }
    }
    

    If you also have a PopToRoot method in your NavigationService, you'd also need to implement something similar to call the same for all pages being removed.

    Another thing to note: If you have any pages registered as Singletons in your DI, then you'll also need special handling for that. I don't use Singleton pages, so it's not something I had to solve, but if I were to implement it, I'd just make an ISingletonPage interface and inherit from it on your singleton pages. Then in GoBackAsync, I'd check if the page is ISingletonPage and skip the disposal if it is.