Skip to content

Commit

Permalink
Merge pull request #3048 from niimima/master_/feature/gobackto-withou…
Browse files Browse the repository at this point in the history
…t-popping-each-page

[WIP] Go back to without popping each page
  • Loading branch information
dansiegel authored Feb 11, 2024
2 parents cef42ed + a6e14b5 commit 17f270b
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 3 deletions.
9 changes: 9 additions & 0 deletions e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ protected ViewModelBase(BaseServices baseServices)
SelectedDialog = AvailableDialogs.FirstOrDefault();
ShowDialog = new DelegateCommand(OnShowDialogCommand, () => !string.IsNullOrEmpty(SelectedDialog))
.ObservesProperty(() => SelectedDialog);
GoBack = new DelegateCommand<string>(OnGoBack);
}

public IEnumerable<string> AvailableDialogs { get; }
Expand All @@ -52,6 +53,8 @@ public string SelectedDialog

public DelegateCommand ShowDialog { get; }

public DelegateCommand<string> GoBack { get; }

private void OnNavigateCommandExecuted(string uri)
{
Messages.Add($"OnNavigateCommandExecuted: {uri}");
Expand All @@ -74,6 +77,12 @@ private void OnShowDialogCommand()
private void DialogCallback(IDialogResult result) =>
Messages.Add("Dialog Closed");

private void OnGoBack(string viewName)
{
Messages.Add($"On Go Back {viewName}");
_navigationService.GoBackAsync(viewName);
}

public void Initialize(INavigationParameters parameters)
{
Messages.Add("ViewModel Initialized");
Expand Down
10 changes: 8 additions & 2 deletions e2e/Maui/MauiModule/Views/ViewD.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
x:DataType="loc:ViewModelBase"
Title="{Binding Title}"
BackgroundColor="White">
<Grid RowDefinitions="*,Auto,Auto,Auto"
<Grid RowDefinitions="*,Auto,Auto,Auto,Auto"
ColumnDefinitions="*,*">
<CollectionView ItemsSource="{Binding Messages}"
Grid.ColumnSpan="2">
Expand Down Expand Up @@ -57,5 +57,11 @@
Margin="10"
Grid.Row="3"
Grid.Column="1"/>
<Button Text="Go Back View B"
Command="{Binding GoBack}"
CommandParameter="ViewB"
Margin="10"
Grid.Row="4"
Grid.Column="0"/>
</Grid>
</ContentPage>
</ContentPage>
8 changes: 8 additions & 0 deletions src/Maui/Prism.Maui/Navigation/INavigationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public interface INavigationService
/// <returns>If <c>true</c> a go back operation was successful. If <c>false</c> the go back operation failed.</returns>
Task<INavigationResult> GoBackAsync(INavigationParameters parameters);

/// <summary>
/// Navigates to the most recent entry in the back navigation history for the <paramref name="viewName"/>.
/// </summary>
/// <param name="viewName">The name of the View to navigate back to</param>
/// <param name="parameters">The navigation parameters</param>
/// <returns>If <c>true</c> a go back operation was successful. If <c>false</c> the go back operation failed.</returns>
Task<INavigationResult> GoBackAsync(string viewName, INavigationParameters parameters);

/// <summary>
/// When navigating inside a NavigationPage: Pops all but the root Page off the navigation stack
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static Task<INavigationResult> GoBackToAsync(this INavigationService navi
/// </summary>
/// <returns><see cref="INavigationResult"/> indicating whether the request was successful or if there was an encountered <see cref="Exception"/>.</returns>
public static Task<INavigationResult> GoBackAsync(this INavigationService navigationService) =>
navigationService.GoBackAsync(null);
navigationService.GoBackAsync(new NavigationParameters());

/// <summary>
/// Navigates to the most recent entry in the back navigation history by popping the calling Page off the navigation stack.
Expand All @@ -33,6 +33,14 @@ public static Task<INavigationResult> GoBackAsync(this INavigationService naviga
return navigationService.GoBackAsync(GetNavigationParameters(parameters));
}

/// <summary>
/// Navigates to the most recent entry in the back navigation history for the <paramref name="viewName"/>.
/// </summary>
/// <param name="navigationService">Service for handling navigation between views</param>
/// <param name="viewName">The name of the View to navigate back to</param>
/// <returns>If <c>true</c> a go back operation was successful. If <c>false</c> the go back operation failed.</returns>
public static Task<INavigationResult> GoBackAsync(this INavigationService navigationService, string viewName) => navigationService.GoBackAsync(viewName, new NavigationParameters());

/// <summary>
/// When navigating inside a NavigationPage: Pops all but the root Page off the navigation stack
/// </summary>
Expand Down
61 changes: 61 additions & 0 deletions src/Maui/Prism.Maui/Navigation/PageNavigationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Web;
using Prism.Common;
using Prism.Events;
using Prism.Extensions;
using Prism.Mvvm;
using Application = Microsoft.Maui.Controls.Application;
using XamlTab = Prism.Navigation.Xaml.TabbedPage;
Expand Down Expand Up @@ -192,6 +193,66 @@ private async Task<INavigationResult> GoBackInternalAsync(INavigationParameters
return Notify(NavigationRequestType.GoBack, parameters, GetGoBackException(page, GetPageFromWindow()));
}

/// <inheritdoc />
public virtual async Task<INavigationResult> GoBackAsync(string viewName, INavigationParameters parameters)
{
await _semaphore.WaitAsync();
try
{
if (parameters is null)
parameters = new NavigationParameters();

parameters.GetNavigationParametersInternal().Add(KnownInternalParameters.NavigationMode, NavigationMode.Back);

var page = GetCurrentPage();
var canNavigate = await MvvmHelpers.CanNavigateAsync(page, parameters);
if (!canNavigate)
{
throw new NavigationException(NavigationException.IConfirmNavigationReturnedFalse, page);
}

var pagesToDestroy = page.Navigation.NavigationStack.ToList(); // get all pages to destroy
pagesToDestroy.Reverse(); // destroy them in reverse order
var goBackPage = pagesToDestroy.FirstOrDefault(p => ViewModelLocator.GetNavigationName(p) == viewName); // find the go back page
if (goBackPage is null)
{
throw new NavigationException(NavigationException.GoBackRequiresNavigationPage);
}
var index = pagesToDestroy.IndexOf(goBackPage);
pagesToDestroy.RemoveRange(index, pagesToDestroy.Count - index); // don't destroy pages from the go back page to the root page
var pagesToRemove = pagesToDestroy.Skip(1).ToList(); // exclude the current page from the destroy pages

bool animated = parameters.ContainsKey(KnownNavigationParameters.Animated) ? parameters.GetValue<bool>(KnownNavigationParameters.Animated) : true;
NavigationSource = PageNavigationSource.NavigationService;
foreach(var removePage in pagesToRemove)
{
page.Navigation.RemovePage(removePage);
}
await page.Navigation.PopAsync(animated);
NavigationSource = PageNavigationSource.Device;

foreach (var destroyPage in pagesToDestroy)
{
MvvmHelpers.OnNavigatedFrom(destroyPage, parameters);
MvvmHelpers.DestroyPage(destroyPage);
}

MvvmHelpers.OnNavigatedTo(goBackPage, parameters);

return Notify(NavigationRequestType.GoBack, parameters);
}
catch (Exception ex)
{
return Notify(NavigationRequestType.GoBack, parameters, ex);
}
finally
{
NavigationSource = PageNavigationSource.Device;
_semaphore.Release();
}
}


private static Exception GetGoBackException(Page currentPage, IView mainPage)
{
if (IsMainPage(currentPage, mainPage))
Expand Down
5 changes: 5 additions & 0 deletions src/Prism.Core/Navigation/NavigationException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public class NavigationException : Exception
/// </summary>
public const string CannotGoBackFromRoot = "Cannot GoBack from NavigationPage Root.";

/// <summary>
/// The <see cref="NavigationException"/> Message returned when GoBackAsync can only be called when the calling Page has been navigated.
/// </summary>
public const string GoBackRequiresNavigationPage = "GoBackAsync can only be called when the calling Page has been navigated.";

/// <summary>
/// The <see cref="NavigationException"/> Message returned when GoBackToRootAsync can only be called when the calling Page is within a NavigationPage.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,59 @@ public async Task GoBack_Issue2232()
Assert.IsType<MockViewA>(navigationPage.CurrentPage);
}

[Fact]
public async Task GoBack_Name_PopsToSpecifiedViewWithoutPoppingEachPage()
{
var mauiApp = CreateBuilder(prism => prism.CreateWindow("NavigationPage/MockViewA/MockViewB/MockViewC/MockViewD/MockViewE"))
.Build();
var window = GetWindow(mauiApp);

Assert.IsAssignableFrom<NavigationPage>(window.Page);
var navigationPage = (NavigationPage)window.Page;
var withoutPoppingPage = (MockViewD)navigationPage.Navigation.NavigationStack.First(p => ViewModelLocator.GetNavigationName(p) == nameof(MockViewD));
var withoutPoppingPageVm = (MockViewModelBase)withoutPoppingPage.BindingContext;

Assert.IsType<MockViewA>(navigationPage.RootPage);
Assert.IsType<MockViewE>(navigationPage.CurrentPage);

var result = await navigationPage.CurrentPage.GetContainerProvider()
.Resolve<INavigationService>()
.GoBackAsync("MockViewC");

Assert.True(result.Success);

Assert.IsType<MockViewC>(navigationPage.CurrentPage);

// In the GoBackAsync method, the OnNavigatedTo method is not called for pages that are not popped.
Assert.True(withoutPoppingPageVm.Actions.Last() == nameof(MockViewModelBase.OnNavigatedFrom));
}

[Fact]
public async Task GoBack_Name_PopsToSpecifiedViewWithoutPoppingEachPageOfLimitation()
{
var mauiApp = CreateBuilder(prism => prism.CreateWindow("NavigationPage/MockViewA/MockViewA/MockViewB/MockViewC/MockViewD/MockViewE"))
.Build();
var window = GetWindow(mauiApp);

Assert.IsAssignableFrom<NavigationPage>(window.Page);
var navigationPage = (NavigationPage)window.Page;

Assert.IsType<MockViewA>(navigationPage.RootPage);
Assert.IsType<MockViewE>(navigationPage.CurrentPage);

var result = await navigationPage.CurrentPage.GetContainerProvider()
.Resolve<INavigationService>()
.GoBackAsync("MockViewA");

Assert.True(result.Success);

Assert.IsType<MockViewA>(navigationPage.CurrentPage);

// If there are two instances of MockViewA, it will return to the instance closest to the current page.
// Therefore, the current modal stack will be in the state of NavigationPage/MockViewA/MockViewA.
Assert.Equal(2, navigationPage.Navigation.NavigationStack.Count);
}

[Fact]
public async Task TabbedPage_SelectTabSets_CurrentTab()
{
Expand Down

0 comments on commit 17f270b

Please sign in to comment.