From 22ca5cf88099e2b3291413ce09ac0fba04ea6a04 Mon Sep 17 00:00:00 2001 From: Alex Macocian Date: Fri, 23 Aug 2024 23:00:19 +0200 Subject: [PATCH] Fix navigation --- .../ControlFlow/IMainPagePresenter.cs | 6 ++ .../ControlFlow/INavigationService.cs | 11 +-- .../ControlFlow/NavigationService.cs | 90 +++++++++++++++---- .../ControlFlow/ScopedApplicationShell.cs | 35 -------- .../ControlFlow/ScopedPageContext.cs | 7 ++ Net.Maui.Extensions/ExtendedApplication.cs | 27 ++++-- .../MauiAppBuilderExtensions.cs | 13 +-- .../Net.Maui.Extensions.csproj | 4 +- 8 files changed, 117 insertions(+), 76 deletions(-) create mode 100644 Net.Maui.Extensions/ControlFlow/IMainPagePresenter.cs delete mode 100644 Net.Maui.Extensions/ControlFlow/ScopedApplicationShell.cs create mode 100644 Net.Maui.Extensions/ControlFlow/ScopedPageContext.cs diff --git a/Net.Maui.Extensions/ControlFlow/IMainPagePresenter.cs b/Net.Maui.Extensions/ControlFlow/IMainPagePresenter.cs new file mode 100644 index 0000000..e557e11 --- /dev/null +++ b/Net.Maui.Extensions/ControlFlow/IMainPagePresenter.cs @@ -0,0 +1,6 @@ +namespace Net.Maui.Extensions.ControlFlow; + +public interface IMainPagePresenter + where TMainPage : ContentPage +{ +} diff --git a/Net.Maui.Extensions/ControlFlow/INavigationService.cs b/Net.Maui.Extensions/ControlFlow/INavigationService.cs index 1ff36ad..8ad7179 100644 --- a/Net.Maui.Extensions/ControlFlow/INavigationService.cs +++ b/Net.Maui.Extensions/ControlFlow/INavigationService.cs @@ -2,15 +2,10 @@ public interface INavigationService { - Task GoTo(bool animated = false) + void GoTo(bool animated = false) where T : ContentPage; - Task GoToModal(bool animated = false) - where T : ContentPage; + void GoBack(bool animated = false); - Task GoBack(bool animated = false); - - Task GoBackModal(bool animated = false); - - Task GoBackToRoot(bool animated = false); + void GoBackToRoot(bool animated = false); } diff --git a/Net.Maui.Extensions/ControlFlow/NavigationService.cs b/Net.Maui.Extensions/ControlFlow/NavigationService.cs index 73d5085..a0407f8 100644 --- a/Net.Maui.Extensions/ControlFlow/NavigationService.cs +++ b/Net.Maui.Extensions/ControlFlow/NavigationService.cs @@ -1,54 +1,106 @@ using Microsoft.Extensions.Logging; using System.Core.Extensions; +using System.Extensions; using System.Extensions.Core; namespace Net.Maui.Extensions.ControlFlow; internal sealed class NavigationService : INavigationService { - private readonly ScopedApplicationShell scopedApplicationShell; + private readonly Stack modalStack = new(); + private readonly Stack navigationStack = new(); + private readonly IServiceProvider serviceProvider; private readonly ILogger logger; + private ExtendedApplication? extendedApplication; + public NavigationService( - ScopedApplicationShell scopedApplicationShell, + IServiceProvider serviceProvider, ILogger logger) { - this.scopedApplicationShell = scopedApplicationShell.ThrowIfNull(); + this.serviceProvider = serviceProvider.ThrowIfNull(); this.logger = logger.ThrowIfNull(); } - public async Task GoBack(bool animated = false) + public void GoBack(bool animated = false) { var scopedLogger = this.logger.CreateScopedLogger(); - scopedLogger.LogInformation("Going back"); - await this.scopedApplicationShell.Navigation.PopAsync(animated); + if (this.navigationStack.Count <= 1) + { + scopedLogger.LogError("Cannot go back. Navigation stack is at root"); + return; + } + + scopedLogger.LogDebug("Going back"); + if (this.navigationStack.TryPop(out var context)) + { + context.Scope?.Dispose(); + } + + this.ShowCurrentPage(); } - public async Task GoBackModal(bool animated = false) + public void GoBackToRoot(bool animated = false) { var scopedLogger = this.logger.CreateScopedLogger(); - scopedLogger.LogInformation("Going back"); - await this.scopedApplicationShell.Navigation.PopModalAsync(animated); + scopedLogger.LogDebug("Going back to root"); + while (this.navigationStack.Count > 1 && this.navigationStack.TryPop(out var context)) + { + context.Scope?.Dispose(); + } + + this.ShowCurrentPage(); } - public async Task GoBackToRoot(bool animated = false) + public void GoTo(bool animated = false) where T : ContentPage { var scopedLogger = this.logger.CreateScopedLogger(); - scopedLogger.LogInformation("Going back to root"); - await this.scopedApplicationShell.Navigation.PopToRootAsync(animated); + scopedLogger.LogDebug($"Going to {typeof(T).Name}"); + + var context = this.GetScopedPageAndProvider(); + this.navigationStack.Push(context); + this.ShowCurrentPage(); } - public async Task GoTo(bool animated = false) where T : ContentPage + public void SetNavigationRoot(ExtendedApplication extendedApplication, Type rootPageType) { var scopedLogger = this.logger.CreateScopedLogger(); - scopedLogger.LogInformation($"Going to {typeof(T).Name}"); - await this.scopedApplicationShell.PushScoped(); + scopedLogger.LogDebug($"Setting navigation root {rootPageType.Name}"); + + this.extendedApplication = extendedApplication.ThrowIfNull(); + var context = this.GetScopedPageAndProvider(rootPageType); + this.navigationStack.Push(context); + this.ShowCurrentPage(); } - public async Task GoToModal(bool animated = false) where T : ContentPage + private void ShowCurrentPage() { - var scopedLogger = this.logger.CreateScopedLogger(); - scopedLogger.LogInformation($"Going to {typeof(T).Name}"); - await this.scopedApplicationShell.PushModalScoped(); + if (this.extendedApplication is null) + { + return; + } + + if (this.navigationStack.TryPeek(out var currentContext)) + { + this.extendedApplication.MainPage = currentContext.Page; + } + } + + private ScopedPageContext GetScopedPageAndProvider() + where T : ContentPage + { + var scope = this.serviceProvider.CreateScope(); + return new ScopedPageContext { Page = scope.ServiceProvider.GetRequiredService(), Scope = scope }; + } + + private ScopedPageContext GetScopedPageAndProvider(Type type) + { + if (!type.IsAssignableTo(typeof(ContentPage))) + { + throw new InvalidOperationException($"{type.Name} must be of type {nameof(ContentPage)}"); + } + + var scope = this.serviceProvider.CreateScope(); + return new ScopedPageContext { Page = scope.ServiceProvider.GetRequiredService(type).Cast(), Scope = scope }; } } diff --git a/Net.Maui.Extensions/ControlFlow/ScopedApplicationShell.cs b/Net.Maui.Extensions/ControlFlow/ScopedApplicationShell.cs deleted file mode 100644 index 42b5ee0..0000000 --- a/Net.Maui.Extensions/ControlFlow/ScopedApplicationShell.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Core.Extensions; - -namespace Net.Maui.Extensions.ControlFlow; - -public class ScopedApplicationShell : Shell -{ - private readonly IServiceProvider serviceProvider; - - internal ScopedApplicationShell( - IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider.ThrowIfNull(); - } - - public async Task PushScoped() - where T : ContentPage - { - var page = this.GetScopedPage(); - await this.Navigation.PushAsync(page); - } - - public async Task PushModalScoped() - where T : ContentPage - { - var page = this.GetScopedPage(); - await this.Navigation.PushModalAsync(page); - } - - private T GetScopedPage() - where T : ContentPage - { - using var scope = this.serviceProvider.CreateScope(); - return scope.ServiceProvider.GetRequiredService(); - } -} diff --git a/Net.Maui.Extensions/ControlFlow/ScopedPageContext.cs b/Net.Maui.Extensions/ControlFlow/ScopedPageContext.cs new file mode 100644 index 0000000..f24c670 --- /dev/null +++ b/Net.Maui.Extensions/ControlFlow/ScopedPageContext.cs @@ -0,0 +1,7 @@ +namespace Net.Maui.Extensions.ControlFlow; + +internal sealed class ScopedPageContext +{ + public ContentPage? Page { get; init; } + public IServiceScope? Scope { get; init; } +} diff --git a/Net.Maui.Extensions/ExtendedApplication.cs b/Net.Maui.Extensions/ExtendedApplication.cs index 4dc8def..f00452b 100644 --- a/Net.Maui.Extensions/ExtendedApplication.cs +++ b/Net.Maui.Extensions/ExtendedApplication.cs @@ -1,20 +1,35 @@ using Net.Maui.Extensions.ControlFlow; using System.Core.Extensions; +using System.Extensions; namespace Net.Maui.Extensions; public abstract class ExtendedApplication : Application { - public ScopedApplicationShell ScopedShell { get; } + private readonly INavigationService navigationService; - public ExtendedApplication(ScopedApplicationShell shell) + public ExtendedApplication( + INavigationService navigationService) { - if (shell is not ScopedApplicationShell) + this.navigationService = navigationService.ThrowIfNull(); + this.SetMainPage(); + } + + private void SetMainPage() + { + var currentType = this.GetType(); + + // Find the interface that implements IMainPagePresenter<> + var interfaceType = currentType + .GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMainPagePresenter<>)); + + if (interfaceType is null) { - throw new InvalidOperationException($"Shell is not of type {typeof(ScopedApplicationShell).Name}"); + throw new InvalidOperationException($"{currentType.Name} must implement IMainPagePresenter"); } - this.MainPage = shell.ThrowIfNull(); - this.ScopedShell = shell.ThrowIfNull(); + var mainPageType = interfaceType.GetGenericArguments()[0]; + this.navigationService.Cast().SetNavigationRoot(this, mainPageType); } } diff --git a/Net.Maui.Extensions/MauiAppBuilderExtensions.cs b/Net.Maui.Extensions/MauiAppBuilderExtensions.cs index d8a9495..538a779 100644 --- a/Net.Maui.Extensions/MauiAppBuilderExtensions.cs +++ b/Net.Maui.Extensions/MauiAppBuilderExtensions.cs @@ -1,8 +1,10 @@ using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Net.Maui.Extensions.Attributes; using Net.Maui.Extensions.Context; using Net.Maui.Extensions.ControlFlow; using System.Core.Extensions; +using System.Extensions; using System.Reflection; namespace Net.Maui.Extensions; @@ -46,19 +48,18 @@ public static class MauiAppBuilderExtensions return appBuilder; } - public static MauiAppBuilder WithExtendedApp(this MauiAppBuilder appBuilder) - where T : ExtendedApplication + public static MauiAppBuilder WithExtendedApp(this MauiAppBuilder appBuilder) + where TApp : ExtendedApplication, IMainPagePresenter + where TMainPage : ContentPage { - appBuilder.ThrowIfNull().UseMauiApp(); + appBuilder.ThrowIfNull().UseMauiApp(); return appBuilder; } public static MauiAppBuilder WithNavigation(this MauiAppBuilder appBuilder) { appBuilder.ThrowIfNull(); - appBuilder.Services.AddScoped(sp => new ScopedApplicationShell(sp)); - appBuilder.Services.AddScoped(sp => sp.GetRequiredService()); - appBuilder.Services.AddScoped(); + appBuilder.Services.AddSingleton(sp => new NavigationService(sp, sp.GetRequiredService>())); return appBuilder; } diff --git a/Net.Maui.Extensions/Net.Maui.Extensions.csproj b/Net.Maui.Extensions/Net.Maui.Extensions.csproj index 7f2ec1d..4d52827 100644 --- a/Net.Maui.Extensions/Net.Maui.Extensions.csproj +++ b/Net.Maui.Extensions/Net.Maui.Extensions.csproj @@ -20,7 +20,7 @@ enable enable true - 0.2.3 + 0.2.4 Alexandru Macocian LICENSE true @@ -52,7 +52,7 @@ - +