From 664d8d1f2cb01d021401019c6ecacaa72556b8ed Mon Sep 17 00:00:00 2001 From: Jacob Fink Date: Thu, 30 Dec 2021 09:31:56 -0500 Subject: [PATCH] create navigation service --- src/App/Abstractions/INavigationService.cs | 45 ++++++ src/App/App.xaml.cs | 4 +- src/App/Services/NavigationService.cs | 164 +++++++++++++++++++++ 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/App/Abstractions/INavigationService.cs create mode 100644 src/App/Services/NavigationService.cs diff --git a/src/App/Abstractions/INavigationService.cs b/src/App/Abstractions/INavigationService.cs new file mode 100644 index 000000000..b5741de50 --- /dev/null +++ b/src/App/Abstractions/INavigationService.cs @@ -0,0 +1,45 @@ +using Bit.App.Pages; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Abstractions +{ + public interface INavigationService + { + /// + /// Sets the viewmodel to be the main page of the application + /// + void PresentAsMainPage(BaseViewModel viewModel); + + /// + /// Sets the viewmodel as the main page of the application, and wraps its page within a Navigation page + /// + void PresentAsNavigatableMainPage(BaseViewModel viewModel); + + /// + /// Navigate to the given page on top of the current navigation stack + /// + Task NavigateTo(BaseViewModel viewModel); + + /// + /// Navigate to the previous item in the navigation stack + /// + Task NavigateBack(); + + /// + /// Navigate back to the element at the root of the navigation stack + /// + Task NavigateBackToRoot(); + } + + public interface IViewLocator + { + Page CreateAndBindPageFor(TViewModel viewModel) where TViewModel : BaseViewModel; + } + + public interface IMainPage + { + Page MainPage { get; set; } + } + +} diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index ab31a0818..28889a1f8 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -15,7 +15,7 @@ using Xamarin.Forms.Xaml; [assembly: XamlCompilation(XamlCompilationOptions.Compile)] namespace Bit.App { - public partial class App : Application + public partial class App : Application, IMainPage { private readonly IUserService _userService; private readonly IBroadcasterService _broadcasterService; @@ -51,6 +51,8 @@ namespace Bit.App _secureStorageService = ServiceContainer.Resolve("secureStorageService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + var navigator = new NavigationService(this, new ViewLocator()); + Bootstrap(); _broadcasterService.Subscribe(nameof(App), async (message) => { diff --git a/src/App/Services/NavigationService.cs b/src/App/Services/NavigationService.cs new file mode 100644 index 000000000..c0c3cbb6b --- /dev/null +++ b/src/App/Services/NavigationService.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xamarin.Forms; +using Xamarin.Forms.Internals; +using Bit.App.Abstractions; +using Bit.App.Pages; + +namespace Bit.App.Services +{ + public class NavigationService : INavigationService + { + private readonly IMainPage _presentationRoot; + private readonly IViewLocator _viewLocator; + + public NavigationService(IMainPage presentationRoot, IViewLocator viewLocator) + { + _presentationRoot = presentationRoot; + _viewLocator = viewLocator; + } + + private Xamarin.Forms.INavigation Navigator => _presentationRoot.MainPage.Navigation; + + public void PresentAsMainPage(BaseViewModel viewModel) + { + var page = _viewLocator.CreateAndBindPageFor(viewModel); + + IEnumerable viewModelsToDismiss = FindViewModelsToDismiss(_presentationRoot.MainPage); + + if (_presentationRoot.MainPage is NavigationPage navPage) + { + // If we're replacing a navigation page, unsub from events + navPage.PopRequested -= NavPagePopRequested; + } + + // viewModel.BeforeFirstShown(); + + _presentationRoot.MainPage = page; + + foreach (BaseViewModel toDismiss in viewModelsToDismiss) + { + // toDismiss.AfterDismissed(); + } + } + + public void PresentAsNavigatableMainPage(BaseViewModel viewModel) + { + var page = _viewLocator.CreateAndBindPageFor(viewModel); + + NavigationPage newNavigationPage = new NavigationPage(page); + + IEnumerable viewModelsToDismiss = FindViewModelsToDismiss(_presentationRoot.MainPage); + + if (_presentationRoot.MainPage is NavigationPage navPage) + { + navPage.PopRequested -= NavPagePopRequested; + } + + // viewModel.BeforeFirstShown(); + + // Listen for back button presses on the new navigation bar + newNavigationPage.PopRequested += NavPagePopRequested; + _presentationRoot.MainPage = newNavigationPage; + + foreach (BaseViewModel toDismiss in viewModelsToDismiss) + { + // toDismiss.AfterDismissed(); + } + } + + private IEnumerable FindViewModelsToDismiss(Page dismissingPage) + { + var viewmodels = new List(); + + if (dismissingPage is NavigationPage) + { + viewmodels.AddRange( + Navigator + .NavigationStack + .Select(p => p.BindingContext) + .OfType() + ); + } + else + { + var viewmodel = dismissingPage?.BindingContext as BaseViewModel; + if (viewmodel != null) viewmodels.Add(viewmodel); + } + + return viewmodels; + } + + private void NavPagePopRequested(object sender, NavigationRequestedEventArgs e) + { + if (Navigator.NavigationStack.LastOrDefault()?.BindingContext is BaseViewModel poppingPage) + { + // poppingPage.AfterDismissed(); + } + } + + public async Task NavigateTo(BaseViewModel viewModel) + { + var page = _viewLocator.CreateAndBindPageFor(viewModel); + + // await viewModel.BeforeFirstShown(); + + await Navigator.PushAsync(page); + } + + public async Task NavigateBack() + { + var dismissing = Navigator.NavigationStack.Last().BindingContext as BaseViewModel; + + await Navigator.PopAsync(); + + // dismissing?.AfterDismissed(); + } + + public async Task NavigateBackToRoot() + { + var toDismiss = Navigator + .NavigationStack + .Skip(1) + .Select(vw => vw.BindingContext) + .OfType() + .ToArray(); + + await Navigator.PopToRootAsync(); + + foreach (BaseViewModel viewModel in toDismiss) + { + // viewModel.AfterDismissed().FireAndForget(); + } + } + } + + public class ViewLocator : IViewLocator + { + public Page CreateAndBindPageFor(TViewModel viewModel) where TViewModel : BaseViewModel + { + var pageType = FindPageForViewModel(viewModel.GetType()); + + var page = (Page)Activator.CreateInstance(pageType); + + page.BindingContext = viewModel; + + return page; + } + + protected virtual Type FindPageForViewModel(Type viewModelType) + { + var pageTypeName = viewModelType + .AssemblyQualifiedName + .Replace("ViewModel", ""); + + var pageType = Type.GetType(pageTypeName); + if (pageType == null) + throw new ArgumentException(pageTypeName + " type does not exist"); + + return pageType; + } + } +} \ No newline at end of file