@@ -35,6 +35,29 @@ public event EventHandler<LocationChangedEventArgs> LocationChanged
3535
3636 private CancellationTokenSource ? _locationChangingCts ;
3737
38+ /// <summary>
39+ /// An event that fires when the page is not found.
40+ /// </summary>
41+ public event EventHandler < NotFoundEventArgs > NotFoundEvent
42+ {
43+ add
44+ {
45+ AssertInitialized ( ) ;
46+ _notFound += value ;
47+ }
48+ remove
49+ {
50+ AssertInitialized ( ) ;
51+ _notFound -= value ;
52+ }
53+ }
54+
55+ private EventHandler < NotFoundEventArgs > ? _notFound ;
56+
57+ private readonly List < Func < NotFoundContext , ValueTask > > _notFoundHandlers = new ( ) ;
58+
59+ private CancellationTokenSource ? _notFoundCts ;
60+
3861 // For the baseUri it's worth storing as a System.Uri so we can do operations
3962 // on that type. System.Uri gives us access to the original string anyway.
4063 private Uri ? _baseUri ;
@@ -177,6 +200,16 @@ protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)]
177200 public virtual void Refresh ( bool forceReload = false )
178201 => NavigateTo ( Uri , forceLoad : true , replace : true ) ;
179202
203+ /// <summary>
204+ /// TODO
205+ /// </summary>
206+ public virtual void NotFound ( ) => NotFoundCore ( ) ;
207+
208+ /// <summary>
209+ /// TODO
210+ /// </summary>
211+ protected virtual void NotFoundCore ( ) => throw new NotImplementedException ( ) ;
212+
180213 /// <summary>
181214 /// Called to initialize BaseURI and current URI before these values are used for the first time.
182215 /// Override <see cref="EnsureInitialized" /> and call this method to dynamically calculate these values.
@@ -308,6 +341,26 @@ protected void NotifyLocationChanged(bool isInterceptedLink)
308341 }
309342 }
310343
344+ /// <summary>
345+ /// Triggers the <see cref="NotFound"/> event with the current URI value.
346+ /// </summary>
347+ protected void NotifyNotFound ( bool isInterceptedLink )
348+ {
349+ try
350+ {
351+ _notFound ? . Invoke (
352+ this ,
353+ new NotFoundEventArgs ( isInterceptedLink )
354+ {
355+ HistoryEntryState = HistoryEntryState
356+ } ) ;
357+ }
358+ catch ( Exception ex )
359+ {
360+ throw new NotFoundRenderingException ( "An exception occurred while dispatching a NotFound event." , ex ) ;
361+ }
362+ }
363+
311364 /// <summary>
312365 /// Notifies the registered handlers of the current location change.
313366 /// </summary>
@@ -433,12 +486,135 @@ protected async ValueTask<bool> NotifyLocationChangingAsync(string uri, string?
433486 cts . Dispose ( ) ;
434487
435488 if ( _locationChangingCts == cts )
436- {
489+ {
437490 _locationChangingCts = null ;
438491 }
439492 }
440493 }
441494
495+ /// <summary>
496+ /// Notifies the registered handlers of the current ot found event.
497+ /// </summary>
498+ /// <param name="isNavigationIntercepted">Whether this not found was intercepted from a link.</param>
499+ /// <returns>A <see cref="ValueTask{TResult}"/> representing the completion of the operation. If the result is <see langword="true"/>, the navigation should continue.</returns>
500+ protected async ValueTask < bool > NotifyNotFoundAsync ( bool isNavigationIntercepted )
501+ {
502+ _notFoundCts ? . Cancel ( ) ;
503+ _notFoundCts = null ;
504+
505+ var handlerCount = _notFoundHandlers . Count ;
506+
507+ if ( handlerCount == 0 )
508+ {
509+ return true ;
510+ }
511+
512+ var cts = new CancellationTokenSource ( ) ;
513+
514+ _notFoundCts = cts ;
515+
516+ var cancellationToken = cts . Token ;
517+ var context = new NotFoundContext
518+ {
519+ // HistoryEntryState = state,
520+ IsNavigationIntercepted = isNavigationIntercepted ,
521+ CancellationToken = cancellationToken ,
522+ } ;
523+
524+ try
525+ {
526+ if ( handlerCount == 1 )
527+ {
528+ var handlerTask = InvokeNotFoundHandlerAsync ( _notFoundHandlers [ 0 ] , context ) ;
529+
530+ if ( handlerTask . IsFaulted )
531+ {
532+ await handlerTask ;
533+ return false ; // Unreachable because the previous line will throw.
534+ }
535+
536+ if ( context . DidPreventRendering )
537+ {
538+ return false ;
539+ }
540+
541+ if ( ! handlerTask . IsCompletedSuccessfully )
542+ {
543+ await handlerTask . AsTask ( ) . WaitAsync ( cancellationToken ) ;
544+ }
545+ }
546+ else
547+ {
548+ var notFoundHandlersCopy = ArrayPool < Func < NotFoundContext , ValueTask > > . Shared . Rent ( handlerCount ) ;
549+
550+ try
551+ {
552+ _notFoundHandlers . CopyTo ( notFoundHandlersCopy ) ;
553+
554+ var notFoundTasks = new HashSet < Task > ( ) ;
555+
556+ for ( var i = 0 ; i < handlerCount ; i ++ )
557+ {
558+ var handlerTask = InvokeNotFoundHandlerAsync ( notFoundHandlersCopy [ i ] , context ) ;
559+
560+ if ( handlerTask . IsFaulted )
561+ {
562+ await handlerTask ;
563+ return false ; // Unreachable because the previous line will throw.
564+ }
565+
566+ if ( context . DidPreventRendering )
567+ {
568+ return false ;
569+ }
570+
571+ notFoundTasks . Add ( handlerTask . AsTask ( ) ) ;
572+ }
573+
574+ while ( notFoundTasks . Count != 0 )
575+ {
576+ var completedHandlerTask = await Task . WhenAny ( notFoundTasks ) . WaitAsync ( cancellationToken ) ;
577+
578+ if ( completedHandlerTask . IsFaulted )
579+ {
580+ await completedHandlerTask ;
581+ return false ; // Unreachable because the previous line will throw.
582+ }
583+
584+ notFoundTasks . Remove ( completedHandlerTask ) ;
585+ }
586+ }
587+ finally
588+ {
589+ ArrayPool < Func < NotFoundContext , ValueTask > > . Shared . Return ( notFoundHandlersCopy ) ;
590+ }
591+ }
592+
593+ return ! context . DidPreventRendering ;
594+ }
595+ catch ( TaskCanceledException ex )
596+ {
597+ if ( ex . CancellationToken == cancellationToken )
598+ {
599+ // This navigation was in progress when a successive navigation occurred.
600+ // We treat this as a canceled navigation.
601+ return false ;
602+ }
603+
604+ throw ;
605+ }
606+ finally
607+ {
608+ cts . Cancel ( ) ;
609+ cts . Dispose ( ) ;
610+
611+ if ( _notFoundCts == cts )
612+ {
613+ _notFoundCts = null ;
614+ }
615+ }
616+ }
617+
442618 private async ValueTask InvokeLocationChangingHandlerAsync ( Func < LocationChangingContext , ValueTask > handler , LocationChangingContext context )
443619 {
444620 try
@@ -455,6 +631,22 @@ private async ValueTask InvokeLocationChangingHandlerAsync(Func<LocationChanging
455631 }
456632 }
457633
634+ private async ValueTask InvokeNotFoundHandlerAsync ( Func < NotFoundContext , ValueTask > handler , NotFoundContext context )
635+ {
636+ try
637+ {
638+ await handler ( context ) ;
639+ }
640+ catch ( OperationCanceledException )
641+ {
642+ // Ignore exceptions caused by cancellations.
643+ }
644+ catch ( Exception ex )
645+ {
646+ HandleNotFoundHandlerException ( ex , context ) ;
647+ }
648+ }
649+
458650 /// <summary>
459651 /// Handles exceptions thrown in location changing handlers.
460652 /// </summary>
@@ -463,6 +655,14 @@ private async ValueTask InvokeLocationChangingHandlerAsync(Func<LocationChanging
463655 protected virtual void HandleLocationChangingHandlerException ( Exception ex , LocationChangingContext context )
464656 => throw new InvalidOperationException ( $ "To support navigation locks, { GetType ( ) . Name } must override { nameof ( HandleLocationChangingHandlerException ) } ") ;
465657
658+ /// <summary>
659+ /// Handles exceptions thrown in NotFound rendering handlers.
660+ /// </summary>
661+ /// <param name="ex">The exception to handle.</param>
662+ /// <param name="context">The context passed to the handler.</param>
663+ protected virtual void HandleNotFoundHandlerException ( Exception ex , NotFoundContext context )
664+ => throw new InvalidOperationException ( $ "To support not found rendering locks, { GetType ( ) . Name } must override { nameof ( HandleNotFoundHandlerException ) } ") ;
665+
466666 /// <summary>
467667 /// Sets whether navigation is currently locked. If it is, then implementations should not update <see cref="Uri"/> and call
468668 /// <see cref="NotifyLocationChanged(bool)"/> until they have first confirmed the navigation by calling
0 commit comments