Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions src/Controls/src/Core/FlyoutPage/FlyoutPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,7 @@ public Page Detail

var previousDetail = _detail;

// Get the actual pages for navigation events (unwrap NavigationPages)
var destinationPage =
value is NavigationPage destinationNavPage ? destinationNavPage.CurrentPage : value;
var previousPage = previousDetail is NavigationPage previousNavPage
? previousNavPage.CurrentPage
: previousDetail;

// Send NavigatingFrom event to the previous detail (if any)
if (previousDetail is not null)
{
previousDetail.SendNavigatingFrom(new NavigatingFromEventArgs(destinationPage,
NavigationType.Replace));
}
previousDetail?.SendNavigatingFrom(new NavigatingFromEventArgs(destinationPage: value, navigationType: NavigationType.Replace));

// Update the detail property
OnPropertyChanging();
Expand All @@ -87,10 +75,10 @@ public Page Detail
if (previousDetail is not null)
{
previousDetail.SendNavigatedFrom(
new NavigatedFromEventArgs(destinationPage, NavigationType.Replace));
new NavigatedFromEventArgs(destinationPage: value, NavigationType.Replace));
}

_detail.SendNavigatedTo(new NavigatedToEventArgs(previousPage, NavigationType.Replace));
_detail.SendNavigatedTo(new NavigatedToEventArgs(previousDetail, NavigationType.Replace));
}
}

Expand Down
352 changes: 351 additions & 1 deletion src/Controls/tests/Core.UnitTests/FlyoutPageUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,356 @@ public async Task VerifyToolbarButtonVisibilityWhenFlyoutReset(int depth)

Assert.True(flyoutToolBar.IsVisible);
}
}

