diff --git a/Directory.Build.props b/Directory.Build.props index d7fdad5..f7d8373 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,9 +16,11 @@ $(MSBuildThisFileDirectory)LICENSE prism;maui;dotnet-maui;xaml;mvvm;ios;android;mac;winui Copyright Dan Siegel 2021 - This is a special preview package. In order to use this be sure that you have installed the Maui workload with Visual Studio 2022 preview 4 or later. Alternatively you may use Maui-Check to ensure that your environment has the required prerequesites to build from the command line with Visual Studio Code. + + Prism is a fully open source version of the Prism guidance originally produced by Microsoft Patterns & Practices. Prism provides an implementation of a collection of design patterns that are helpful in writing well structured, maintainable, and testable XAML applications, including MVVM, dependency injection, commanding, event aggregation, and more. Prism's core functionality is a shared library targeting the .NET Framework and .NET Standard. Features that need to be platform specific are implemented in the respective libraries for the target platform (WPF, Uno Platform, Xamarin Forms, and .NET MAUI). -As this is special preview build there are no docs, be sure to check out the source repo at: https://github.com/dansiegel/prism.maui + Prism for .NET MAUI helps you more easily design and build rich, flexible, and easy to maintain .NET MAUI applications. This library provides user interface composition as well as modularity support. + true false @@ -38,7 +40,7 @@ As this is special preview build there are no docs, be sure to check out the sou { + containerRegistry.RegisterGlobalNavigationObserver(); containerRegistry.RegisterForNavigation(); containerRegistry.RegisterForNavigation(); containerRegistry.RegisterForNavigation(); }) + .AddGlobalNavigationObserver(context => context.Subscribe(x => + { + if (x.Type == NavigationRequestType.Navigate) + Console.WriteLine($"Navigation: {x.Type} - {x.Uri}"); + else + Console.WriteLine($"Navigation: {x.Type}"); + + var status = x.Cancelled ? "Cancelled" : x.Result.Success ? "Success" : "Failed"; + Console.WriteLine($"Result: {status}"); + })) .OnAppStart(navigationService => navigationService.CreateBuilder() .AddNavigationSegment("MainPage") .AddNavigationPage() diff --git a/sample/PrismMauiDemo/PrismMauiDemo.csproj b/sample/PrismMauiDemo/PrismMauiDemo.csproj index 77d7b35..a81774e 100644 --- a/sample/PrismMauiDemo/PrismMauiDemo.csproj +++ b/sample/PrismMauiDemo/PrismMauiDemo.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Prism.DryIoc.Maui/Prism.DryIoc.Maui.csproj b/src/Prism.DryIoc.Maui/Prism.DryIoc.Maui.csproj index a08ee0e..a62a415 100644 --- a/src/Prism.DryIoc.Maui/Prism.DryIoc.Maui.csproj +++ b/src/Prism.DryIoc.Maui/Prism.DryIoc.Maui.csproj @@ -5,6 +5,7 @@ true true true + Prism.DryIoc.Maui provides the implementation of Prism's IContainerExtension using the DryIoc container. This is currently the only supported container for .NET MAUI. diff --git a/src/Prism.Maui.Rx/GlobalNavigationObserver.cs b/src/Prism.Maui.Rx/GlobalNavigationObserver.cs new file mode 100644 index 0000000..befd906 --- /dev/null +++ b/src/Prism.Maui.Rx/GlobalNavigationObserver.cs @@ -0,0 +1,29 @@ +using System; +using System.Reactive.Subjects; +using Prism.Events; + +namespace Prism.Navigation; + +internal class GlobalNavigationObserver : IGlobalNavigationObserver, IDisposable +{ + private readonly Subject _subject; + private SubscriptionToken _token; + + public GlobalNavigationObserver(IEventAggregator eventAggregator) + { + _subject = new Subject(); + _token = eventAggregator.GetEvent().Subscribe(context => _subject.OnNext(context)); + } + + public IObservable NavigationRequest => _subject; + + public void Dispose() + { + if (_token is null) + return; + + _token.Dispose(); + _token = null; + _subject.Dispose(); + } +} diff --git a/src/Prism.Maui.Rx/IGlobalNavigationObserver.cs b/src/Prism.Maui.Rx/IGlobalNavigationObserver.cs new file mode 100644 index 0000000..1474089 --- /dev/null +++ b/src/Prism.Maui.Rx/IGlobalNavigationObserver.cs @@ -0,0 +1,8 @@ +using System; + +namespace Prism.Navigation; + +public interface IGlobalNavigationObserver +{ + IObservable NavigationRequest { get; } +} diff --git a/src/Prism.Maui.Rx/NavigationObserverRegistrationExtensions.cs b/src/Prism.Maui.Rx/NavigationObserverRegistrationExtensions.cs new file mode 100644 index 0000000..f34a157 --- /dev/null +++ b/src/Prism.Maui.Rx/NavigationObserverRegistrationExtensions.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.DependencyInjection; +using Prism.Ioc; + +namespace Prism.Navigation; + +public static class NavigationObserverRegistrationExtensions +{ + private static bool s_IsRegistered; + + public static IContainerRegistry RegisterGlobalNavigationObserver(this IContainerRegistry container) + { + if (s_IsRegistered) + return container; + + s_IsRegistered = true; + return container.RegisterSingleton(); + } + + public static IServiceCollection RegisterGlobalNavigationObserver(this IServiceCollection services) + { + if (s_IsRegistered) + return services; + + s_IsRegistered = true; + return services.AddSingleton(); + } + + public static PrismAppBuilder AddGlobalNavigationObserver(this PrismAppBuilder builder, Action> addObservable) => + builder.OnInitialized(c => + { + if (!s_IsRegistered) + throw new Exception("IGlobalNavigationObserver has not been registered. Be sure to call 'container.RegisterGlobalNavigationObserver()'."); + + addObservable(c.Resolve().NavigationRequest); + }); + + public static PrismAppBuilder AddGlobalNavigationObserver(this PrismAppBuilder builder, Action> addObservable) => + builder.OnInitialized(c => + { + if (!s_IsRegistered) + throw new Exception("IGlobalNavigationObserver has not been registered. Be sure to call 'container.RegisterGlobalNavigationObserver()'."); + + addObservable(c, c.Resolve().NavigationRequest); + }); +} \ No newline at end of file diff --git a/src/Prism.Maui.Rx/Prism.Maui.Rx.csproj b/src/Prism.Maui.Rx/Prism.Maui.Rx.csproj new file mode 100644 index 0000000..65145f8 --- /dev/null +++ b/src/Prism.Maui.Rx/Prism.Maui.Rx.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + true + Prism.Maui.Rx is a support package for .NET MAUI developers. This package provides some helpers to access an IObservable for globally handling Navigation Request Results. + + + + + + + + + + + diff --git a/src/Prism.Maui/Behaviors/NavigationPageSystemGoBackBehavior.cs b/src/Prism.Maui/Behaviors/NavigationPageSystemGoBackBehavior.cs index 68015c9..ba29568 100644 --- a/src/Prism.Maui/Behaviors/NavigationPageSystemGoBackBehavior.cs +++ b/src/Prism.Maui/Behaviors/NavigationPageSystemGoBackBehavior.cs @@ -21,6 +21,7 @@ private void NavigationPage_Popped(object sender, NavigationEventArgs e) { if (PageNavigationService.NavigationSource == PageNavigationSource.Device) { + System.Diagnostics.Trace.WriteLine("NavigationPage has encountered an unhandled GoBack. Be sure to inherit from PrismNavigationPage."); PageUtilities.HandleSystemGoBack(e.Page, AssociatedObject.CurrentPage); } } diff --git a/src/Prism.Maui/Common/PageUtilities.cs b/src/Prism.Maui/Common/PageUtilities.cs index 122f048..4ea1190 100644 --- a/src/Prism.Maui/Common/PageUtilities.cs +++ b/src/Prism.Maui/Common/PageUtilities.cs @@ -229,19 +229,8 @@ public static void SetCurrentPageDelegate(Func getCurrentPageDelegat public static async Task HandleNavigationPageGoBack(NavigationPage navigationPage) { - var previousPage = navigationPage.CurrentPage; - var parameters = new NavigationParameters(); - parameters.GetNavigationParametersInternal().Add(KnownInternalParameters.NavigationMode, NavigationMode.Back); - if (!CanNavigate(previousPage, parameters) || - !await CanNavigateAsync(previousPage, parameters)) - return; - - PageNavigationService.NavigationSource = PageNavigationSource.NavigationService; - await navigationPage.PopAsync(); - PageNavigationService.NavigationSource = PageNavigationSource.Device; - OnNavigatedFrom(previousPage, parameters); - OnNavigatedTo(navigationPage.CurrentPage, parameters); - DestroyPage(previousPage); + var navigationService = Navigation.Xaml.Navigation.GetNavigationService(navigationPage.CurrentPage); + await navigationService.GoBackAsync(); } public static void HandleSystemGoBack(IView previousPage, IView currentPage) diff --git a/src/Prism.Maui/Events/NavigationRequestEvent.cs b/src/Prism.Maui/Events/NavigationRequestEvent.cs new file mode 100644 index 0000000..23c591e --- /dev/null +++ b/src/Prism.Maui/Events/NavigationRequestEvent.cs @@ -0,0 +1,8 @@ +using Prism.Navigation; + +namespace Prism.Events; + +public class NavigationRequestEvent : PubSubEvent +{ +} + diff --git a/src/Prism.Maui/Navigation/KnownNavigationParameters.cs b/src/Prism.Maui/Navigation/KnownNavigationParameters.cs index d24afd1..2c0cb05 100644 --- a/src/Prism.Maui/Navigation/KnownNavigationParameters.cs +++ b/src/Prism.Maui/Navigation/KnownNavigationParameters.cs @@ -17,6 +17,11 @@ public static class KnownNavigationParameters /// public const string UseModalNavigation = "useModalNavigation"; + /// + /// Used to control whether the navigation should be animated. + /// + public const string Animated = "animated"; + /// /// Used to define a navigation parameter that is bound directly to a CommandParameter via {Binding .}. /// diff --git a/src/Prism.Maui/Navigation/NavigationRequestContext.cs b/src/Prism.Maui/Navigation/NavigationRequestContext.cs new file mode 100644 index 0000000..7f6eb1c --- /dev/null +++ b/src/Prism.Maui/Navigation/NavigationRequestContext.cs @@ -0,0 +1,10 @@ +namespace Prism.Navigation; + +public record NavigationRequestContext +{ + public bool Cancelled => Result?.Exception is not null && Result.Exception is NavigationException ne && ne.Message == NavigationException.IConfirmNavigationReturnedFalse; + public NavigationRequestType Type { get; init; } + public Uri? Uri { get; init; } + public INavigationParameters Parameters { get; init; } + public INavigationResult Result { get; init; } +} \ No newline at end of file diff --git a/src/Prism.Maui/Navigation/NavigationRequestType.cs b/src/Prism.Maui/Navigation/NavigationRequestType.cs new file mode 100644 index 0000000..5cb9c61 --- /dev/null +++ b/src/Prism.Maui/Navigation/NavigationRequestType.cs @@ -0,0 +1,8 @@ +namespace Prism.Navigation; + +public enum NavigationRequestType +{ + Navigate, + GoBack, + GoToRoot, +} diff --git a/src/Prism.Maui/Navigation/PageNavigationService.cs b/src/Prism.Maui/Navigation/PageNavigationService.cs index ab63851..2420bcc 100644 --- a/src/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Prism.Maui/Navigation/PageNavigationService.cs @@ -1,5 +1,6 @@ using Prism.Behaviors; using Prism.Common; +using Prism.Events; using Prism.Ioc; using Application = Microsoft.Maui.Controls.Application; @@ -19,6 +20,7 @@ public class PageNavigationService : INavigationService, IPageAware private readonly IContainerProvider _container; protected readonly IApplication _application; + protected readonly IEventAggregator _eventAggregator; protected Window Window; protected Page _page; @@ -47,100 +49,65 @@ Page IPageAware.Page /// /// The that will be used to resolve pages for navigation. /// The that will let us ensure the Application.MainPage is set. - /// The that will apply base and custom behaviors to pages created in the . - public PageNavigationService(IContainerProvider container, IApplication application) + /// The that will raise . + public PageNavigationService(IContainerProvider container, IApplication application, IEventAggregator eventAggregator) { _container = container; _application = application; - } - - /// - /// Navigates to the most recent entry in the back navigation history by popping the calling Page off the navigation stack. - /// - /// If true a go back operation was successful. If false the go back operation failed. - public virtual Task GoBackAsync() - { - return GoBackAsync(null); - } - - /// - /// Navigates to the most recent entry in the back navigation history by popping the calling Page off the navigation stack. - /// - /// The navigation parameters - /// If true a go back operation was successful. If false the go back operation failed. - public virtual Task GoBackAsync(INavigationParameters parameters) - { - return GoBackInternal(parameters, null, true); - } - - /// - /// Navigates to the most recent entry in the back navigation history by popping the calling Page off the navigation stack. - /// - /// The navigation parameters - /// If true uses PopModalAsync, if false uses PopAsync - /// If true the transition is animated, if false there is no animation on transition. - /// indicating whether the request was successful or if there was an encountered . - public virtual Task GoBackAsync(INavigationParameters parameters, bool? useModalNavigation, bool animated) - { - return GoBackInternal(parameters, useModalNavigation, animated); + _eventAggregator = eventAggregator; } /// /// Navigates to the most recent entry in the back navigation history by popping the calling Page off the navigation stack. /// /// The navigation parameters - /// If true uses PopModalAsync, if false uses PopAsync - /// If true the transition is animated, if false there is no animation on transition. /// If true a go back operation was successful. If false the go back operation failed. - protected async virtual Task GoBackInternal(INavigationParameters parameters, bool? useModalNavigation, bool animated) + public virtual async Task GoBackAsync(INavigationParameters parameters) { Page page = null; try { + if (parameters is null) + parameters = new NavigationParameters(); + NavigationSource = PageNavigationSource.NavigationService; page = GetCurrentPage(); if (IsRoot(GetPageFromWindow(), page)) throw new NavigationException(NavigationException.CannotPopApplicationMainPage, page); - var segmentParameters = UriParsingHelper.GetSegmentParameters(null, parameters); - segmentParameters.GetNavigationParametersInternal().Add(KnownInternalParameters.NavigationMode, NavigationMode.Back); + parameters.GetNavigationParametersInternal().Add(KnownInternalParameters.NavigationMode, NavigationMode.Back); - var canNavigate = await PageUtilities.CanNavigateAsync(page, segmentParameters); + var canNavigate = await PageUtilities.CanNavigateAsync(page, parameters); if (!canNavigate) { - return new NavigationResult - { - Exception = new NavigationException(NavigationException.IConfirmNavigationReturnedFalse, page) - }; + throw new NavigationException(NavigationException.IConfirmNavigationReturnedFalse, page); } - bool useModalForDoPop = UseModalGoBack(page, useModalNavigation); + bool useModalForDoPop = UseModalGoBack(page, parameters); Page previousPage = PageUtilities.GetOnNavigatedToTarget(page, Window?.Page, useModalForDoPop); + bool animated = parameters.ContainsKey(KnownNavigationParameters.Animated) ? parameters.GetValue(KnownNavigationParameters.Animated) : true; var poppedPage = await DoPop(page.Navigation, useModalForDoPop, animated); if (poppedPage != null) { - PageUtilities.OnNavigatedFrom(page, segmentParameters); - PageUtilities.OnNavigatedTo(previousPage, segmentParameters); + PageUtilities.OnNavigatedFrom(page, parameters); + PageUtilities.OnNavigatedTo(previousPage, parameters); PageUtilities.DestroyPage(poppedPage); - return new NavigationResult { Success = true }; + return Notify(NavigationRequestType.GoBack, parameters); } } catch (Exception ex) { - return new NavigationResult { Exception = ex }; + return Notify(NavigationRequestType.GoBack, parameters, ex); } finally { NavigationSource = PageNavigationSource.Device; } - return new NavigationResult - { - Exception = GetGoBackException(page, GetPageFromWindow()) - }; + return Notify(NavigationRequestType.GoBack, parameters, GetGoBackException(page, GetPageFromWindow())); } private static Exception GetGoBackException(Page currentPage, IView mainPage) @@ -189,17 +156,7 @@ private static bool IsMainPage(IView currentPage, IView mainPage) /// The navigation parameters /// indicating whether the request was successful or if there was an encountered . /// Only works when called from a View within a NavigationPage - public virtual Task GoBackToRootAsync(INavigationParameters parameters) - { - return GoBackToRootInternal(parameters); - } - - /// - /// When navigating inside a NavigationPage: Pops all but the root Page off the navigation stack - /// - /// The navigation parameters - /// Only works when called from a View within a NavigationPage - protected async virtual Task GoBackToRootInternal(INavigationParameters parameters) + public virtual async Task GoBackToRootAsync(INavigationParameters parameters) { try { @@ -212,10 +169,7 @@ protected async virtual Task GoBackToRootInternal(INavigation var canNavigate = await PageUtilities.CanNavigateAsync(page, parameters); if (!canNavigate) { - return new NavigationResult - { - Exception = new NavigationException(NavigationException.IConfirmNavigationReturnedFalse, page) - }; + throw new NavigationException(NavigationException.IConfirmNavigationReturnedFalse, page); } var pagesToDestroy = page.Navigation.NavigationStack.ToList(); // get all pages to destroy @@ -223,7 +177,10 @@ protected async virtual Task GoBackToRootInternal(INavigation var root = pagesToDestroy.Last(); pagesToDestroy.Remove(root); //don't destroy the root page - await page.Navigation.PopToRootAsync(); + bool animated = parameters.ContainsKey(KnownNavigationParameters.Animated) ? parameters.GetValue(KnownNavigationParameters.Animated) : true; + NavigationSource = PageNavigationSource.NavigationService; + await page.Navigation.PopToRootAsync(animated); + NavigationSource = PageNavigationSource.Device; foreach (var destroyPage in pagesToDestroy) { @@ -233,29 +190,16 @@ protected async virtual Task GoBackToRootInternal(INavigation PageUtilities.OnNavigatedTo(root, parameters); - return new NavigationResult { Success = true }; + return Notify(NavigationRequestType.GoToRoot, parameters); } catch (Exception ex) { - return new NavigationResult { Exception = ex }; + return Notify(NavigationRequestType.GoToRoot, parameters, ex); } - } - - /// - /// Initiates navigation to the target specified by the . - /// - /// The name of the target to navigate to. - /// The navigation parameters - /// If true uses PopModalAsync, if false uses PopAsync - /// If true the transition is animated, if false there is no animation on transition. - protected virtual Task NavigateInternal(string name, INavigationParameters parameters, bool? useModalNavigation, bool animated) - { - if (name.StartsWith(RemovePageRelativePath)) + finally { - name = name.Replace(RemovePageRelativePath, RemovePageInstruction); + NavigationSource = PageNavigationSource.Device; } - - return NavigateInternal(UriParsingHelper.Parse(name), parameters, useModalNavigation, animated); } /// @@ -267,44 +211,31 @@ protected virtual Task NavigateInternal(string name, INavigat /// /// NavigateAsync(new Uri("MainPage?id=3&name=brian", UriKind.RelativeSource), parameters); /// - public virtual Task NavigateAsync(Uri uri, INavigationParameters parameters) - { - return NavigateInternal(uri, parameters, null, true); - } - - /// - /// Initiates navigation to the target specified by the . - /// - /// The Uri to navigate to - /// The navigation parameters - /// If true uses PopModalAsync, if false uses PopAsync - /// If true the transition is animated, if false there is no animation on transition. - /// Navigation parameters can be provided in the Uri and by using the . - /// - /// Navigate(new Uri("MainPage?id=3&name=brian", UriKind.RelativeSource), parameters); - /// - protected async virtual Task NavigateInternal(Uri uri, INavigationParameters parameters, bool? useModalNavigation, bool animated) + public virtual async Task NavigateAsync(Uri uri, INavigationParameters parameters) { try { + if (parameters is null) + parameters = new NavigationParameters(); + NavigationSource = PageNavigationSource.NavigationService; var navigationSegments = UriParsingHelper.GetUriSegments(uri); if (uri.IsAbsoluteUri) { - await ProcessNavigationForAbsoluteUri(navigationSegments, parameters, useModalNavigation, animated); - return new NavigationResult { Success = true }; + await ProcessNavigationForAbsoluteUri(navigationSegments, parameters, null, true); } else { - await ProcessNavigation(GetCurrentPage(), navigationSegments, parameters, useModalNavigation, animated); - return new NavigationResult { Success = true }; + await ProcessNavigation(GetCurrentPage(), navigationSegments, parameters, null, true); } + + return Notify(uri, parameters); } catch (Exception ex) { - return new NavigationResult { Exception = ex }; + return Notify(uri, parameters, ex); } finally { @@ -331,10 +262,9 @@ protected virtual async Task ProcessNavigation(Page currentPage, Queue s var nextSegment = segments.Dequeue(); var pageParameters = UriParsingHelper.GetSegmentParameters(nextSegment); - if (pageParameters.ContainsKey(KnownNavigationParameters.UseModalNavigation)) - { - useModalNavigation = pageParameters.GetValue(KnownNavigationParameters.UseModalNavigation); - } + //var useModalNavigation = pageParameters.ContainsKey(KnownNavigationParameters.UseModalNavigation) ? pageParameters.GetValue(KnownNavigationParameters.UseModalNavigation) : false; + + //var animated = pageParameters.ContainsKey(KnownNavigationParameters.Animated) ? pageParameters.GetValue(KnownNavigationParameters.Animated) : true; if (nextSegment == RemovePageSegment) { @@ -1040,10 +970,10 @@ internal static bool UseModalNavigation(Page currentPage, bool? useModalNavigati return useModalNavigation; } - internal bool UseModalGoBack(Page currentPage, bool? useModalNavigationDefault) + internal bool UseModalGoBack(Page currentPage, INavigationParameters parameters) { - if (useModalNavigationDefault.HasValue) - return useModalNavigationDefault.Value; + if (parameters.ContainsKey(KnownNavigationParameters.UseModalNavigation)) + return parameters.GetValue(KnownNavigationParameters.UseModalNavigation); else if (currentPage is NavigationPage navPage) return GoBackModal(navPage); else if (PageUtilities.HasNavigationPageParent(currentPage, out var navParent)) @@ -1070,6 +1000,42 @@ internal static bool UseReverseNavigation(Page currentPage, Type nextPageType) return PageUtilities.HasNavigationPageParent(currentPage) && PageUtilities.IsSameOrSubclassOf(nextPageType); } + private INavigationResult Notify(NavigationRequestType type, INavigationParameters parameters, Exception exception = null) + { + var result = new NavigationResult + { + Exception = exception, + Success = exception is null + }; + _eventAggregator.GetEvent().Publish(new NavigationRequestContext + { + Parameters = parameters, + Result = result, + Type = type, + }); + + return result; + } + + private INavigationResult Notify(Uri uri, INavigationParameters parameters, Exception exception = null) + { + var result = new NavigationResult + { + Exception = exception, + Success = exception is null, + }; + + _eventAggregator.GetEvent().Publish(new NavigationRequestContext + { + Parameters = parameters, + Result = result, + Type = NavigationRequestType.Navigate, + Uri = uri, + }); + + return result; + } + protected static bool IsRoot(Page mainPage, Page currentPage) { if (mainPage == currentPage) diff --git a/src/Prism.Maui/PrismApplication.cs b/src/Prism.Maui/PrismApplication.cs index d0a0ca2..e7eb188 100644 --- a/src/Prism.Maui/PrismApplication.cs +++ b/src/Prism.Maui/PrismApplication.cs @@ -20,6 +20,17 @@ protected PrismApplication() _containerExtension = ContainerLocator.Current; RegisterTypes(_containerExtension); NavigationService = Container.Resolve((typeof(IApplication), this)); + this.ModalPopping += PrismApplication_ModalPopping; + } + + private async void PrismApplication_ModalPopping(object sender, ModalPoppingEventArgs e) + { + if (PageNavigationService.NavigationSource == PageNavigationSource.NavigationService) + return; + + e.Cancel = true; + var navService = Navigation.Xaml.Navigation.GetNavigationService(e.Modal); + await navService.GoBackAsync(); } void ILegacyPrismApplication.OnInitialized() => OnInitialized();