0

My app navigates using the following shell command:

await Shell.Current.GoToAsync("PageName");

I've been experiencing performance issues which become worse the longer you use the app so I figured I might not be releasing resources. On further investigation I discovered that every page I've navigated to remains active in memory and never disposed of. If a page contains a timer then that timer continues to run long after I've navigated away from the page.

I need to find a very basic means of navigation where there is only ever one page active at any one time and the last page is always disposed of when I navigate to the next page. I never need a previous page to remain alive.

Does anyone know how this can be achieved please?

When using Shell navigation, I expected the previous page to be disposed of. Tests show this isn't the case.

Thanks!

Sutts99
  • 21
  • 6
  • 1
    Do you need to be able to navigate back to the previous page? If not, then you could consider using `await Shell.Current.GoToAsync("//PageName");` (notice the absolute path "//") so the page is not pushed onto the current navigation stack. – Al John Jul 21 '23 at 20:13

3 Answers3

2

Thanks so much for your help guys. This solution works great!
You always get the same instance of each requested page. No more nasty page build up in memory. Just make sure any timers are instantiated in the constructor and turned on/off in the Disappearing/Appearing events or disposed of properly in the Disappearing event as they will live on and build up in the background if not handled properly.


App.xaml.cs

public partial class App : Application
{
    public static IServiceProvider Services;

    public App(IServiceProvider services)
    {
        Services = services;

        InitializeComponent();

        MainPage = App.Services.GetService<InitialPage>();
    }
}

MauiProgram.cs

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        // Cache Pages
        builder.Services.AddSingleton<InitialPage>();
        builder.Services.AddSingleton<Page1>();        
        builder.Services.AddSingleton<Page2>();
        builder.Services.AddSingleton<Page3>();

        return builder.Build();
    }
}

ContentPage Code To Switch Page

Application.Current.MainPage = App.Services.GetService<Page1>();
Sutts99
  • 21
  • 6
  • See [my earlier answer elsewhere](https://stackoverflow.com/a/76548413/199364) for a discussion of the `public App(IServiceProvider services)` technique. This may get `null exception`, if main page or its view model uses DI injection in its constructor (relies on some other registered service). There, in an UPDATE, I show an alternative implementation of `App.Services`. Its fine to use the simple technique in this answer; if you hit this limitation (and get null exception) then switch to that updated technique. – ToolmakerSteve Aug 01 '23 at 02:10
1
  • Maui supports three types (kinds; techniques) of navigation.
  • The type used is determined by Application.Current.MainPage.
  • The initial type is set in App.xaml.cs / App constructor, by this line:
MainPage =  new ...;   // Creates a new instance.
// OR Use DI and `AppServiceProvider` (later in this answer):
MainPage = AppServiceProvider.GetService<...>();

Navigation Type 1: Set the page directly

IMPORTANT: With this technique, NONE of the Shell layout or Shell navigation features exist. App layout, and navigation, is entirely up to you.

This acts like your requirement

I never need a previous page to remain alive.

To begin this, set:

MainPage = new MyFirstPage();   // Creates a new instance.
// OR Use DI and `AppServiceProvider` (later in this answer):
MainPage = AppServiceProvider.GetService<MyFirstPage>();

To go to another page, do:

Application.Current.MainPage = new MyNextPage();   // Creates a new instance.
// OR Use DI and `AppServiceProvider` (later in this answer):
Application.Current.MainPage = AppServiceProvider.GetService<MyNextPage>();

See Sutts answer for a nice example of this direct page setting. That answer uses a different technique for getting at Service Provider.


Navigation Type 2: NavigationPage

IMPORTANT: With this technique, NONE of the Shell layout or Shell navigation features exist. Navigation uses NavigationPage commands. App layout is entirely up to you.

This is a simpler "navigation stack" paradigm than AppShell.

It gives you direct control over the nav stack. No routes or query parameters. Just pushing and popping pages.

To begin this, set:

MainPage = new NavigationPage(new MyFirstPage());   // Creates a new instance.
// OR Use DI and `AppServiceProvider` (later in this answer):
MainPage = new NavigationPage(AppServiceProvider.GetService<MyFirstPage>());

To go to another page, REPLACING the current stack of pages, do:

// NOTE: The root (first) page is always kept.
await Navigation.PopToRootAsync();

// "new ..."
await Navigation.PushAsync(new MyNextPage());   // Creates a new instance.
// OR Use DI and `AppServiceProvider` (later in this answer):
await Navigation.PushAsync(AppServiceProvider.GetService<MyNextPage>());

To go to another page, pushing it on to the stack (so can go back), do:

await Navigation.PushAsync(new MyNextPage());   // Creates a new instance.
// OR Use DI and `AppServiceProvider` (later in this answer):
await Navigation.PushAsync(AppServiceProvider.GetService<MyNextPage>());

Navigation Type 3: Shell Navigation

WARNING (25-July-2023): it has been reported that Maui currently NEVER DISPOSES pages. Therefore, having only one tab does not (currently) help minimize which pages are kept. This answer section on Shell won't help minimize memory usage, until Maui is capable of disposing of pages.

Based on other StackOverflow questions I've seen, AppShell keeps the "root" of EACH TAB around forever. [Unless a new option is added, this will be true even if-and-when Maui is capable of disposing of pages. Tabs are considered "persistent".]

Therefore, to satisfy the requirement "don't keep other pages around", DO NOT do the standard technique of defining your pages "inside" Shell's XAML, as tabs:

<!-- This creates MULTIPLE TABS; these seem to "stick" in memory -->
<!-- DO NOT do this, if you want memory released -->
<Shell ...>
  <ShellContent ... />
  <ShellContent ... />
  <ShellContent ... />
</Shell>

Instead, have a single ShellContent. From this, you navigate to other pages, that are not part of Shell's hierarchy:

<Shell ...
  <!-- Only create ONE TAB -->
  <ShellContent ... Route="MyRoot" />   <!-- aka "MyFirstPage" -->
  <!-- NO MORE "ShellContents". -->
</Shell>

in Shell code-behind:

// Define routes for pages that are NOT part of Shell's XAML.
Routing.RegisterRoute("MyRoot/MyNextPage", typeof(MyNextPage));

navigate to another page:

// This REPLACES nav stack with MyRoot, followed by MyNextPage.
// A limitation of shell is that "//MyNextPage" is not valid,
// unless "MyNextPage" is a TAB as described above. We are avoiding multiple tabs.
await Shell.Current.GoToAsync("//MyRoot/MyNextPage");

Similar to NavigationPage example, the FIRST page (root of the tab) stays in memory.



EXTRA INFO
AppServiceProvider: USE SERVICE PROVIDER TO AVOID "new MyPage();"

To avoid the memory issues from views/pages not being disposed, re-use the page instances.

Classes used with DI need to be registered at app startup:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
        => MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            // Omitted for brevity            
            .RegisterViewsAndViewModels()
            .Build();
            
    public static MauiAppBuilder RegisterViewsAndViewModels(this MauiAppBuilder builder)
    {
        // Singletons: Re-use page instances.
        builder.Services.AddSingleton<MainPage>();
        builder.Services.AddSingleton<MyNextPage>();

        // Singletons: ViewModels that keep their data between appearances of page.
        builder.Services.AddSingleton<SomeViewModel>();

        // Transients: ViewModels that start "clean" (default values) on each appearance of page.
        builder.Services.AddTransient<SomeViewModel2>();

        // Returning the builder supports "chaining" with "."s.
        return builder;
    }
}