[Fact]
public void FlyoutPageDetailNavigation_EventsWithContentPage()
{
// Test that navigation events are fired directly on the page when Detail is a ContentPage
var firstDetail = new NavigationObserverPage { Title = "First Detail" };
var secondDetail = new NavigationObserverPage { Title = "Second Detail" };
var flyoutPage = new FlyoutPage
{
Flyout = new ContentPage { Title = "Flyout" },
Detail = firstDetail
}.AddToTestWindow();

flyoutPage.Detail = secondDetail;

// Verify NavigatingFrom was called on the first detail directly
Assert.NotNull(firstDetail.NavigatingFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatingFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatingFromArgs.NavigationType);

// Verify NavigatedFrom was called on the first detail directly
Assert.NotNull(firstDetail.NavigatedFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatedFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatedFromArgs.NavigationType);

// Verify NavigatedTo was called on the second detail directly
Assert.NotNull(secondDetail.NavigatedToArgs);
Assert.Equal(firstDetail, secondDetail.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, secondDetail.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageDetailNavigation_EventsWithNavigationPage()
{
// Test that navigation events are fired on NavigationPage directly, NOT on CurrentPage
var firstDetailContent = new NavigationObserverPage { Title = "First Detail Content" };
var firstDetail = new TrackingNavigationPage(firstDetailContent) { Title = "First Detail Nav" };
var secondDetailContent = new NavigationObserverPage { Title = "Second Detail Content" };
var secondDetail = new TrackingNavigationPage(secondDetailContent) { Title = "Second Detail Nav" };
var flyoutPage = new FlyoutPage
{
Flyout = new ContentPage { Title = "Flyout" },
Detail = firstDetail
}.AddToTestWindow();

// Clear any initial navigation args on the content pages to focus on Detail replacement
firstDetailContent.ClearNavigationArgs();
secondDetailContent.ClearNavigationArgs();
firstDetail.ClearNavigationArgs();
secondDetail.ClearNavigationArgs();

flyoutPage.Detail = secondDetail;

// Verify that navigation events WERE fired on the NavigationPage containers
Assert.NotNull(firstDetail.NavigatingFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatingFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatingFromArgs.NavigationType);

Assert.NotNull(firstDetail.NavigatedFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatedFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatedFromArgs.NavigationType);

Assert.NotNull(secondDetail.NavigatedToArgs);
Assert.Equal(firstDetail, secondDetail.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, secondDetail.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageDetailNavigation_MixedPageTypes()
{
// Test that navigation events work correctly when mixing ContentPage and NavigationPage
var firstDetail = new NavigationObserverPage { Title = "First Detail" };
var secondDetailContent = new NavigationObserverPage { Title = "Second Detail Content" };
var secondDetail = new TrackingNavigationPage(secondDetailContent) { Title = "Second Detail Nav" };
var flyoutPage = new FlyoutPage
{
Flyout = new ContentPage { Title = "Flyout" },
Detail = firstDetail
}.AddToTestWindow();

// Clear initial navigation args
secondDetailContent.ClearNavigationArgs();
secondDetail.ClearNavigationArgs();

flyoutPage.Detail = secondDetail;

// Verify NavigatingFrom was called on the ContentPage directly
Assert.NotNull(firstDetail.NavigatingFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatingFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatingFromArgs.NavigationType);

// Verify NavigatedFrom was called on the ContentPage directly
Assert.NotNull(firstDetail.NavigatedFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatedFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatedFromArgs.NavigationType);

// Verify NavigatedTo was called on the NavigationPage container
Assert.NotNull(secondDetail.NavigatedToArgs);
Assert.Equal(firstDetail, secondDetail.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, secondDetail.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageDetailNavigation_NavigationPageToContentPage()
{
// Test navigation from NavigationPage to ContentPage
var firstDetailContent = new NavigationObserverPage { Title = "First Detail Content" };
var firstDetail = new TrackingNavigationPage(firstDetailContent) { Title = "First Detail Nav" };
var secondDetail = new NavigationObserverPage { Title = "Second Detail" };
var flyoutPage = new FlyoutPage
{
Flyout = new ContentPage { Title = "Flyout" },
Detail = firstDetail
}.AddToTestWindow();

// Clear any initial navigation args
firstDetailContent.ClearNavigationArgs();
firstDetail.ClearNavigationArgs();
secondDetail.ClearNavigationArgs();

flyoutPage.Detail = secondDetail;

// Verify NavigatingFrom was called on the NavigationPage container
Assert.NotNull(firstDetail.NavigatingFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatingFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatingFromArgs.NavigationType);

// Verify NavigatedFrom was called on the NavigationPage container
Assert.NotNull(firstDetail.NavigatedFromArgs);
Assert.Equal(secondDetail, firstDetail.NavigatedFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatedFromArgs.NavigationType);

// Verify NavigatedTo was called on the ContentPage with NavigationPage as previous page
Assert.NotNull(secondDetail.NavigatedToArgs);
Assert.Equal(firstDetail, secondDetail.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, secondDetail.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageFlyoutNavigation_EventsWithContentPage()
{
// Test that navigation events are fired directly on Flyout pages when they are ContentPages
var firstFlyout = new NavigationObserverPage { Title = "First Flyout" };
var secondFlyout = new NavigationObserverPage { Title = "Second Flyout" };
var flyoutPage = new FlyoutPage
{
Flyout = firstFlyout,
Detail = new ContentPage { Title = "Detail" }
}.AddToTestWindow();

flyoutPage.Flyout = secondFlyout;

// Verify NavigatingFrom was called on the first flyout directly
Assert.NotNull(firstFlyout.NavigatingFromArgs);
Assert.Equal(secondFlyout, firstFlyout.NavigatingFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstFlyout.NavigatingFromArgs.NavigationType);

// Verify NavigatedFrom was called on the first flyout directly
Assert.NotNull(firstFlyout.NavigatedFromArgs);
Assert.Equal(secondFlyout, firstFlyout.NavigatedFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstFlyout.NavigatedFromArgs.NavigationType);

// Verify NavigatedTo was called on the second flyout directly
Assert.NotNull(secondFlyout.NavigatedToArgs);
Assert.Equal(firstFlyout, secondFlyout.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, secondFlyout.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageFlyoutNavigation_EventsWithNavigationPage()
{
// Test that navigation events are fired on NavigationPage directly for Flyout, NOT on CurrentPage
var firstFlyoutContent = new NavigationObserverPage { Title = "First Flyout Content" };
var firstFlyout = new TrackingNavigationPage(firstFlyoutContent) { Title = "First Flyout Nav" };
var secondFlyoutContent = new NavigationObserverPage { Title = "Second Flyout Content" };
var secondFlyout = new TrackingNavigationPage(secondFlyoutContent) { Title = "Second Flyout Nav" };
var flyoutPage = new FlyoutPage
{
Flyout = firstFlyout,
Detail = new ContentPage { Title = "Detail" }
}.AddToTestWindow();

// Clear any initial navigation args on the content pages
firstFlyoutContent.ClearNavigationArgs();
secondFlyoutContent.ClearNavigationArgs();
firstFlyout.ClearNavigationArgs();
secondFlyout.ClearNavigationArgs();

flyoutPage.Flyout = secondFlyout;

// Verify that navigation events WERE fired on the NavigationPage containers
Assert.NotNull(firstFlyout.NavigatingFromArgs);
Assert.Equal(secondFlyout, firstFlyout.NavigatingFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstFlyout.NavigatingFromArgs.NavigationType);

Assert.NotNull(firstFlyout.NavigatedFromArgs);
Assert.Equal(secondFlyout, firstFlyout.NavigatedFromArgs.DestinationPage);
Assert.Equal(NavigationType.Replace, firstFlyout.NavigatedFromArgs.NavigationType);

Assert.NotNull(secondFlyout.NavigatedToArgs);
Assert.Equal(firstFlyout, secondFlyout.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, secondFlyout.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageDetailNavigation_CorrectNavigationType()
{
// Test that all navigation events use NavigationType.Replace
var firstDetail = new NavigationObserverPage { Title = "First Detail" };
var secondDetail = new NavigationObserverPage { Title = "Second Detail" };
var flyoutPage = new FlyoutPage
{
Flyout = new ContentPage { Title = "Flyout" },
Detail = firstDetail
}.AddToTestWindow();

flyoutPage.Detail = secondDetail;

Assert.Equal(NavigationType.Replace, firstDetail.NavigatingFromArgs.NavigationType);
Assert.Equal(NavigationType.Replace, firstDetail.NavigatedFromArgs.NavigationType);
Assert.Equal(NavigationType.Replace, secondDetail.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageFlyoutNavigation_CorrectNavigationType()
{
// Test that all Flyout navigation events use NavigationType.Replace
var firstFlyout = new NavigationObserverPage { Title = "First Flyout" };
var secondFlyout = new NavigationObserverPage { Title = "Second Flyout" };
var flyoutPage = new FlyoutPage
{
Flyout = firstFlyout,
Detail = new ContentPage { Title = "Detail" }
}.AddToTestWindow();

flyoutPage.Flyout = secondFlyout;

Assert.Equal(NavigationType.Replace, firstFlyout.NavigatingFromArgs.NavigationType);
Assert.Equal(NavigationType.Replace, firstFlyout.NavigatedFromArgs.NavigationType);
Assert.Equal(NavigationType.Replace, secondFlyout.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageDetailNavigation_NullPreviousDetailHandled()
{
// Test that navigation events are handled correctly when there's no previous Detail
var detail = new NavigationObserverPage { Title = "Detail" };
var flyoutPage = new FlyoutPage
{
Flyout = new ContentPage { Title = "Flyout" }
};

// Set detail first, which should trigger NavigatedTo with null previous page
flyoutPage.Detail = detail;

// Then add to window after both properties are set
var window = flyoutPage.AddToTestWindow();

// Verify NavigatedTo was called with null previous page
Assert.NotNull(detail.NavigatedToArgs);
Assert.Null(detail.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, detail.NavigatedToArgs.NavigationType);
}

[Fact]
public void FlyoutPageFlyoutNavigation_NullPreviousFlyoutHandled()
{
// Test that navigation events are handled correctly when there's no previous Flyout
var flyout = new NavigationObserverPage { Title = "Flyout" };
var flyoutPage = new FlyoutPage
{
Detail = new ContentPage { Title = "Detail" }
};

// Set flyout first, which should trigger NavigatedTo with null previous page
flyoutPage.Flyout = flyout;

// Then add to window after both properties are set
var window = flyoutPage.AddToTestWindow();

// Verify NavigatedTo was called with null previous page
Assert.NotNull(flyout.NavigatedToArgs);
Assert.Null(flyout.NavigatedToArgs.PreviousPage);
Assert.Equal(NavigationType.Replace, flyout.NavigatedToArgs.NavigationType);
}

public class NavigationObserverPage : ContentPage
{
public NavigatedFromEventArgs NavigatedFromArgs { get; private set; }
public NavigatingFromEventArgs NavigatingFromArgs { get; private set; }
public NavigatedToEventArgs NavigatedToArgs { get; private set; }

public void ClearNavigationArgs()
{
NavigatedFromArgs = null;
NavigatingFromArgs = null;
NavigatedToArgs = null;
}

protected override void OnNavigatedFrom(NavigatedFromEventArgs args)
{
base.OnNavigatedFrom(args);
NavigatedFromArgs = args;
}

protected override void OnNavigatingFrom(NavigatingFromEventArgs args)
{
base.OnNavigatingFrom(args);
NavigatingFromArgs = args;
}

protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
NavigatedToArgs = args;
}
}

public class TrackingNavigationPage : NavigationPage
{
public NavigatedFromEventArgs NavigatedFromArgs { get; private set; }
public NavigatingFromEventArgs NavigatingFromArgs { get; private set; }
public NavigatedToEventArgs NavigatedToArgs { get; private set; }

public TrackingNavigationPage(Page root) : base(root) { }

public void ClearNavigationArgs()
{
NavigatedFromArgs = null;
NavigatingFromArgs = null;
NavigatedToArgs = null;
}

protected override void OnNavigatedFrom(NavigatedFromEventArgs args)
{
base.OnNavigatedFrom(args);
NavigatedFromArgs = args;
}

protected override void OnNavigatingFrom(NavigatingFromEventArgs args)
{
base.OnNavigatingFrom(args);
NavigatingFromArgs = args;
}

protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
NavigatedToArgs = args;
}
}
}
}
Loading