Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
NavigationService.cs
1using System.Collections.Concurrent;
6
7
9{
14 [Singleton]
16 {
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();
21
22 private bool isNavigating;
23 private bool isExecuting;
24 private NavigationArgs? latestArguments;
25
26 private bool IsBusy => this.isNavigating || this.isExecuting;
27
28 private IShellPresenter Presenter =>
29 (Application.Current.MainPage as NavigationPage)?.CurrentPage as IShellPresenter
30 ?? Application.Current.MainPage as IShellPresenter
31 ?? throw new InvalidOperationException("CustomShell presenter not found.");
32
33 #region INavigationService implementation
34
36 public BaseContentPage? CurrentPage => this.screenStack.Count > 0 ? this.screenStack.Peek() : null;
37
39 public Task GoToAsync(string Route) => this.GoToAsync(Route, Services.UI.BackMethod.Inherited, null);
40
42 public Task GoToAsync(string Route, BackMethod BackMethod, string? UniqueId = null) => this.GoToAsync<NavigationArgs>(Route, null, BackMethod, UniqueId);
43
45 public Task GoToAsync<TArgs>(string Route, TArgs? Args) where TArgs : NavigationArgs, new() => this.GoToAsync(Route, Args, Services.UI.BackMethod.Inherited, null);
46
48 public Task GoToAsync<TArgs>(string Route, TArgs? Args, BackMethod BackMethod, string? UniqueId = null) where TArgs : NavigationArgs, new()
49 {
50 return this.Enqueue(async () =>
51 {
52 NavigationArgs? parent = this.navigationArgsStack.Count > 0 ? this.navigationArgsStack.Peek() : null;
53 NavigationArgs navArgs = Args ?? new TArgs();
54 navArgs.SetBackArguments(parent, BackMethod, UniqueId);
55 this.latestArguments = navArgs;
56 this.PushArgs(Route, navArgs);
57
58 BaseContentPage page = Routing.GetOrCreateContent(Route, ServiceRef.Provider) as BaseContentPage
59 ?? throw new InvalidOperationException($"No page registered for route '{Route}' or not a BaseContentPage");
60
61 this.navigationArgsStack.Push(navArgs);
62 this.screenStack.Push(page);
63
64 Task initializationTask = Task.CompletedTask;
65 try
66 {
67 this.isNavigating = true;
68 await this.Presenter.ShowScreen(page, TransitionType.Fade);
69 this.Presenter.UpdateBars(page);
70 initializationTask = EnsureInitializedAsync(page);
71 _ = page.Dispatcher.Dispatch(async () =>
72 {
73 try
74 {
75 await initializationTask;
76 await Task.Yield();
77 await page.OnAppearingAsync();
78 }
79 catch (Exception ex) { ServiceRef.LogService.LogException(Waher.Events.Log.UnnestException(ex)); }
80 finally { navArgs.NavigationCompletionSource.TrySetResult(true); }
81 });
82 }
83 catch (Exception ex)
84 {
86 navArgs.NavigationCompletionSource.TrySetResult(false);
87 }
88 finally { this.isNavigating = false; }
89 });
90 }
91
93 public Task GoToAsync(BaseContentPage Page)
94 {
95 return this.Enqueue(async () =>
96 {
97 this.latestArguments = null;
98 this.navigationArgsStack.Push(null);
99 this.screenStack.Push(Page);
100 Task initializationTask = Task.CompletedTask;
101 try
102 {
103 this.isNavigating = true;
104 await this.Presenter.ShowScreen(Page, TransitionType.Fade);
105 this.Presenter.UpdateBars(Page);
106 initializationTask = EnsureInitializedAsync(Page);
107 _ = Page.Dispatcher.Dispatch(async () =>
108 {
109 try
110 {
111 await initializationTask;
112 await Task.Yield();
113 await Page.OnAppearingAsync();
114 }
115 catch (Exception ex) { ServiceRef.LogService.LogException(Waher.Events.Log.UnnestException(ex)); }
116 });
117 }
118 finally { this.isNavigating = false; }
119 });
120 }
121
123 public Task SetRootAsync(string Route)
124 {
125 return this.Enqueue(async () =>
126 {
127 BaseContentPage page = Routing.GetOrCreateContent(Route, ServiceRef.Provider) as BaseContentPage
128 ?? throw new InvalidOperationException($"No page registered for route '{Route}' or not a BaseContentPage");
129 await this.SetRootInternalAsync(page, null);
130 });
131 }
132
134 public Task SetRootAsync(BaseContentPage Page) => this.Enqueue(async () => await this.SetRootInternalAsync(Page, null));
135
137 public Task SetRootAsync<TArgs>(string Route, TArgs? Args) where TArgs : NavigationArgs, new()
138 {
139 return this.Enqueue(async () =>
140 {
141 TArgs navArgs = Args ?? new TArgs();
142 this.latestArguments = navArgs;
143 BaseContentPage page = Routing.GetOrCreateContent(Route, ServiceRef.Provider) as BaseContentPage
144 ?? throw new InvalidOperationException($"No page registered for route '{Route}' or not a BaseContentPage");
145 await this.SetRootInternalAsync(page, navArgs);
146 });
147 }
148
150 public Task GoBackAsync() => this.Enqueue(this.InternalGoBackAsync);
151
153 public Task PopToRootAsync() => this.Enqueue(async () =>
154 {
155 if (this.screenStack.Count <= 1)
156 return;
157 List<BaseContentPage> removedPages = new();
158 bool transitionCompleted = false;
159 this.isNavigating = true;
160 try
161 {
162 while (this.screenStack.Count > 1)
163 {
164 _ = this.navigationArgsStack.Pop();
165 BaseContentPage popped = this.screenStack.Pop();
166 this.RemoveArgsFor(popped);
167 removedPages.Add(popped);
168 }
169 BaseContentPage root = this.screenStack.Peek();
170 await this.Presenter.ShowScreen(root, TransitionType.Fade);
171 transitionCompleted = true;
172 _ = root.Dispatcher.Dispatch(async () =>
173 {
174 try { await Task.Yield(); await root.OnAppearingAsync(); } catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
175 });
176 this.Presenter.UpdateBars(root);
177 }
178 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
179 finally { this.isNavigating = false; }
180
181 await Task.Yield();
182 for (int i = 0; i < removedPages.Count; i++)
183 {
184 BaseContentPage removed = removedPages[i];
185 try
186 {
187 bool skipDisappearing = transitionCompleted && i == 0;
188 if (!skipDisappearing)
189 await removed.OnDisappearingAsync();
190 await removed.OnDisposeAsync();
191 if (removed is IAsyncDisposable asyncDisposable)
192 await asyncDisposable.DisposeAsync();
193 else if (removed is IDisposable disposable)
194 disposable.Dispose();
195 }
196 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
197 }
198 });
199
201 public TArgs? PopLatestArgs<TArgs>() where TArgs : NavigationArgs, new()
202 {
203 if (this.latestArguments is TArgs match)
204 {
205 this.latestArguments = null;
206 return match;
207 }
208 if (this.screenStack.Count > 0)
209 {
210 BaseContentPage page = this.screenStack.Peek();
211 string route = Routing.GetRoute(page) ?? string.Empty;
212 if (TryGetPageName(route, out string pageName))
213 {
214 foreach (KeyValuePair<string, NavigationArgs> kv in this.navigationArgsMap.ToList())
215 {
216 if (kv.Key.StartsWith(pageName, StringComparison.OrdinalIgnoreCase))
217 {
218 this.navigationArgsMap.Remove(kv.Key);
219 if (kv.Value is TArgs typed)
220 return typed;
221 }
222 }
223 }
224 }
225 return null;
226 }
227
228 #endregion
229
230 #region Back handling
231
232 public bool WouldHandleBack() => true; // Always intercept
233
234 public Task<bool> HandleBackAsync()
235 {
236 if (this.IsBusy)
237 return Task.FromResult(true);
238 TaskCompletionSource<bool> tcs = new();
239 _ = this.Enqueue(async () =>
240 {
241 try
242 {
244 {
245 // PopupService handles dismissal via shell event; just consume.
246 tcs.TrySetResult(true);
247 return;
248 }
249 BaseContentPage? page = this.screenStack.Count > 0 ? this.screenStack.Peek() : null;
250 if (page is IBackButtonHandler pageHandler)
251 {
252 try { if (await pageHandler.OnBackButtonPressedAsync()) { tcs.TrySetResult(true); return; } } catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
253 }
254 if (page?.BindingContext is IBackButtonHandler vmHandler)
255 {
256 try { if (await vmHandler.OnBackButtonPressedAsync()) { tcs.TrySetResult(true); return; } } catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
257 }
258 if (this.screenStack.Count > 1)
259 {
260 await this.InternalGoBackAsync();
261 tcs.TrySetResult(true);
262 return;
263 }
264 tcs.TrySetResult(true); // consume
265 }
266 catch (Exception ex)
267 {
268 ServiceRef.LogService.LogException(ex);
269 tcs.TrySetResult(true);
270 }
271 });
272 return tcs.Task;
273 }
274
275 private async Task InternalGoBackAsync()
276 {
277 if (this.screenStack.Count <= 1)
278 return;
279 List<BaseContentPage> removedPages = new();
280 bool transitionCompleted = false;
281 try
282 {
283 NavigationArgs? poppedArgs = this.navigationArgsStack.Pop();
284 BaseContentPage popped = this.screenStack.Pop();
285 this.RemoveArgsFor(popped);
286 removedPages.Add(popped);
287
288 int levels = ComputeBackLevels(poppedArgs);
289 for (int i = 1; i < levels && this.screenStack.Count > 1; i++)
290 {
291 _ = this.navigationArgsStack.Pop();
292 BaseContentPage extra = this.screenStack.Pop();
293 this.RemoveArgsFor(extra);
294 removedPages.Add(extra);
295 }
296
297 BaseContentPage target = this.screenStack.Peek();
298 this.isNavigating = true;
299 await this.Presenter.ShowScreen(target, TransitionType.Fade);
300 transitionCompleted = true;
301 _ = target.Dispatcher.Dispatch(async () =>
302 {
303 try { await Task.Yield(); await target.OnAppearingAsync(); } catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
304 });
305 this.Presenter.UpdateBars(target);
306 }
307 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
308 finally { this.isNavigating = false; }
309
310 await Task.Yield();
311 for (int i = 0; i < removedPages.Count; i++)
312 {
313 BaseContentPage removed = removedPages[i];
314 try
315 {
316 bool skipDisappearing = transitionCompleted && i == 0;
317 if (!skipDisappearing)
318 await removed.OnDisappearingAsync();
319 await removed.OnDisposeAsync();
320 if (removed is IAsyncDisposable asyncDisposable)
321 await asyncDisposable.DisposeAsync();
322 else if (removed is IDisposable disposable)
323 disposable.Dispose();
324 }
325 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
326 }
327 }
328
329 #endregion
330
331 #region Queue & helpers
332
333 private Task Enqueue(Func<Task> action)
334 {
335 TaskCompletionSource<bool> tcs = new();
336 this.taskQueue.Enqueue(async () =>
337 {
338 try { await action(); tcs.TrySetResult(true); }
339 catch (Exception ex) { tcs.TrySetException(ex); }
340 });
341 if (!this.isExecuting)
342 {
343 this.isExecuting = true;
344 MainThread.BeginInvokeOnMainThread(async () => await this.ProcessQueueAsync());
345 }
346 return tcs.Task;
347 }
348
349 private async Task ProcessQueueAsync()
350 {
351 try
352 {
353 while (this.taskQueue.TryDequeue(out Func<Task>? action))
354 await action();
355 }
356 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
357 finally { this.isExecuting = false; }
358 }
359
360 private static async Task EnsureInitializedAsync(object obj)
361 {
362 if (obj is ILifeCycleView lcv)
363 {
364 System.Reflection.PropertyInfo? prop = obj.GetType().GetProperty("IsInitialized", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
365 bool isInit = false;
366 if (prop?.CanRead == true)
367 isInit = (bool)(prop.GetValue(obj) ?? false);
368 if (!isInit)
369 {
370 await lcv.OnInitializeAsync();
371 if (prop?.CanWrite == true)
372 prop.SetValue(obj, true);
373 }
374 }
375 }
376
377 private static int ComputeBackLevels(NavigationArgs? args)
378 {
379 if (args is null)
380 return 1;
381 try
382 {
383 string backRoute = args.GetBackRoute();
384 if (string.IsNullOrWhiteSpace(backRoute))
385 return 1;
386 return backRoute.Split('/', StringSplitOptions.RemoveEmptyEntries).Count(s => s == "..");
387 }
388 catch { return 1; }
389 }
390
391 private void PushArgs(string Route, NavigationArgs Args)
392 {
393 this.latestArguments = Args;
394 if (TryGetPageName(Route, out string pageName))
395 {
396 string key = pageName;
397 if (!string.IsNullOrEmpty(Args.UniqueId))
398 key += "?UniqueId=" + Args.UniqueId;
399 this.navigationArgsMap[key] = Args;
400 }
401 }
402
403 private void RemoveArgsFor(object view)
404 {
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))
409 {
410 foreach (KeyValuePair<string, NavigationArgs> kv in this.navigationArgsMap.ToList())
411 {
412 if (kv.Key.StartsWith(pageName, StringComparison.OrdinalIgnoreCase))
413 this.navigationArgsMap.Remove(kv.Key);
414 }
415 }
416 }
417
418 private static bool TryGetPageName(string Route, out string PageName)
419 {
420 PageName = string.Empty;
421 if (!string.IsNullOrWhiteSpace(Route))
422 {
423 string trimmed = Route.TrimStart('.', '/');
424 if (!string.IsNullOrWhiteSpace(trimmed))
425 {
426 PageName = trimmed;
427 return true;
428 }
429 }
430 return false;
431 }
432
433 private async Task SetRootInternalAsync(BaseContentPage Page, NavigationArgs? Args)
434 {
435 List<BaseContentPage> removedPages = new();
436 while (this.screenStack.Count > 0)
437 {
438 BaseContentPage existing = this.screenStack.Pop();
439 removedPages.Add(existing);
440 this.RemoveArgsFor(existing);
441 }
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)
448 {
449 string route = Routing.GetRoute(Page) ?? Page.GetType().Name;
450 this.PushArgs(route, Args);
451 }
452 Task initializationTask = Task.CompletedTask;
453 bool transitionCompleted = false;
454 try
455 {
456 this.isNavigating = true;
457 await this.Presenter.ShowScreen(Page, TransitionType.Fade);
458 transitionCompleted = true;
459 this.Presenter.UpdateBars(Page);
460 initializationTask = EnsureInitializedAsync(Page);
461 _ = Page.Dispatcher.Dispatch(async () =>
462 {
463 try
464 {
465 await initializationTask;
466 await Task.Yield();
467 await Page.OnAppearingAsync();
468 }
469 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
470 finally { Args?.NavigationCompletionSource.TrySetResult(true); }
471 });
472 }
473 catch (Exception ex)
474 {
475 ServiceRef.LogService.LogException(ex);
476 Args?.NavigationCompletionSource.TrySetResult(false);
477 }
478 finally { this.isNavigating = false; }
479
480 await Task.Yield();
481 BaseContentPage? outgoingPage = removedPages.Count > 0 ? removedPages[0] : null;
482 foreach (BaseContentPage removed in removedPages)
483 {
484 try
485 {
486 bool skipDisappearing = transitionCompleted && ReferenceEquals(removed, outgoingPage);
487 if (!skipDisappearing)
488 await removed.OnDisappearingAsync();
489 await removed.OnDisposeAsync();
490 if (removed is IAsyncDisposable asyncDisposable)
491 await asyncDisposable.DisposeAsync();
492 else if (removed is IDisposable disposable)
493 disposable.Dispose();
494 }
495 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
496 }
497 }
498
499 #endregion
500 }
501}
Base class that references services in the app.
Definition: ServiceRef.cs:43
static IServiceProvider Provider
The service provider for the app. This is set before the app is started, and will be used to resolve ...
Definition: ServiceRef.cs:48
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
static IPopupService PopupService
Popup service for presenting application popups.
Definition: ServiceRef.cs:142
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 ...
Definition: Log.cs:14
static Exception UnnestException(Exception Exception)
Unnests an exception, to extract the relevant inner exception.
Definition: Log.cs:828
Implement on a Page or its ViewModel to intercept back button presses. Return true to consume (preven...
Service for navigating between pages using route-based navigation.
Presenter abstraction host for page, popup and toast transitions.
bool HasOpenPopups
Returns true if at least one popup is currently presented.
Interface for views who need to react to life-cycle events.
Definition: ImplTypes.g.cs:58
BackMethod
Navigation Back Method
Definition: BackMethod.cs:7
TransitionType
Transition types for page navigation in CustomShell.
Definition: CustomShell.cs:662
Definition: App.xaml.cs:4