Now that pages and their view models are registered, can automatically "inject" the view model into the page, by adding it as a parameter to the constructor:

public partial class MyPage : ContentPage
{
    public MyPage(MyViewModel vm)
    {
        InitializeComponent();

        // OPTIONAL: Convenience for accessing vm properties in code.
        VM = vm;

        BindingContext = vm;
    }

    // OPTIONAL: Convenience for accessing vm properties in code.
    private MyViewModel VM;
}

In Marc Fabregat's answer to a DI question, there is code to create a static class that gives convenient access to Maui's Dependency Injection Container (an IServiceProvider):

public static class AppServiceProvider
{
    public static TService GetService<TService>()
        => Current.GetService<TService>();

    public static IServiceProvider Current
        =>
#if WINDOWS10_0_17763_0_OR_GREATER
        MauiWinUIApplication.Current.Services;
#elif ANDROID
        MauiApplication.Current.Services;
#elif IOS || MACCATALYST
            MauiUIApplicationDelegate.Current.Services;
#else
        null;
#endif
}

That implementation of AppServiceProvider works even within Maui App constructor; we rely on that in code a bit later below.

This shows a given page, without using new MyPage(); (which would create a new instance each time):

Application.Current.MainPage = AppServiceProvider.GetService<MyPage>();

Set the app's initial page in App.xaml.cs's constructor:

