1+ using System . Diagnostics ;
12using System . Text . RegularExpressions ;
23using System . Web ;
34using Prism . Common ;
@@ -16,7 +17,7 @@ public class PageNavigationService : INavigationService, IRegistryAware
1617{
1718 private static readonly SemaphoreSlim _semaphore = new ( 1 , 1 ) ;
1819 private static readonly TimeSpan _minTimeBetweenNavigations = TimeSpan . FromMilliseconds ( 150 ) ;
19- private static DateTime _lastNavigate ;
20+ private static long _lastNavigateTimestamp ;
2021 internal const string RemovePageRelativePath = "../" ;
2122 internal const string RemovePageInstruction = "__RemovePage/" ;
2223 internal const string RemovePageSegment = "__RemovePage" ;
@@ -84,7 +85,7 @@ public virtual async Task<INavigationResult> GoBackAsync(INavigationParameters p
8485 }
8586 finally
8687 {
87- _lastNavigate = DateTime . Now ;
88+ _lastNavigateTimestamp = Stopwatch . GetTimestamp ( ) ;
8889 NavigationSource = PageNavigationSource . Device ;
8990 _semaphore . Release ( ) ;
9091 }
@@ -199,7 +200,7 @@ public virtual async Task<INavigationResult> GoBackToAsync(string viewName, INav
199200 }
200201 finally
201202 {
202- _lastNavigate = DateTime . Now ;
203+ _lastNavigateTimestamp = Stopwatch . GetTimestamp ( ) ;
203204 NavigationSource = PageNavigationSource . Device ;
204205 _semaphore . Release ( ) ;
205206 }
@@ -293,7 +294,7 @@ public virtual async Task<INavigationResult> GoBackToRootAsync(INavigationParame
293294 }
294295 finally
295296 {
296- _lastNavigate = DateTime . Now ;
297+ _lastNavigateTimestamp = Stopwatch . GetTimestamp ( ) ;
297298 NavigationSource = PageNavigationSource . Device ;
298299 _semaphore . Release ( ) ;
299300 }
@@ -337,7 +338,7 @@ public virtual async Task<INavigationResult> NavigateAsync(Uri uri, INavigationP
337338 }
338339 finally
339340 {
340- _lastNavigate = DateTime . Now ;
341+ _lastNavigateTimestamp = Stopwatch . GetTimestamp ( ) ;
341342 NavigationSource = PageNavigationSource . Device ;
342343 _semaphore . Release ( ) ;
343344 }
@@ -430,7 +431,7 @@ x is NavigationPage navPage
430431 }
431432 finally
432433 {
433- _lastNavigate = DateTime . Now ;
434+ _lastNavigateTimestamp = Stopwatch . GetTimestamp ( ) ;
434435 NavigationSource = PageNavigationSource . Device ;
435436 _semaphore . Release ( ) ;
436437 }
@@ -440,13 +441,42 @@ private static async Task WaitForPendingNavigationRequests()
440441 {
441442 await _semaphore . WaitAsync ( ) ;
442443 // Ensure adequate time has passed since last navigation so that UI Refresh can Occur
443- TimeSpan timeSinceLastNav = DateTime . Now - _lastNavigate ;
444- if ( timeSinceLastNav < _minTimeBetweenNavigations )
444+ var delay = GetRemainingNavigationDelay ( _lastNavigateTimestamp , Stopwatch . GetTimestamp ( ) ) ;
445+ if ( delay > TimeSpan . Zero )
445446 {
446- await Task . Delay ( _minTimeBetweenNavigations - timeSinceLastNav ) ;
447+ await Task . Delay ( delay ) ;
447448 }
448449 }
449450
451+ /// <summary>
452+ /// Exposes the minimum interval enforced between navigations. Used by tests.
453+ /// </summary>
454+ internal static TimeSpan MinTimeBetweenNavigations => _minTimeBetweenNavigations ;
455+
456+ /// <summary>
457+ /// Calculates how long the next navigation should wait so that at least
458+ /// <see cref="_minTimeBetweenNavigations"/> elapses between navigations, giving the native
459+ /// platform time to push/pop a page before the next request begins.
460+ /// </summary>
461+ /// <param name="lastNavigateTimestamp">The <see cref="Stopwatch.GetTimestamp"/> value captured after the previous navigation.</param>
462+ /// <param name="currentTimestamp">The current <see cref="Stopwatch.GetTimestamp"/> value.</param>
463+ internal static TimeSpan GetRemainingNavigationDelay ( long lastNavigateTimestamp , long currentTimestamp )
464+ {
465+ var elapsed = Stopwatch . GetElapsedTime ( lastNavigateTimestamp , currentTimestamp ) ;
466+
467+ // A monotonic timestamp should always move forward, so elapsed is normally non-negative.
468+ // If it ever regresses (e.g. a device clock/timezone change producing a backward reading),
469+ // treat it as "no time elapsed" and wait the full minimum rather than the size of the
470+ // backward jump. This keeps navigation responsive instead of freezing for the duration of
471+ // the jump - e.g. an hour when travelling Eastern -> Central. See issue #3405.
472+ if ( elapsed < TimeSpan . Zero )
473+ return _minTimeBetweenNavigations ;
474+
475+ return elapsed < _minTimeBetweenNavigations
476+ ? _minTimeBetweenNavigations - elapsed
477+ : TimeSpan . Zero ;
478+ }
479+
450480 /// <summary>
451481 /// Processes the Navigation for the Queued navigation segments
452482 /// </summary>
0 commit comments