1using System.Collections.Concurrent;
17 private readonly Stack<BaseContentPage> screenStack =
new();
18 private readonly Stack<NavigationArgs?> navigationArgsStack =
new();
19 private readonly ConcurrentQueue<Func<Task>> taskQueue =
new();
20 private readonly Dictionary<string, NavigationArgs> navigationArgsMap =
new();
22 private bool isNavigating;
23 private bool isExecuting;
26 private bool IsBusy => this.isNavigating || this.isExecuting;
31 ??
throw new InvalidOperationException(
"CustomShell presenter not found.");
33 #region INavigationService implementation
39 public Task
GoToAsync(
string Route) => this.
GoToAsync(Route, Services.UI.BackMethod.Inherited,
null);
50 return this.Enqueue(async () =>
52 NavigationArgs? parent = this.navigationArgsStack.Count > 0 ? this.navigationArgsStack.Peek() :
null;
55 this.latestArguments = navArgs;
56 this.PushArgs(Route, navArgs);
59 ??
throw new InvalidOperationException($
"No page registered for route '{Route}' or not a BaseContentPage");
61 this.navigationArgsStack.Push(navArgs);
62 this.screenStack.Push(page);
64 Task initializationTask = Task.CompletedTask;
67 this.isNavigating =
true;
69 this.Presenter.UpdateBars(page);
70 initializationTask = EnsureInitializedAsync(page);
71 _ = page.Dispatcher.Dispatch(async () =>
75 await initializationTask;
88 finally { this.isNavigating =
false; }
95 return this.Enqueue(async () =>
97 this.latestArguments =
null;
98 this.navigationArgsStack.Push(
null);
99 this.screenStack.Push(Page);
100 Task initializationTask = Task.CompletedTask;
103 this.isNavigating =
true;
105 this.Presenter.UpdateBars(Page);
106 initializationTask = EnsureInitializedAsync(Page);
107 _ = Page.Dispatcher.Dispatch(async () =>
111 await initializationTask;
118 finally { this.isNavigating =
false; }
125 return this.Enqueue(async () =>
128 ??
throw new InvalidOperationException($
"No page registered for route '{Route}' or not a BaseContentPage");
129 await this.SetRootInternalAsync(page,
null);
139 return this.Enqueue(async () =>
141 TArgs navArgs = Args ??
new TArgs();
142 this.latestArguments = navArgs;
144 ??
throw new InvalidOperationException($
"No page registered for route '{Route}' or not a BaseContentPage");
145 await this.SetRootInternalAsync(page, navArgs);
150 public Task
GoBackAsync() => this.Enqueue(this.InternalGoBackAsync);
155 if (this.screenStack.Count <= 1)
157 List<BaseContentPage> removedPages =
new();
158 bool transitionCompleted =
false;
159 this.isNavigating =
true;
162 while (this.screenStack.Count > 1)
164 _ = this.navigationArgsStack.Pop();
165 BaseContentPage popped = this.screenStack.Pop();
166 this.RemoveArgsFor(popped);
167 removedPages.Add(popped);
171 transitionCompleted =
true;
172 _ = root.Dispatcher.Dispatch(async () =>
174 try { await Task.Yield(); await root.OnAppearingAsync(); }
catch (Exception ex) {
ServiceRef.
LogService.LogException(ex); }
176 this.Presenter.UpdateBars(root);
179 finally { this.isNavigating =
false; }
182 for (
int i = 0; i < removedPages.Count; i++)
187 bool skipDisappearing = transitionCompleted && i == 0;
188 if (!skipDisappearing)
191 if (removed is IAsyncDisposable asyncDisposable)
192 await asyncDisposable.DisposeAsync();
193 else if (removed is IDisposable disposable)
194 disposable.Dispose();
203 if (this.latestArguments is TArgs match)
205 this.latestArguments =
null;
208 if (this.screenStack.Count > 0)
211 string route = Routing.GetRoute(page) ??
string.Empty;
212 if (TryGetPageName(route, out
string pageName))
214 foreach (KeyValuePair<string, NavigationArgs> kv
in this.navigationArgsMap.ToList())
216 if (kv.Key.StartsWith(pageName, StringComparison.OrdinalIgnoreCase))
218 this.navigationArgsMap.Remove(kv.Key);
219 if (kv.Value is TArgs typed)
230 #region Back handling
232 public bool WouldHandleBack() =>
true;
234 public Task<bool> HandleBackAsync()
237 return Task.FromResult(
true);
238 TaskCompletionSource<bool> tcs =
new();
239 _ = this.Enqueue(async () =>
246 tcs.TrySetResult(true);
249 BaseContentPage? page =
this.screenStack.Count > 0 ?
this.screenStack.Peek() :
null;
252 try {
if (await pageHandler.OnBackButtonPressedAsync()) { tcs.TrySetResult(true); return; } }
catch (Exception ex) {
ServiceRef.
LogService.LogException(ex); }
256 try {
if (await vmHandler.OnBackButtonPressedAsync()) { tcs.TrySetResult(true); return; } }
catch (Exception ex) {
ServiceRef.
LogService.LogException(ex); }
258 if (this.screenStack.Count > 1)
260 await this.InternalGoBackAsync();
261 tcs.TrySetResult(true);
264 tcs.TrySetResult(
true);
269 tcs.TrySetResult(
true);
275 private async Task InternalGoBackAsync()
277 if (this.screenStack.Count <= 1)
279 List<BaseContentPage> removedPages =
new();
280 bool transitionCompleted =
false;
285 this.RemoveArgsFor(popped);
286 removedPages.Add(popped);
288 int levels = ComputeBackLevels(poppedArgs);
289 for (
int i = 1; i < levels && this.screenStack.Count > 1; i++)
291 _ = this.navigationArgsStack.Pop();
293 this.RemoveArgsFor(extra);
294 removedPages.Add(extra);
298 this.isNavigating =
true;
300 transitionCompleted =
true;
301 _ = target.Dispatcher.Dispatch(async () =>
305 this.Presenter.UpdateBars(target);
308 finally { this.isNavigating =
false; }
311 for (
int i = 0; i < removedPages.Count; i++)
316 bool skipDisappearing = transitionCompleted && i == 0;
317 if (!skipDisappearing)
320 if (removed is IAsyncDisposable asyncDisposable)
321 await asyncDisposable.DisposeAsync();
322 else if (removed is IDisposable disposable)
323 disposable.Dispose();
331 #region Queue & helpers
333 private Task Enqueue(Func<Task> action)
335 TaskCompletionSource<bool> tcs =
new();
336 this.taskQueue.Enqueue(async () =>
338 try { await action(); tcs.TrySetResult(
true); }
339 catch (Exception ex) { tcs.TrySetException(ex); }
341 if (!this.isExecuting)
343 this.isExecuting =
true;
344 MainThread.BeginInvokeOnMainThread(async () => await this.ProcessQueueAsync());
349 private async Task ProcessQueueAsync()
353 while (this.taskQueue.TryDequeue(out Func<Task>? action))
357 finally { this.isExecuting =
false; }
360 private static async Task EnsureInitializedAsync(
object obj)
364 System.Reflection.PropertyInfo? prop = obj.GetType().GetProperty(
"IsInitialized",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic);
366 if (prop?.CanRead ==
true)
367 isInit = (bool)(prop.GetValue(obj) ??
false);
370 await lcv.OnInitializeAsync();
371 if (prop?.CanWrite ==
true)
372 prop.SetValue(obj,
true);
384 if (
string.IsNullOrWhiteSpace(backRoute))
386 return backRoute.Split(
'/', StringSplitOptions.RemoveEmptyEntries).Count(s => s ==
"..");
393 this.latestArguments = Args;
394 if (TryGetPageName(Route, out
string pageName))
396 string key = pageName;
397 if (!
string.IsNullOrEmpty(Args.
UniqueId))
398 key +=
"?UniqueId=" + Args.
UniqueId;
399 this.navigationArgsMap[key] = Args;
403 private void RemoveArgsFor(
object view)
405 string route =
string.Empty;
406 if (view is BindableObject bindable)
407 route = Routing.GetRoute(bindable) ??
string.Empty;
408 if (TryGetPageName(route, out
string pageName))
410 foreach (KeyValuePair<string, NavigationArgs> kv
in this.navigationArgsMap.ToList())
412 if (kv.Key.StartsWith(pageName, StringComparison.OrdinalIgnoreCase))
413 this.navigationArgsMap.Remove(kv.Key);
418 private static bool TryGetPageName(
string Route, out
string PageName)
420 PageName =
string.Empty;
421 if (!
string.IsNullOrWhiteSpace(Route))
423 string trimmed = Route.TrimStart(
'.',
'/');
424 if (!
string.IsNullOrWhiteSpace(trimmed))
435 List<BaseContentPage> removedPages =
new();
436 while (this.screenStack.Count > 0)
439 removedPages.Add(existing);
440 this.RemoveArgsFor(existing);
442 this.navigationArgsStack.Clear();
443 this.navigationArgsMap.Clear();
444 this.latestArguments = Args;
445 this.navigationArgsStack.Push(
null);
446 this.screenStack.Push(Page);
447 if (Args is not
null)
449 string route = Routing.GetRoute(Page) ?? Page.GetType().Name;
450 this.PushArgs(route, Args);
452 Task initializationTask = Task.CompletedTask;
453 bool transitionCompleted =
false;
456 this.isNavigating =
true;
458 transitionCompleted =
true;
459 this.Presenter.UpdateBars(Page);
460 initializationTask = EnsureInitializedAsync(Page);
461 _ = Page.Dispatcher.Dispatch(async () =>
465 await initializationTask;
478 finally { this.isNavigating =
false; }
481 BaseContentPage? outgoingPage = removedPages.Count > 0 ? removedPages[0] :
null;
486 bool skipDisappearing = transitionCompleted && ReferenceEquals(removed, outgoingPage);
487 if (!skipDisappearing)
490 if (removed is IAsyncDisposable asyncDisposable)
491 await asyncDisposable.DisposeAsync();
492 else if (removed is IDisposable disposable)
493 disposable.Dispose();
Base class that references services in the app.
static IServiceProvider Provider
The service provider for the app. This is set before the app is started, and will be used to resolve ...
static ILogService LogService
Log service.
static IPopupService PopupService
Popup service for presenting application popups.
An base class holding page specific navigation parameters.
string? UniqueId
An unique view identifier used to search the args of similar view types.
readonly TaskCompletionSource< bool > NavigationCompletionSource
The completion source for the navigation task. Will return true when the navigation and transitions a...
void SetBackArguments(NavigationArgs? ParentArgs, BackMethod BackMethod=BackMethod.Inherited, string? UniqueId=null)
Sets the reference to the main parent's NavigationArgs.
string GetBackRoute()
Get the route used for the IUiService.GoBackAsync method.
Navigation service managing a custom stack of BaseContentPage instances. Modal navigation is deprecat...
Task GoBackAsync()
Pops the current page from the navigation stack and displays the previous page.
Task GoToAsync(string Route)
Navigates to the specified route and pushes the page onto the navigation stack.
Task SetRootAsync< TArgs >(string Route, TArgs? Args)
Replaces the entire navigation stack with the page registered for the given route....
Task GoToAsync(BaseContentPage Page)
Navigates directly to a shell-hosted view.
Task GoToAsync< TArgs >(string Route, TArgs? Args)
Navigates to the specified route and pushes the page onto the navigation stack with navigation argume...
Task GoToAsync(string Route, BackMethod BackMethod, string? UniqueId=null)
Navigates to the specified route and pushes the page onto the navigation stack specifying back method...
Task PopToRootAsync()
Pops all pages until only the root page remains on the navigation stack.
BaseContentPage? CurrentPage
Gets the current visible view.
Task SetRootAsync(string Route)
Replaces the entire navigation stack with the page registered for the given route....
Task SetRootAsync(BaseContentPage Page)
Replaces the entire navigation stack with the provided page instance. After completion,...
A base class for all pages, intended for custom navigation with explicit life-cycle events.
virtual async Task OnAppearingAsync()
Called when the page is about to be shown. Triggers restore state, appearing logic,...
virtual async Task OnDisappearingAsync()
Called when the page is about to be hidden. Triggers save state, disappearing logic,...
virtual async Task OnDisposeAsync()
Override to unregister event handlers, cleanup, etc. Called when page is permanently removed.
Static class managing the application event log. Applications and services log events on this static ...
static Exception UnnestException(Exception Exception)
Unnests an exception, to extract the relevant inner exception.
Service for navigating between pages using route-based navigation.
Presenter abstraction host for page, popup and toast transitions.
Interface for views who need to react to life-cycle events.
BackMethod
Navigation Back Method
TransitionType
Transition types for page navigation in CustomShell.