MainPage = AppServiceProvider.GetService<MyFirstPage>();
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • Appreciate the help with this, thank you. Why can't Microsoft make their instructions so clear? I went with option 1 which navigates fine but unfortunately the previous pages still remain active. I'm thinking that disposing of previous pages is something that needs to be handled separately to the navigation. Any idea how to kill a unwanted page pls? – Sutts99 Jul 24 '23 at 21:15
  • Based on discussion in [this open issue](https://github.com/dotnet/maui/issues/7354), there currently is no way to dispose the page. Not even manually, because ContentPage does not have a `Dispose` method built-in. Create what you need in [Page.OnAppearing](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.page.onappearing), and delete that stuff in [Page.OnDisappearing](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.page.ondisappearing). – ToolmakerSteve Jul 24 '23 at 22:43
  • OR define your pages as `public partial class MyPageNameHere : ContentPage, IDisposable`, and call `Dispose` on it. BUT that won't fully dispose the page; its just a place you could put your own logic to delete stuff, if you don't do it in `OnDisappearing`. – ToolmakerSteve Jul 24 '23 at 22:46
  • Thanks for the tips. Doesn't sound terribly well thought out does it!? – Sutts99 Jul 25 '23 at 09:05
  • I've been running some tests. I have 2 pages and each has a timer which appends a code to a global string variable every 15 seconds. I use your "Navigation Type 2" method Navigation.PushAsync(new Page2()); to go from Page 1 to Page 2. I then use your "Navigation Type 2" method Navigation.PopToRootAsync(); to go back to Page 1. I do this forwards and back navigation several times and then wait for the results to be displayed in the regularly refreshed output on Page 1. – Sutts99 Jul 25 '23 at 20:20
  • Each refresh shows multiple codes being appended to the string i.e. Every time I visit Page 2, I get a new instance of Page 2 which continues to live in the background forever, updating the global variable with its unique code. – Sutts99 Jul 25 '23 at 20:23
  • Even if I kill the timer before leaving each page, I'm still going to end up with dozens and dozens of page instances in memory. No wonder the app is crashing. Surely there has to be some way to use the same page instance multiple times or kill the page completely? – Sutts99 Jul 25 '23 at 20:26
  • That is not good! New section added to answer. Uses DI registration and ServiceProvider to re-use page instances. – ToolmakerSteve Jul 25 '23 at 22:15
  • @ToolmakerSteve Very comprehensive and well written. One questions about the ServiceProvider. If you get a Service like that you will create a singleton every time you use that extention. Will you not? What you need in my humble opinion is CreateScope to have it disposed after use.`var scope = Current.CreateScope(); return scope.ServiceProvider.GetRequiredService();` – Peter Wessberg Jul 26 '23 at 08:20
  • No, not needed. The code boils down to `MauiApplication.Current.Services.GetService();`, which is what DI does. It uses Maui's current DI Container (`Scope`). This call uses the same scope Maui uses throughout the app's lifetime. So if `SomeClass` is registered as a Singleton, `Current.Services` returns that same instance of `SomeClass` each time. https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovider.getservice – ToolmakerSteve Jul 26 '23 at 18:50
0

Navigation is a stack. So when you open a new page you add to that stack. This is good because you can traverse back to where you come from without having to use code. It is just to click on the backward button.

When you need to go back and want to do that with code you need to use something like Shell.Current.GoToAsync("..");

If you dont want to use the stack you need to use absuolute route like ///MainPage/Page instead of a relative.

I also want to point out that you must make sure you not define routes more than once. Like when you have routes in the shell and also define them with RegisterRoute.

More about this .NET MAUI Shell navigation

Peter Wessberg
  • 879
  • 6
  • 13
  • Thanks for the prompt help Peter. If I use absolute navigation as described, will my existing page be disposed of properly when I navigate to the next page please? – Sutts99 Jul 21 '23 at 17:34
  • That is how I interpret the documentation. But I fail to understand how you can get to this point in any case. I mean, you should be able to go back to previous page most of the time. If you have an app that jumps around to different pages all the time perhaps you have a structural problem that is the root cause of all this. Maybe we are trying to solve the wrong problem. – Peter Wessberg Jul 21 '23 at 18:46
  • Thanks Peter. It's an app for testing pipework and relies on a strict sequence of screens with no possibility of backtracking. All navigation is handled programmatically with back buttons disabled. – Sutts99 Jul 21 '23 at 20:39
  • You probably shouldn’t be using navigation for this use case – Jason Jul 21 '23 at 21:05
  • @Jason, please explain. What are you suggesting as an alternative? – ToolmakerSteve Jul 21 '23 at 21:12
  • just assign `MainPage` when you want to change the page – Jason Jul 21 '23 at 21:13
  • 1
    I agree with @Jason. What you should do is just do Application.Current.MainPage = new MyPageView(); – Peter Wessberg Jul 21 '23 at 21:30
  • 1
    Indeed, there may be a memory issue doing what this answer suggests. Unless it is done "just right". See my answer if you want to continue using Shell, yet avoid those issues. I also cover the other two types of Maui Navigation. Setting MainPage directly, as in the avove comment, and NavigationPage, a more "direct" management of a navigation stack. – ToolmakerSteve Jul 21 '23 at 21:53
  • @PeterWessberg and Jason: FYI, given Sutts report of memory problems, I've updated my answer to show how to use IServiceProvider to avoid use of "new", with DI Singleton registration, in the direct setting of `MainPage`. – ToolmakerSteve Jul 25 '23 at 22:42