Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go back to without popping each page #3048

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
Loading